Skip to content

Fix/video lazy loading preload none#1

Open
bgrana75 wants to merge 1294 commits intomainfrom
fix/video-lazy-loading-preload-none
Open

Fix/video lazy loading preload none#1
bgrana75 wants to merge 1294 commits intomainfrom
fix/video-lazy-loading-preload-none

Conversation

@bgrana75
Copy link

@bgrana75 bgrana75 commented Nov 2, 2025

Problem

IPFS videos cause network spikes because preload="metadata" downloads video data immediately on page load, bypassing the Intersection Observer's lazy loading intent.

Solution

Changed preload="metadata" to preload="none" in VideoRenderer.tsx and added explicit .load() call when video enters viewport.

Changes:

  • Line 455: preload="none" prevents any download until triggered
  • Line 397: Added videoRef.current.load() when isInView becomes true

Impact

✅ Videos only load when scrolled into view
✅ No more simultaneous IPFS requests on page load
✅ Significantly improved feed performance

Testing

Tested with posts like @garciarodrigues/20251101t001357625z. Network tab now shows videos load progressively as you scroll instead of all at once.

Summary by CodeRabbit

  • New Features

    • Farcaster notifications integration for account updates
    • Enhanced VIP signup system with key management
    • Multi-chain token airdrop system with recipient filtering
    • Embedded game page with fullscreen support
    • Chat widget for user support
    • Magazine view for content discovery
    • Witness voting functionality
    • Dynamic coin trading and portfolio pages
  • UI/UX Improvements

    • Expanded theme selection and styling system
    • Improved responsive design and layouts
    • Enhanced visual components and branding
  • Documentation

    • Setup guides and configuration documentation for development

sktbrd and others added 30 commits August 11, 2025 20:51
- Add CoinHeader component for displaying coin details and metadata editing.
- Create CoinStats component to show market cap, total volume, and creator earnings.
- Introduce CoinTabs component for navigating between comments, holders, and details.
- Develop MediaRenderer component for handling media display with IPFS support.
- Implement TradeInterface component for buying and selling coins with a user-friendly interface.
- Add useCoinTrading hook to manage trading logic, including fetching quotes and executing trades.
- Create utility functions for formatting numbers, currency, and dates.
- Define CoinData and UserBalance types for better type safety and clarity.
- Update index file to export all new components for easier imports.
Introduces a new 'Missing Witness' filter option across airdrop components, allowing selection and sorting of active users who haven't voted for a witness. Updates useAirdropManager logic to support this filter, refines sort handling, and improves airdrop modal UX by auto-closing on transaction completion or failure. Also updates announcement content formatting and type definitions to support the new filter.
…humbnail generation, and optimized video processing logic
- Integrated VideoTrimModal for trimming videos before upload.
- Implemented logic to check video duration and enforce limits based on user's Hive Power.
- Updated SnapComposer to handle video file selection and processing.
- Enhanced VideoUploader to generate thumbnails and handle video uploads.
- Added UI elements for video trimming, including play/pause controls and timeline sliders.
- Improved error handling during video metadata loading in VideoUploader.
- Added thumbnail capture component to allow users to capture custom thumbnails from video frames.
- Implemented VideoPlayer component for better video playback control.
- Created VideoTimeline component for visualizing and selecting video segments.
- Developed VideoTrimModal for trimming videos with a user-friendly interface.
- Added VideoTrimModalFooter for action buttons related to trimming and bypassing.
- Introduced utility functions for time formatting and clamping values
…rror-413

Add mobile video compression fallback
…r handling; update next.config.mjs to optimize production builds; enhance package.json scripts for better development workflow; clean up pnpm-lock.yaml by removing deprecated types; add polyfills for server-side rendering to support missing browser APIs.
sktbrd and others added 28 commits October 13, 2025 15:14
- Deleted obsolete documentation files related to browser-safe hash implementation, enhanced device detection, React key duplication fix, and video upload enhancement.
- Removed unused hooks: useFarcasterNotifications, useInstagramEmbeds, usePosts, useVideoPreloader, and useGifUpload.
- Updated hashUtils to streamline hashing methods, focusing on simpleHash for cache keys and removing SHA-256 implementation.
… for better clarity

refactor: Enhance error handling and logging in useCoinCreation for improved debugging
feat: Add new error log entry for unknown JavaScript errors in client-errors.log
- Created README.md for scripts directory detailing structure and usage
- Implemented fix-auth-ott-schema.js to fix schema issues in auth_ott table
- Added inspect-schema.js to inspect current database table schemas
- Developed migrate-database.sh for database migration tasks
- Introduced index.js as a command runner for various scripts
- Created generate-vip.js for generating new VIP invitation codes
- Added SQL migration scripts for VIP code compatibility and signup schema
- Defined TypeScript types for signup-related database tables
- Change preload='metadata' to preload='none' in VideoRenderer
- Add explicit .load() call when video enters viewport via Intersection Observer
- Prevents network spikes from IPFS videos downloading metadata immediately on page load
- Videos now only start loading when scrolled into view
- Fixes performance issues with posts containing IPFS iframes (e.g., @garciarodrigues/20251101t001357625z)

Technical Details:
- preload='metadata' was causing all videos to download metadata immediately
- With Intersection Observer, videos should only load when visible
- Added readyState check to ensure .load() is only called once
- Network requests now spread out naturally as user scrolls

Performance Impact:
- Eliminates hundreds of simultaneous IPFS requests on page load
- Reduces network bandwidth usage significantly
- Improves initial page load time and scrolling performance
@coderabbitai
Copy link

coderabbitai bot commented Nov 2, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

This PR introduces comprehensive infrastructure, documentation, and feature expansion for Skatehive 3.0. It adds development environment configuration, extensive environment variable structure, detailed setup and coding guidelines, a complex signup/VIP system with Supabase integration, Farcaster webhook and notification handling, new pages and API routes for auctions, blogs, coins, leaderboards, and an advanced airdrop distribution system supporting both Hive and ERC-20 tokens. Major routing refactors consolidate slug-based pages into explicit routes and introduce a client-side layout wrapper.

Changes

Cohort / File(s) Summary
Development & Environment Configuration
.devcontainer/devcontainer.json, .env.local.example, .gitignore, .vscode/settings.json
Added Node.js 20 devcontainer with pnpm and VSCode extensions; expanded .env.local with organized sections for Hive API, Supabase, Farcaster, email, database, and other services; added cache and build artifact ignore patterns; configured VSCode editor settings with file visibility controls
Core Documentation
LICENSE, README.md, AGENTS.md, RULES.md, SKATEHIVE_SIGNUP_SYSTEM.md
Added MIT license; rewrote README with Skatehive 3.0 features and pnpm setup; added AGENTS.md with tech stack and provider architecture; added RULES.md with TypeScript/Next.js conventions; added detailed SKATEHIVE_SIGNUP_SYSTEM.md documenting VIP signup architecture, API endpoints, database schema, and security measures
Core Layout & Metadata
app/layout.tsx, app/page.tsx, app/globals.css, app/providers.tsx, app/robots.ts, app/sitemap.ts
Refactored root layout with splash screen, hydration-aware rendering, and RootLayoutClient wrapper; added site-wide metadata, viewport, and fc:frame exports; updated global CSS with custom fonts, AIOHA modal styling, and scrollbar suppression; expanded providers with wagmi, RainbowKit, OnchainKit, Aioha, and VoteWeight integration; added robots.txt and dynamic sitemap generation
Routing Refactors
app/[...slug]/page.tsx (deleted), app/HomePageClient.tsx, app/RootLayoutClient.tsx, app/.well-known/farcaster.json/route.ts
Removed catch-all slug-based routing; added dedicated HomePageClient with responsive layout and mobile drawer support; introduced RootLayoutClient with search overlay, airdrop modal, leaderboard data fetching, and hydration-safe rendering; added Farcaster manifest endpoint
Authentication & Signup System
app/api/auth/ott/route.ts, app/api/signup/init/route.ts, app/api/signup/submit/route.ts, app/api/signup/burn-code/route.ts, app/api/signup/test-email/route.ts, app/api/signup/key-backup/route.ts, app/api/signup/key-backup/[backup_id]/route.ts, app/signup/page.tsx
Implemented one-time-token (OTT) authentication with JWT support; added VIP signup flow with rate limiting, Argon2 password verification, and session management; integrated Hive account creation through signer service; added emergency key backup with encryption; implemented test email functionality; created multi-step signup UI with key generation and signer service health monitoring
Farcaster Integration
app/api/farcaster/cleanup/route.ts, app/api/farcaster/dev-register/route.ts, app/api/farcaster/init-db/route.ts, app/api/farcaster/link-skatehive/route.ts, app/api/farcaster/notify/route.ts, app/api/farcaster/notifications-queue/route.ts, app/api/farcaster/status/route.ts, app/api/farcaster/test-notifications/route.ts, app/api/farcaster/test-webhook/route.ts, app/api/farcaster/unlink/route.ts, app/api/farcaster/update-preferences/route.ts, app/api/farcaster/user-preferences/route.ts, app/api/farcaster/user-status/route.ts, app/api/farcaster/webhook/route.ts
Comprehensive Farcaster service layer with token management, user linking/unlinking, preference updates, notification routing, webhook handling, database initialization, and admin testing endpoints; integrated AutomatedNotificationService for notification processing and log management
Utility API Routes
app/api/admin/check/route.ts, app/api/cron/route.ts, app/api/database/init/route.ts, app/api/debug-hive-notifications/route.ts, app/api/invite/route.ts, app/api/logs/client-errors/route.ts, app/api/opengraph/route.ts, app/api/support/route.ts, app/api/test-notification/route.ts, app/api/upload-metadata/route.ts, app/api/video-proxy/route.ts, app/api/webhook/route.ts
Added admin status checking with security logging; implemented cron endpoint for notification processing; added database initialization and Farcaster notification debugging; created email invite flow; added client error logging with file persistence and filtering; implemented Open Graph metadata fetching; added support request mailer; created Pinata metadata upload and video proxy endpoints; added webhook handler for frame events
Media Upload Routes
app/api/instagram-download/route.ts, app/api/instagram-health/route.ts, app/api/generate-podium/route.ts, app/api/pinata/route.ts, app/api/pinata-chunked/route.ts, app/api/pinata-mobile/route.ts, app/api/pinata/metadata/[hash]/route.ts, app/api/portfolio/[address]/route.ts
Implemented Instagram media download with multiple server fallback and rate-limit detection; added Instagram health check with tiered endpoint probing; created dynamic SVG podium image generation with 3D styling; implemented Pinata file uploads (standard, chunked, and mobile-optimized) with metadata handling; added Pinata metadata caching with TTL; created portfolio aggregation endpoint
Auction System
app/auction/page.tsx, app/auction/[tokenId]/page.tsx
Added main auction page with dynamic metadata generation, latest auction fetching, and OG/Twitter preview; added token-specific auction page with per-token metadata (title, image, bid info) and dynamic routing
Blog System
app/blog/page.tsx, app/blog/README.md
Refactored blog page with configurable view modes (grid/magazine), GOAT batch fetching, deduplication logic, payout-based sorting, URL state synchronization, and responsive layout; added comprehensive blog system documentation covering architecture, components, configuration, and state management
Coin/Token Pages
app/coin/[address]/page.tsx, app/coin/[address]/ZoraCoinPageClient.tsx, app/coin/[address]/error.tsx, app/coin/[address]/loading.tsx, app/coin/[address]/not-found.tsx
Added coin detail page with server-side metadata generation, dynamic coin data fetching, contract metadata resolution with IPFS support, trading interface, and coin type detection; created client-side coin page component with error/loading/not-found states and trading panel
Feature Pages
app/bounties/page.tsx, app/chat/page.tsx, app/dao/page.tsx, app/game/page.tsx, app/game/GameClientPage.tsx, app/invite/page.tsx, app/magazine/page.tsx, app/map/page.tsx, app/wallet/page.tsx
Added metadata and routing for bounties, chat widget (external provider), DAO assets, and game pages; created game page with fullscreen toggle, iframe management, and keyboard shortcuts; implemented invite page with account creation flow, key generation, and email invite integration; added magazine mode page with community tag filtering; created map page with embedded component
Leaderboard System
app/leaderboard/page.tsx, app/leaderboard/leaderboardClient.tsx, app/leaderboard/RulesModal.tsx
Added leaderboard page with dynamic podium image generation, top-3 skater extraction, and complex metadata; created client-side leaderboard with sortable columns (points, power, posts, NFTs, donations, witness status), rank icons, memoized rows, and responsive design; added rules modal with detailed scoring documentation
Notifications & User Pages
app/notifications/page.tsx, app/notifications/NotificationsPageClient.tsx, app/user/[username]/page.tsx, app/user/[username]/snap/[permlink]/page.tsx, app/post/[author]/[permlink]/page.tsx
Added notifications page with hydration-aware authentication; created user profile page with dynamic OG metadata, profile data fetching, and follower/following counts; added snap (media) detail page with metadata generation and profile redirect; implemented post detail page with safe JSON parsing, text sanitization, and image extraction for OG tags
Settings & Witness
app/settings/page.tsx, app/settings/farcaster/page.tsx, app/witness/page.tsx
Refactored settings with tab-based UI (Main Settings/Advanced) and user data wiring; added Farcaster admin panel with custom notification sending and notification queue display; created witness page with Keychain SDK integration for vote casting
Compose & Posting
app/compose/page.tsx, app/compose/Editor.tsx (deleted)
Removed MDX editor component; refactored compose page with structured useComposeForm hook, image compression/upload, video uploader, GIF upload, drag-and-drop support, hashtag/beneficiary inputs, thumbnail picker, and multi-tab settings for captions and beneficiaries
Miscellaneous Pages
app/share/page.tsx, app/privacypolicy/page.tsx
Added share page for cast previews with Suspense boundary; created privacy policy page with multi-section static content and Chakra UI layout
Theme System
app/themeProvider.tsx
Expanded theme map with 17 themes (forest, bluesky, hacker variants, nounish, windows95, hiveBR, cannabis, gay, cyberpunk, paper, lime, mac, gruvbox variants); added localStorage persistence with hydration-safe initialization; introduced ThemeName type export; integrated additional fonts (Joystix, Mechanical, Tarrget3D)
Airdrop System
components/airdrop/AirdropManager.tsx, components/airdrop/AirdropModal.tsx, components/airdrop/TransactionStatusDisplay.tsx, components/airdrop/steps/ConfigurationStep.tsx, components/airdrop/steps/ConfirmationStep.tsx, components/airdrop/steps/PreviewStep.tsx, components/airdrop/steps/AnnouncementPreviewStep.tsx, components/airdrop/index.ts, components/airdrop/AIRDROP_SYSTEM_DOCS.md
Implemented comprehensive airdrop distribution system with multi-chain support (Hive and Base ERC-20); created modal-based workflow with token selection, recipient filtering (sort/limit options), weighted distribution, ERC-20 approval flows, React Flow network visualization, transaction status tracking, and announcement post generation; added detailed system documentation

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant VIPSignupAPI as /api/signup/init
    participant SignerService
    participant Supabase
    participant HiveBlockchain

    User->>Frontend: Enter username, email, VIP code
    Frontend->>VIPSignupAPI: POST (username, email, vip_code)
    VIPSignupAPI->>Supabase: Validate VIP code (Argon2 + pepper)
    VIPSignupAPI->>Supabase: Create signup_sessions (INIT status)
    VIPSignupAPI-->>Frontend: Return signup_token
    
    User->>Frontend: View & confirm generated keys
    Frontend->>Frontend: Generate Hive keys (owner, active, posting, memo)
    
    User->>Frontend: Click Create Account
    Frontend->>SignerService: POST to /api/signup/submit (pubkeys, backup_blob)
    SignerService->>SignerService: Validate & claim account credit
    SignerService->>HiveBlockchain: Create account operation
    HiveBlockchain-->>SignerService: Account created
    SignerService->>Supabase: Mark VIP code consumed
    SignerService->>Supabase: Create user record
    SignerService->>Supabase: Update signup_sessions (SUCCESS)
    SignerService->>Supabase: Generate OTT (10-min TTL)
    SignerService-->>Frontend: Return OTT
    
    Frontend->>Frontend: /api/auth/ott?ott=... to establish auth
    Frontend-->>User: Display success, OTT ready
Loading
sequenceDiagram
    participant Farcaster as Farcaster Network
    participant Webhook as /api/farcaster/webhook
    participant TokenStore
    participant Supabase
    participant NotificationService

    Farcaster->>Webhook: POST (miniapp_added event)
    Webhook->>Webhook: Validate signature
    Webhook->>TokenStore: Store token (fid, token, notification_url)
    Webhook->>Supabase: Upsert skatehive_farcaster_preferences
    Webhook->>Supabase: Log to farcaster_notification_logs
    Webhook-->>Farcaster: 200 OK
    
    Note over Webhook,Supabase: User enables notifications
    
    Farcaster->>Webhook: POST (notifications_enabled)
    Webhook->>TokenStore: Update notifications flag
    Webhook->>Supabase: Update preferences (notifications_enabled=true)
    Webhook-->>Farcaster: 200 OK
    
    Note over Webhook,NotificationService: Cron triggers
    
    NotificationService->>Supabase: Fetch unread notifications
    NotificationService->>TokenStore: Get all active tokens
    NotificationService->>NotificationService: Build HiveToFarcasterNotification
    NotificationService->>Webhook: Send via webhook URL
    Webhook-->>Farcaster: POST to notification endpoint
Loading
sequenceDiagram
    participant User
    participant Frontend as AirdropModal
    participant ERC20Service
    participant Base as Base Network
    participant Hive as Hive Network

    User->>Frontend: Select token (ERC-20 on Base)
    User->>Frontend: Configure recipients & amount
    Frontend->>ERC20Service: estimateGasCost()
    Frontend-->>User: Show cost estimate
    
    User->>Frontend: Click Approve Token
    Frontend->>Base: token.approve(airdropService, amount)
    Base-->>Frontend: Approval hash
    Frontend->>Frontend: Monitor & display approval status
    
    User->>Frontend: Execute Airdrop
    Frontend->>ERC20Service: executeAirdrop(recipients, amounts)
    ERC20Service->>Base: batchTransfer via multi-call
    Base-->>ERC20Service: Transaction hash
    ERC20Service-->>Frontend: Success with hash & progress
    Frontend->>Frontend: Display transaction status & link to explorer
    
    alt Hive Token Selected
        Frontend->>Hive: Build transfer operations
        Frontend->>Hive: Broadcast via Keychain/Aioha
        Hive-->>Frontend: Transaction hash
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Authentication flow (app/api/auth/ott/route.ts, app/api/signup/*): JWT generation, session management, Argon2 pepper verification, and state transitions require careful security review for timing attacks, token leakage, and race conditions.
  • Farcaster webhook handling (app/api/farcaster/webhook/route.ts, app/api/webhook/route.ts): Signature verification bypass logic, token store consistency, and concurrent event handling need validation.
  • Airdrop execution (components/airdrop/AirdropManager.tsx): Multi-chain token transfers, approval flows, and status tracking with batch processing logic need verification for fund loss scenarios.
  • Media upload pipelines (app/api/instagram-download/route.ts, app/api/pinata-*): Multiple external service dependencies, error aggregation, and timeout handling across fallback servers.
  • API route error handling and validation: Numerous routes lack comprehensive input validation and error standardization; review for injection attacks and unexpected payloads.
  • Supabase operations: Direct SQL execution in database init, lack of parameterization in some queries, and transaction atomicity guarantees.
  • Rate limiting and DOS vectors: In-memory rate limiting in signup routes may not be sufficient for production; no global rate limiting observed.

Poem

🐰 Hop, hop! The warren grows with flair—
New routes and flows beyond compare.
From Farcaster frames to airdrops blessed,
Skatehive's features pass the test.
OAuth tokens, themes galore,
Three-dot chains open wider doors! 🎨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The PR title "Fix/video lazy loading preload none" refers to a specific technical change (modifying video preload behavior) that is mentioned in the PR objectives. However, the actual changeset is massive, encompassing hundreds of file additions, modifications, and deletions across infrastructure configuration, API routes, UI components, layout restructuring, and entirely new systems (airdrop, Farcaster integration, signup flows, etc.). The title describes a minor optimization detail rather than reflecting the primary scope of this PR, which appears to be a comprehensive system refactoring. A developer scanning the history based on this title would expect a small, focused video rendering fix but would instead encounter a major architectural rewrite affecting the entire application structure. The PR title should be revised to accurately represent the primary objective of this changeset. Consider a more descriptive title such as "refactor: major system redesign with new infrastructure, API endpoints, and layout architecture" or similar that conveys the comprehensive nature of the changes. If the video lazy loading fix is important but secondary, document it in the PR description rather than in the title, which should highlight the main work the developer is contributing.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 82

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/globals.css (1)

1-8: Move @import rules before @tailwind directives.

CSS @import rules must precede all other valid at-rules and style rules (except @charset and @layer). Currently, the imports for markdown.css and snap.css come after the @tailwind directives, which may cause them to be ignored in some browsers.

Apply this diff:

+/* Import markdown styles */
+@import '../styles/markdown.css';
+/* Import snap component styles */
+@import '../styles/snap.css';
+
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
 
-/* Import markdown styles */
-@import '../styles/markdown.css';
-/* Import snap component styles */
-@import '../styles/snap.css';
-
 :root {
   --foreground-rgb: 0, 0, 0;
🧹 Nitpick comments (44)
app/coin/[address]/loading.tsx (1)

3-22: Consider adding accessibility attributes and TypeScript return type.

The loading skeleton is well-structured and functional. For improved accessibility and type safety, consider these optional enhancements:

  1. Add an explicit return type annotation
  2. Add aria-busy="true" and aria-label to the Container for screen reader support

Apply this diff:

-export default function Loading() {
+export default function Loading(): JSX.Element {
   return (
-    <Container maxW="container.lg" py={8}>
+    <Container maxW="container.lg" py={8} aria-busy="true" aria-label="Loading coin information">
       <VStack spacing={6}>
app/dao/page.tsx (1)

5-21: Consider adding social media preview images to metadata.

The metadata is missing images arrays in both openGraph and twitter objects. While these may inherit from the root layout, explicitly including them ensures proper social media previews when the DAO page is shared. The root layout (app/layout.tsx) includes /ogimage.png for both fields.

Apply this diff to add explicit social media images:

   openGraph: {
     title: "DAO - Under Construction | Skatehive",
     description:
       "The Skatehive DAO is currently under construction. Stay tuned for updates!",
     type: "website",
+    images: [
+      {
+        url: "/ogimage.png",
+        width: 1200,
+        height: 630,
+        alt: "Skatehive DAO - Under Construction",
+      },
+    ],
   },
   twitter: {
     card: "summary_large_image",
     title: "DAO - Under Construction | Skatehive",
     description:
       "The Skatehive DAO is currently under construction. Stay tuned for updates!",
+    images: ["/ogimage.png"],
   },
app/api/debug-hive-notifications/route.ts (2)

6-6: Consider validating the username parameter.

The username is accepted without validation. While fetchNotifications likely handles invalid usernames, adding basic validation improves error messages and prevents unnecessary API calls.

Apply this diff to add validation:

     const { searchParams } = new URL(request.url);
-    const username = searchParams.get('username') || 'xvlad';
+    const rawUsername = searchParams.get('username') || 'xvlad';
+    const username = rawUsername.trim();
+    
+    if (!username || username.length < 3 || username.length > 20 || !/^[a-z0-9.-]+$/.test(username)) {
+        return NextResponse.json({ 
+            success: false, 
+            error: 'Invalid username format. Must be 3-20 characters, lowercase alphanumeric with dots/hyphens.' 
+        }, { status: 400 });
+    }
+
     try {

6-6: Hardcoded default username could be configurable.

The fallback username 'xvlad' is hardcoded. For flexibility, consider reading from an environment variable.

-    const username = searchParams.get('username') || 'xvlad';
+    const username = searchParams.get('username') || process.env.DEBUG_DEFAULT_USER || 'xvlad';
app/share/page.tsx (3)

12-27: Remove redundant loading state.

The useEffect performs only synchronous operations (reading search params), making the loading state unnecessary. This causes an extra render cycle without benefit.

Apply this diff to simplify:

 function SharePageContent() {
   const searchParams = useSearchParams();
-  const [castData, setCastData] = useState<any>(null);
-  const [loading, setLoading] = useState(true);
-
-  useEffect(() => {
-    // Get cast data from URL parameters
-    const castHash = searchParams.get("castHash");
-    const castFid = searchParams.get("castFid");
-    const viewerFid = searchParams.get("viewerFid");
-
-    if (castHash && castFid) {
-      setCastData({
-        hash: castHash,
-        authorFid: castFid,
-        viewerFid: viewerFid,
-      });
-    }
-
-    setLoading(false);
-  }, [searchParams]);
-
-  if (loading) {
-    return (
-      <Box
-        display="flex"
-        justifyContent="center"
-        alignItems="center"
-        minH="100vh"
-      >
-        <Spinner size="xl" />
-      </Box>
-    );
-  }
+  
+  const castHash = searchParams.get("castHash");
+  const castFid = searchParams.get("castFid");
+  const viewerFid = searchParams.get("viewerFid");
+  
+  const castData = castHash && castFid ? {
+    hash: castHash,
+    authorFid: castFid,
+    viewerFid: viewerFid,
+  } : null;

63-66: Implement the cast display logic.

The placeholder comment indicates the cast rendering is not yet implemented. Users will only see metadata without the actual cast content.

Do you want me to help implement the cast fetching and rendering logic, or is this intentionally left as a stub for future work?


79-94: Extract the duplicated spinner component.

The loading spinner JSX is duplicated between the Suspense fallback and the SharePageContent component (lines 31-39). Extracting it reduces duplication.

Apply this diff to extract the spinner:

+const LoadingSpinner = () => (
+  <Box
+    display="flex"
+    justifyContent="center"
+    alignItems="center"
+    minH="100vh"
+  >
+    <Spinner size="xl" />
+  </Box>
+);
+
 export default function SharePage() {
   return (
-    <Suspense fallback={
-      <Box
-        display="flex"
-        justifyContent="center"
-        alignItems="center"
-        minH="100vh"
-      >
-        <Spinner size="xl" />
-      </Box>
-    }>
+    <Suspense fallback={<LoadingSpinner />}>
       <SharePageContent />
     </Suspense>
   );
 }
app/coin/[address]/error.tsx (1)

38-45: Consider displaying the error digest if available.

The error type includes an optional digest property (line 19), but it's never displayed in the UI. If the digest is useful for debugging, consider showing it when available.

Apply this diff to display the digest when present:

         <VStack spacing={4}>
           <Text color="gray.600" fontSize="sm">
             Error details: {error.message}
+            {error.digest && ` (${error.digest})`}
           </Text>
app/bounties/page.tsx (1)

20-20: Use relative URLs or BASE_URL constant for maintainability.

The hardcoded absolute URLs here are inconsistent with the root layout pattern (app/layout.tsx lines 34-109), which defines metadataBase: new URL(BASE_URL) and then uses relative paths. If the domain changes, these URLs must be manually updated in multiple locations.

Apply this diff to use relative URLs that respect the metadataBase:

   openGraph: {
     title: "Bounties | Skate Trick Challenges",
     description:
       "Submit and complete skate trick bounties. Post a challenge, submit your trick, and earn respect!",
-    url: "https://skatehive.app/bounties",
+    url: "/bounties",
     images: [
       {
-        url: "https://skatehive.app/og-bounties.png",
+        url: "/og-bounties.png",
         width: 1200,
         height: 630,
         alt: "Skatehive Bounties Open Graph Image",
       },
     ],
     siteName: "Skatehive",
     type: "website",
   },
   twitter: {
     card: "summary_large_image",
     title: "Bounties | Skate Trick Challenges",
     description:
       "Submit and complete skate trick bounties. Post a challenge, submit your trick, and earn respect!",
-    images: ["https://skatehive.app/og-bounties.png"],
+    images: ["/og-bounties.png"],
   },
   alternates: {
-    canonical: "https://skatehive.app/bounties",
+    canonical: "/bounties",
   },

Also applies to: 40-40

app/api/skatespots/route.ts (1)

27-30: Improve type safety and extract magic strings.

The filtering logic has two issues:

  1. Using any type defeats TypeScript's benefits
  2. Tag strings 'skatespot' and 'hive-173115' are hardcoded

Apply these improvements:

+    // Define constants at module level
+    const SKATESPOT_TAG = 'skatespot';
+    const SKATEHIVE_COMMUNITY_TAG = 'hive-173115';
+
+    interface SkateSpotPost {
+      tags?: string[];
+      body: string;
+      author: string;
+      // ... add other fields as needed
+    }
+
     // Filter to only include skatespot posts
-    const skatespots = data.data.filter((post: any) => {
+    const skatespots = data.data.filter((post: SkateSpotPost) => {
       const tags = post.tags || [];
-      return tags.includes('skatespot') && tags.includes('hive-173115');
+      return tags.includes(SKATESPOT_TAG) && tags.includes(SKATEHIVE_COMMUNITY_TAG);
     });
components/airdrop/steps/ConfirmationStep.tsx (3)

66-66: Add validation for unknown tokens.

If selectedToken doesn't exist in tokenDictionary, selectedTokenInfo will be undefined, which could cause issues even with optional chaining. Consider adding early validation or a fallback.

 const selectedTokenInfo = tokenDictionary[selectedToken];
+
+if (!selectedTokenInfo) {
+  return (
+    <Alert status="error">
+      <AlertIcon />
+      <AlertTitle>Unknown token: {selectedToken}</AlertTitle>
+    </Alert>
+  );
+}

178-192: Validate numeric input before using parseFloat.

The parseFloat(totalAmount) <= 0 check on line 182 doesn't handle invalid number strings properly. parseFloat("invalid") returns NaN, and NaN <= 0 evaluates to false, potentially allowing the button to be enabled with invalid input.

         {!isHiveToken && (
           <Button
             onClick={onApprove}
             isLoading={isApproving}
             loadingText="Approving..."
-            disabled={!validation.isValid || parseFloat(totalAmount) <= 0}
+            disabled={
+              !validation.isValid ||
+              isNaN(parseFloat(totalAmount)) ||
+              parseFloat(totalAmount) <= 0
+            }
             leftIcon={isApproving ? undefined : <WarningIcon />}
             colorScheme="orange"
             size="md"
             width="100%"
           >

213-237: Consider consolidating duplicate wallet warning blocks.

The Hive and Ethereum wallet warnings (lines 214-237) share identical structure and differ only in content. This could be refactored to reduce duplication.

-        {isHiveToken && !isHiveConnected && (
+        {((isHiveToken && !isHiveConnected) || (!isHiveToken && !isEthereumConnected)) && (
           <Alert status="warning">
             <AlertIcon />
             <Box>
-              <AlertTitle>Hive Wallet Required!</AlertTitle>
+              <AlertTitle>
+                {isHiveToken ? "Hive" : "Ethereum"} Wallet Required!
+              </AlertTitle>
               <AlertDescription>
-                Please connect your Hive wallet (Keychain, HiveAuth, etc.) to
-                execute this airdrop.
+                Please connect your {isHiveToken ? "Hive wallet (Keychain, HiveAuth, etc.)" : "Ethereum wallet"} to execute this airdrop.
               </AlertDescription>
             </Box>
           </Alert>
         )}
-
-        {!isHiveToken && !isEthereumConnected && (
-          <Alert status="warning">
-            <AlertIcon />
-            <Box>
-              <AlertTitle>Ethereum Wallet Required!</AlertTitle>
-              <AlertDescription>
-                Please connect your Ethereum wallet to execute this airdrop.
-              </AlertDescription>
-            </Box>
-          </Alert>
-        )}
app/api/portfolio/[address]/route.ts (2)

30-35: Consider including response preview in error message.

The text response is read but not included in the error message, which could aid debugging when the external API returns unexpected content.

  if (!contentType || !contentType.includes('application/json')) {
    const textResponse = await response.text();
-   throw new Error('API did not return JSON data');
+   throw new Error(`API did not return JSON data. Response: ${textResponse.substring(0, 200)}`);
  }

69-79: Add safeguards against NaN propagation.

Using parseFloat on potentially invalid data can result in NaN values that propagate through calculations, leading to incorrect totals.

  const totalBalanceUsdTokens = rawTokens.reduce((sum: number, token: any) => {
    const tokenData = token.token || token;
    const balanceUSD = parseFloat(tokenData.balanceUSD || token.balanceUSD || 0);
-   return sum + balanceUSD;
+   return sum + (isNaN(balanceUSD) ? 0 : balanceUSD);
  }, 0);

  const totalBalanceUSDApp = apps.reduce((sum: number, app: any) => {
-   return sum + (app.balanceUSD || 0);
+   const balanceUSD = parseFloat(app.balanceUSD || 0);
+   return sum + (isNaN(balanceUSD) ? 0 : balanceUSD);
  }, 0);
app/notifications/NotificationsPageClient.tsx (1)

20-27: Minor: Loading UI accessibility.

The loading state combines a Spinner and Text but they're not semantically grouped. Consider wrapping them in a <Flex> with proper spacing instead of relying on margin, or add an aria-label to the Spinner for better accessibility.

Apply this diff:

-    return (
-      <Center height="200px">
-        <Spinner size="lg" />
-        <Text ml={3}>Loading...</Text>
-      </Center>
-    );
+    return (
+      <Center height="200px">
+        <Flex align="center" gap={3}>
+          <Spinner size="lg" />
+          <Text>Loading...</Text>
+        </Flex>
+      </Center>
+    );
app/chat/page.tsx (2)

5-5: Improve type safety for StWidget.

Declaring StWidget as any removes all type checking benefits. Define a proper interface or type definition.

-declare const StWidget: any;
+interface StWidgetConstructor {
+  new (url: string): StWidgetInstance;
+}
+
+interface StWidgetInstance {
+  setProperties(options: Record<string, any>): void;
+  createElement(width: string, height: string, arg3: boolean, arg4: boolean): HTMLElement;
+  cleanup?(): void;
+  destroy?(): void;
+}
+
+declare const StWidget: StWidgetConstructor | undefined;

Then update usage:

-if (typeof StWidget === "function" && container) {
+if (StWidget && container) {

150-161: Consider extracting loading UI to a reusable component.

The loading spinner markup (lines 150-161) is inline and could be reused elsewhere in the application. Extracting it would improve maintainability.

Create a shared component:

// components/LoadingSpinner.tsx
export function LoadingSpinner({ message, submessage }: { message: string; submessage?: string }) {
  return (
    <div className="w-full border rounded-lg bg-gray-50 shadow-lg flex items-center justify-center" style={{ minHeight: "600px" }}>
      <div className="text-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
        <p className="text-gray-600">{message}</p>
        {submessage && <p className="text-sm text-gray-500 mt-2">{submessage}</p>}
      </div>
    </div>
  );
}

Then use it:

-{isLoading && (
-  <div className="w-full border rounded-lg bg-gray-50 shadow-lg flex items-center justify-center" style={{ minHeight: "600px" }}>
-    <div className="text-center">
-      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
-      <p className="text-gray-600">Loading chat widget...</p>
-      <p className="text-sm text-gray-500 mt-2">This may take a moment</p>
-    </div>
-  </div>
-)}
+{isLoading && <LoadingSpinner message="Loading chat widget..." submessage="This may take a moment" />}
app/compose/page.tsx (5)

71-71: Remove redundant assignment

This assignment adds no value—just use originalHandleSubmit directly at line 494.

-  // Use the wrapped handleSubmit from useComposeForm
-  const handleSubmit = originalHandleSubmit;

Then update line 494:

-        onClick={handleSubmit}
+        onClick={originalHandleSubmit}

73-79: Remove dead code

This useEffect has dependencies but no body—it does nothing and should be removed.

-  React.useEffect(() => {}, [
-    beneficiaries,
-    title,
-    markdown,
-    hashtags,
-    isSubmitting,
-  ]);

162-170: Remove unused function

The handleVideoDurationError function is defined but never called. The VideoUploader component at lines 233-243 doesn't receive an onDurationError callback, so this duration error state (lines 158-160) and handler are unused.

-  // Video duration error handling
-  const [videoDurationError, setVideoDurationError] = useState<string | null>(
-    null
-  );
-
-  const handleVideoDurationError = (duration: number) => {
-    const minutes = Math.floor(duration / 60);
-    const seconds = Math.floor(duration % 60);
-    setVideoDurationError(
-      `Video is ${minutes}m ${seconds}s long. Long videos will be uploaded without compression to prevent crashes.`
-    );
-    // Clear error after 5 seconds
-    setTimeout(() => setVideoDurationError(null), 5000);
-  };

Also remove the unused UI at lines 247-251:

-      {videoDurationError && (
-        <Center bg="red.50" color="red.800" p={2} borderRadius="md" mb={2}>
-          {videoDurationError}
-        </Center>
-      )}

180-189: Remove commented-out code

If this GIF modal functionality isn't needed immediately, remove the commented code. If it will be implemented soon, track it in an issue instead.

-  // TODO: Implement GIF modal functionality
-  // Handle GIF modal effects - currently disabled until GIF modal is implemented
-  // React.useEffect(() => {
-  //   if (isGifModalOpen) {
-  //     gifMakerWithSelectorRef.current?.reset();
-  //     setGifUrl(null);
-  //     setGifSize(null);
-  //     setIsProcessingGif(false);
-  //   }
-  // }, [isGifModalOpen, setGifUrl, setGifSize, setIsProcessingGif]);

363-363: Simplify redundant ternary

Both branches of the ternary evaluate to 0.

-            borderRightRadius={imagesInMarkdown.length === 0 ? 0 : 0}
+            borderRightRadius={0}
app/api/farcaster/user-preferences/route.ts (1)

15-21: Consider handling null preferences explicitly.

The getUserPreferences method can return null when no preferences are found (as seen in the relevant code snippet), but this code treats null as a successful response with data: null.

Consider being explicit about whether returning null preferences is expected or should be a 404:

     const preferences = await SkateHiveFarcasterService.getUserPreferences(hiveUsername);

+    if (!preferences) {
+        return NextResponse.json(
+            { success: false, message: 'User preferences not found' },
+            { status: 404 }
+        );
+    }
+
     return NextResponse.json({
         success: true,
         data: preferences
     });

Or if null is intentionally valid, document this behavior in a comment.

.vscode/settings.json (2)

14-15: Reconsider hiding README.md and docs directory.

Hiding README.md and the docs directory from the editor workspace is unusual and may hinder development. Developers typically need access to documentation files.

Consider removing these entries:

     "tsconfig.tsbuildinfo": true,
     "node_modules": true,
-    "README.md": true,
-    "docs": true
+    "node_modules": true
   },

17-23: Duplication with files.exclude setting.

The hide-files.files array duplicates the hiding logic from files.exclude. If this is for a specific VSCode extension, it's acceptable, but verify that both settings are necessary.

app/api/farcaster/init-db/route.ts (1)

29-53: Consider adding authentication to the status endpoint.

While less sensitive than the POST endpoint, this GET handler exposes operational metrics (token counts, last updated timestamp) that could aid reconnaissance or reveal usage patterns.

If this endpoint is intended for monitoring/admin purposes, add authentication similar to the POST handler.

app/.well-known/farcaster.json/route.ts (1)

32-32: Track TODO for custom hero image.

The TODO comment indicates a custom hero image (1200x630px) should be created to replace the generic opengraph-image.png for the heroImageUrl field.

Would you like me to open an issue to track this task?

app/api/pinata-chunked/route.ts (1)

29-32: Validate base64 chunk data before processing.

Line 31 assumes chunk is valid base64 without validation. Malformed input could cause Buffer.from() to fail or produce unexpected results.

Add validation:

         if (totalChunks === 1) {
             // Single chunk - upload directly
+            if (!chunk || typeof chunk !== 'string') {
+                return NextResponse.json({ error: 'Invalid chunk data' }, { status: 400 });
+            }
+            
+            // Validate base64 format
+            if (!/^[A-Za-z0-9+/]*={0,2}$/.test(chunk)) {
+                return NextResponse.json({ error: 'Chunk must be valid base64' }, { status: 400 });
+            }
+            
             const buffer = Buffer.from(chunk, 'base64');
app/api/farcaster/webhook/route.ts (1)

5-36: Limit error message details in production.

Error responses on lines 11, 24, and 30 expose internal details that could aid attackers. In production, generic error messages are preferred.

         if (!signature.header || !signature.payload || !signature.signature) {
-            console.error('[Webhook] Invalid payload format:', signature);
+            console.error('[Webhook] Invalid payload format');
             return NextResponse.json(
-                { error: 'Invalid webhook payload format' },
+                { error: process.env.NODE_ENV === 'production' ? 'Bad request' : 'Invalid webhook payload format' },
                 { status: 400 }
             );
         }

Apply similar changes to other error responses.

app/api/farcaster/test-notifications/route.ts (1)

10-12: Use 403 Forbidden instead of 404 for production block.

Returning 404 is misleading - it suggests the endpoint doesn't exist. Use 403 to clearly indicate the endpoint exists but access is forbidden in production.

     if (process.env.NODE_ENV === 'production') {
-        return NextResponse.json({ error: 'Not available in production' }, { status: 404 })
+        return NextResponse.json({ error: 'Not available in production' }, { status: 403 })
     }
app/api/opengraph/route.ts (2)

51-83: Fragile regex-based HTML parsing.

Using regex to parse HTML (lines 51-83) is brittle and can fail on:

  • Attribute order variations: <meta content="..." property="...">
  • Single vs double quotes
  • Whitespace variations
  • Self-closing tags

Consider using a proper HTML parser for more robust extraction:

// Add cheerio or similar HTML parser
import * as cheerio from 'cheerio';

// Replace regex parsing with:
const $ = cheerio.load(html);

ogData.title = $('meta[property="og:title"]').attr('content') || 
               $('title').text() || '';

ogData.description = $('meta[property="og:description"]').attr('content') ||
                     $('meta[name="description"]').attr('content') || '';

ogData.image = $('meta[property="og:image"]').attr('content') || '';

ogData.siteName = $('meta[property="og:site_name"]').attr('content') ||
                  urlObj.hostname.replace('www.', '');

This handles attribute order, quotes, and whitespace more reliably.


11-103: Add caching to reduce redundant fetches.

Every request to the same URL fetches the page again, which:

  • Wastes bandwidth and API quota
  • Increases latency
  • May trigger rate limits on target sites

Add caching:

import { Redis } from '@upstash/redis'; // or your cache solution

const cache = new Map<string, { data: OpenGraphData; timestamp: number }>();
const CACHE_TTL = 3600000; // 1 hour

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const url = searchParams.get('url');

  if (!url) {
    return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 });
  }

  // Check cache
  const cached = cache.get(url);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return NextResponse.json(cached.data);
  }

  try {
    // ... fetch and parse logic ...
    
    // Store in cache
    cache.set(url, { data: ogData, timestamp: Date.now() });
    
    return NextResponse.json(ogData);
  } catch (error) {
    // ... error handling ...
  }
}

For production, use Redis or similar persistent cache instead of in-memory Map.

app/api/farcaster/unlink/route.ts (1)

26-39: Consider transaction consistency and connection cleanup.

The token is removed from the token store before the database update. If the database update fails, the token removal cannot be rolled back, potentially leaving the system in an inconsistent state. Additionally, the Pool instance is not closed after use, which may lead to connection leaks.

Consider wrapping these operations in a transaction or reversing the order so database operations complete first. Also, ensure the pool is properly closed.

Apply this diff to add connection cleanup:

         const { Pool } = await import('pg');
         const pool = new Pool({
             connectionString: process.env.STORAGE_POSTGRES_URL || process.env.POSTGRES_URL
         });
-        await pool.query(
-            'UPDATE skatehive_farcaster_preferences SET hive_username = NULL WHERE fid = $1',
-            [preferences.fid]
-        );
+        try {
+            await pool.query(
+                'UPDATE skatehive_farcaster_preferences SET hive_username = NULL WHERE fid = $1',
+                [preferences.fid]
+            );
+        } finally {
+            await pool.end();
+        }
app/api/farcaster/notifications-queue/route.ts (1)

30-31: Remove empty conditional blocks.

These empty if blocks serve no purpose and should be removed to improve code clarity.

Apply this diff:

         } catch (err) {
             console.error(`[notifications-queue] Error in fetchNewNotificationsServer:`, err);
             throw new Error('fetchNewNotificationsServer failed');
         }
-        if (allNotifications.length > 0) {
-        }

         // Filter unread notifications
         const unread = allNotifications.filter(n => {
             const notifDate = new Date(n.date).getTime();
             const lastReadTimestamp = new Date(lastReadDate).getTime();
             return notifDate > lastReadTimestamp;
         });
-        if (unread.length > 0) {
-        }

Also applies to: 39-40

app/api/database/init/route.ts (1)

7-9: Avoid mutating process.env at module scope.

Setting process.env.POSTGRES_URL at the module level can cause unexpected side effects across the application, especially if other modules have already imported and cached the original value. This pattern also makes testing harder.

Consider passing the connection string directly to the sql client or using a configuration helper function.

app/api/signup/key-backup/route.ts (1)

26-26: Use unknown instead of any for error type.

For better type safety, catch errors as unknown rather than any.

Apply this diff:

-  } catch (error: any) {
+  } catch (error: unknown) {
     console.error('Key backup error:', error);
     return NextResponse.json(
       { error: 'Internal server error during key backup' },
       { status: 500 }
     );
   }
app/api/signup/init/route.ts (1)

69-76: Consider more robust email validation.

The regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/ is simplistic and will accept invalid emails like a@b.c or test@-domain.com.

Consider using a library or more comprehensive validation:

// Option 1: Use a validation library (recommended)
import { z } from 'zod';
const emailSchema = z.string().email();
if (!emailSchema.safeParse(email).success) {
  return NextResponse.json(
    { error: 'Invalid email format' },
    { status: 400 }
  );
}

// Option 2: More robust regex
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
.env.local.example (1)

1-84: Improve formatting consistency.

The file has inconsistent formatting (quotes, spacing, trailing whitespace). While functional, standardizing would improve maintainability.

Consider these improvements:

  • Add quotes around values with special characters or URLs
  • Remove spaces around = signs (lines 35, 43, 63, 66-70)
  • Remove trailing whitespace (line 59)
  • Group related variables consistently

Example:

-NEXT_PUBLIC_WC_PROJECT_ID=                # WalletConnect project ID
+NEXT_PUBLIC_WC_PROJECT_ID=""              # WalletConnect project ID
-NEXT_PUBLIC_TOKEN=  0xfe10d3ce1b0f090935670368ec6de00d8d965523
+NEXT_PUBLIC_TOKEN="0xfe10d3ce1b0f090935670368ec6de00d8d965523"
app/signup/page.tsx (2)

78-117: RC check may have unintended side effects.

Calling /claim-account with test usernames to check RC creates noise in signer logs and could trigger unintended operations. The signer service should provide a dedicated health check endpoint that validates RC without side effects.

Coordinate with the signer service team to add a dedicated RC check endpoint:

// Ideal approach:
const rcResponse = await fetch(`${signerUrl}/health/rc`, {
  method: "GET",
  headers: { "x-signer-token": signerToken },
});
const rcData = await rcResponse.json();
// Response: { rc_available: 1500000000000, rc_needed: 2000000000000, sufficient: false }

Until then, document this behavior:

// HACK: Testing RC by attempting a claim-account operation with a dummy username
// TODO: Replace with dedicated /health/rc endpoint to avoid log noise

279-282: Replace alert() with toast notifications.

Native alert() blocks the UI and provides poor UX. Chakra UI provides a toast system that's already available.

Apply this pattern:

+import { useToast } from "@chakra-ui/react";
+
 export default function SignupPage() {
+  const toast = useToast();
   // ... other state
   
   const copyToClipboard = (text: string, label: string) => {
     navigator.clipboard.writeText(text);
-    alert(`${label} copied to clipboard!`);
+    toast({
+      title: `${label} copied!`,
+      status: "success",
+      duration: 2000,
+      isClosable: true,
+    });
   };

Apply similar changes to lines 317, 354-356.

app/layout.tsx (1)

19-36: Production URL fallback may cause dev environment issues.

Defaulting BASE_URL to https://skatehive.app when NEXT_PUBLIC_BASE_URL is unset will cause development metadata (Open Graph images, canonical URLs) to point to production.

Add environment-aware fallback:

-const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "https://skatehive.app";
+const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 
+  (process.env.NODE_ENV === 'production' 
+    ? "https://skatehive.app" 
+    : "http://localhost:3000");

Or make it required:

+if (!process.env.NEXT_PUBLIC_BASE_URL) {
+  throw new Error("NEXT_PUBLIC_BASE_URL environment variable is required");
+}
 const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
app/post/[author]/[permlink]/page.tsx (2)

167-187: Consider markdown parser for robust image extraction.

The regex /!\[.*?\]\((.*?)\)/g for extracting images is simplistic and may miss edge cases (escaped brackets, nested parentheses, multi-line syntax).

For more robust parsing, consider using a markdown library:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import { visit } from 'unist-util-visit';

function extractImagesFromMarkdown(markdown: string): string[] {
  const tree = unified().use(remarkParse).parse(markdown);
  const images: string[] = [];
  
  visit(tree, 'image', (node: any) => {
    if (node.url) images.push(node.url);
  });
  
  return images;
}

However, if the current regex works for your content, this may be over-engineering.


45-86: Consider moving cleanTextForDescription to shared utilities.

This comprehensive text cleaning function could be useful for other metadata generation scenarios (user profiles, comments, etc.).

Move to a shared location:

// lib/utils/text-cleaning.ts
export function cleanTextForDescription(text: string): string {
  // ... existing implementation
}

// Then import it:
import { cleanTextForDescription } from "@/lib/utils/text-cleaning";

This is only recommended if you find yourself needing similar functionality elsewhere.


# local env files
.env*.local
.env*
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Overly broad ignore pattern will exclude template file.

The pattern .env* will ignore all files starting with .env, including .env.local.example which appears to be a tracked template file based on the README and other references in this PR.

Apply this diff to use a more specific pattern:

 # local env files
 .env*.local
-.env*
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local

Or if you want to ignore all .env files except the example:

 # local env files
 .env*.local
-.env*
+.env
+.env.*
+!.env.local.example

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .gitignore at line 30, the pattern ".env*" is too broad and excludes the
tracked template file (.env.local.example); replace the broad pattern with
explicit entries for files you want to ignore (for example ".env" and
".env.local") or keep a broad ".env*" but add a negation for the example by
adding a line like "!.env.local.example" immediately after the .env* entry so
the example remains tracked.

Comment on lines +4 to +12
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const username = searchParams.get('username') || 'xvlad';
try {
const notifications = await serverHiveClient.fetchNotifications(username);
return NextResponse.json({ success: true, username, notifications });
} catch (error) {
return NextResponse.json({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add authentication to prevent unauthorized access to user notifications.

This debug endpoint exposes Hive notifications for any username without authentication or authorization checks. Notifications typically contain PII (mentions, replies, transfers) and should be protected.

Apply this diff to add basic authentication:

 export async function GET(request: Request) {
+    // Add authentication check
+    const session = await getServerSession(); // or your auth mechanism
+    if (!session || !session.user?.isAdmin) {
+        return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
+    }
+
     const { searchParams } = new URL(request.url);
     const username = searchParams.get('username') || 'xvlad';

Alternatively, if this is strictly for local development, ensure it's disabled in production via environment checks or route configuration.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/api/debug-hive-notifications/route.ts around lines 4 to 12, the debug GET
endpoint currently returns notifications for any username with no auth; add a
protection layer: check environment (only enable in development/local) and
require a secret (e.g., header Authorization: Bearer <DEBUG_TOKEN> or basic
auth) before calling serverHiveClient.fetchNotifications, returning 401 on
missing/invalid credentials; validate the token from an env var (e.g.,
DEBUG_HIVE_TOKEN) and reject requests when NODE_ENV === 'production'; keep the
existing response shapes for success and error.

Comment on lines +4 to +27
// Initialize database endpoint - should only be run once
export async function POST(request: NextRequest) {
try {
const tokenStore = new DatabaseTokenStore();
await tokenStore.initializeDatabase();

return NextResponse.json({
success: true,
message: 'Database initialized successfully',
timestamp: new Date().toISOString(),
});

} catch (error) {
console.error('Database initialization failed:', error);

return NextResponse.json(
{
error: 'Database initialization failed',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add authentication to protect database initialization.

This endpoint can initialize or reinitialize the database schema but lacks authentication. Any client can trigger database initialization, which could disrupt service or cause data consistency issues.

Consider adding authentication middleware or checking for an admin secret:

 export async function POST(request: NextRequest) {
+    // Verify admin access
+    const authHeader = request.headers.get('authorization');
+    if (authHeader !== `Bearer ${process.env.ADMIN_SECRET_KEY}`) {
+        return NextResponse.json(
+            { error: 'Unauthorized' },
+            { status: 401 }
+        );
+    }
+
     try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Initialize database endpoint - should only be run once
export async function POST(request: NextRequest) {
try {
const tokenStore = new DatabaseTokenStore();
await tokenStore.initializeDatabase();
return NextResponse.json({
success: true,
message: 'Database initialized successfully',
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('Database initialization failed:', error);
return NextResponse.json(
{
error: 'Database initialization failed',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}
// Initialize database endpoint - should only be run once
export async function POST(request: NextRequest) {
// Verify admin access
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.ADMIN_SECRET_KEY}`) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
try {
const tokenStore = new DatabaseTokenStore();
await tokenStore.initializeDatabase();
return NextResponse.json({
success: true,
message: 'Database initialized successfully',
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('Database initialization failed:', error);
return NextResponse.json(
{
error: 'Database initialization failed',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}

Comment on lines +97 to +133
const logsDir = path.join(process.cwd(), 'logs');
const logFile = path.join(logsDir, 'client-errors.log');

if (!existsSync(logFile)) {
return NextResponse.json({ logs: [] });
}

// Read log file
const fs = require('fs');
const logs = fs.readFileSync(logFile, 'utf8')
.split('\n')
.filter((line: string) => line.trim())
.map((line: string) => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(Boolean)
.reverse() // Most recent first
.slice(0, limit);

// Apply filters
let filteredLogs = logs;
if (level) {
filteredLogs = filteredLogs.filter((log: ClientErrorLog) => log.level === level);
}
if (type) {
filteredLogs = filteredLogs.filter((log: ClientErrorLog) => log.type === type);
}

return NextResponse.json({
logs: filteredLogs,
total: logs.length,
filtered: filteredLogs.length
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Lock down the GET handler

Line 129 currently serves the entire client-errors.log to anyone who hits this endpoint, and the POST handler on Line 57 persists clientIp and user-agent data. That’s unauthenticated access to PII and internal diagnostics. Please add strict authentication/authorization (or remove this GET route from the public surface) before shipping.

🤖 Prompt for AI Agents
In app/api/logs/client-errors/route.ts around lines 97 to 133 the GET handler
currently returns parsed client-errors.log (including persisted clientIp and
user-agent) to any caller; this exposes PII and internal diagnostics. Fix by
restricting access: require and validate authentication (e.g., check a signed
session cookie or Authorization bearer token), verify an authorized role/claim
(admin or internal service) before reading the file, return 401/403 for
unauthorized requests, and ensure only non-PII fields are returned (or redact
clientIp/userAgent) if authorization is not present; alternatively remove the
public GET route entirely and move it behind an internal-only route or admin
panel. Ensure access attempts are logged (without leaking PII) and fail closed
if auth validation cannot be performed.

Comment on lines +55 to +62
const uploadResponse = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
method: 'POST',
headers: {
'pinata_api_key': pinataApiKey,
'pinata_secret_api_key': pinataSecretApiKey,
},
body: uploadFormData,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add timeout to external Pinata API call.

The fetch call to Pinata's API has no timeout configuration. If Pinata's service is slow or unresponsive, this could cause the request to hang indefinitely and tie up server resources.

Add a timeout using AbortSignal:

+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
+
         const uploadResponse = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
             method: 'POST',
             headers: {
                 'pinata_api_key': pinataApiKey,
                 'pinata_secret_api_key': pinataSecretApiKey,
             },
             body: uploadFormData,
+            signal: controller.signal,
-        });
+        }).finally(() => clearTimeout(timeoutId));
🤖 Prompt for AI Agents
In app/api/pinata-chunked/route.ts around lines 55 to 62 the fetch call to
Pinata has no timeout and can hang; wrap the request with an AbortController,
start a timer (e.g., setTimeout) that calls controller.abort() after a chosen
timeout (e.g., 10s), pass controller.signal to fetch, clear the timer after
fetch completes, and handle the abort case (catch the AbortError and
return/throw a timeout-specific error or response). Ensure the secret/key
headers and body remain unchanged and the controller is cleaned up to avoid
leaks.

Comment on lines +29 to +33
data: {
username: username,
witness: "skatehive",
vote: true,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor the witness parameter
witnessVoteWithKeychain accepts a witness argument but immediately overwrites it with the constant "skatehive". Any caller passing a different witness will still vote for skatehive, which breaks the exported helper’s contract and makes future reuse impossible. Wire the supplied argument through instead.

-        username: username,
-        witness: "skatehive",
+        username,
+        witness,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data: {
username: username,
witness: "skatehive",
vote: true,
},
data: {
username,
witness,
vote: true,
},
🤖 Prompt for AI Agents
In app/witness/page.tsx around lines 29 to 33, the function call builds a data
object that unconditionally sets witness to the constant "skatehive", ignoring
the witness parameter passed into witnessVoteWithKeychain; update the data
construction to use the function’s witness argument (or a validated fallback)
instead of the hard-coded string so the supplied witness is honored and the
helper becomes reusable.

Comment on lines +21 to +43
interface ConfirmationStepProps {
selectedToken: string;
totalAmount: string;
airdropUsers: any[];
isHiveToken: boolean;
isHiveConnected: boolean;
isEthereumConnected: boolean;
isExecuting: boolean;
isApproving: boolean;
isEstimating: boolean;
costEstimate: any;
validation: {
isValid: boolean;
errors: string[];
};
status: any;
onBack: () => void;
onStartOver: () => void;
onExecute: () => void;
onApprove: () => void;
onEstimateCost: () => void;
onResetStatus: () => void;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace any types with proper interfaces for type safety.

The interface uses any for airdropUsers, costEstimate, and status, which bypasses TypeScript's type checking and can lead to runtime errors.

Consider defining proper types:

+interface AirdropUser {
+  username: string;
+  amount: string;
+  // ... other fields
+}
+
+interface CostEstimate {
+  network: "hive" | "base";
+  tokenAmount?: string;
+  gasEstimate?: {
+    totalCost: string;
+  };
+  errors?: string[];
+}
+
+interface TransactionStatus {
+  state: "idle" | "pending" | "success" | "error";
+  // ... other fields
+}
+
 interface ConfirmationStepProps {
   selectedToken: string;
   totalAmount: string;
-  airdropUsers: any[];
+  airdropUsers: AirdropUser[];
   isHiveToken: boolean;
   isHiveConnected: boolean;
   isEthereumConnected: boolean;
   isExecuting: boolean;
   isApproving: boolean;
   isEstimating: boolean;
-  costEstimate: any;
+  costEstimate: CostEstimate | null;
   validation: {
     isValid: boolean;
     errors: string[];
   };
-  status: any;
+  status: TransactionStatus;
   onBack: () => void;
   onStartOver: () => void;
   onExecute: () => void;
   onApprove: () => void;
   onEstimateCost: () => void;
   onResetStatus: () => void;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface ConfirmationStepProps {
selectedToken: string;
totalAmount: string;
airdropUsers: any[];
isHiveToken: boolean;
isHiveConnected: boolean;
isEthereumConnected: boolean;
isExecuting: boolean;
isApproving: boolean;
isEstimating: boolean;
costEstimate: any;
validation: {
isValid: boolean;
errors: string[];
};
status: any;
onBack: () => void;
onStartOver: () => void;
onExecute: () => void;
onApprove: () => void;
onEstimateCost: () => void;
onResetStatus: () => void;
}
interface AirdropUser {
username: string;
amount: string;
// ... other fields
}
interface CostEstimate {
network: "hive" | "base";
tokenAmount?: string;
gasEstimate?: {
totalCost: string;
};
errors?: string[];
}
interface TransactionStatus {
state: "idle" | "pending" | "success" | "error";
// ... other fields
}
interface ConfirmationStepProps {
selectedToken: string;
totalAmount: string;
airdropUsers: AirdropUser[];
isHiveToken: boolean;
isHiveConnected: boolean;
isEthereumConnected: boolean;
isExecuting: boolean;
isApproving: boolean;
isEstimating: boolean;
costEstimate: CostEstimate | null;
validation: {
isValid: boolean;
errors: string[];
};
status: TransactionStatus;
onBack: () => void;
onStartOver: () => void;
onExecute: () => void;
onApprove: () => void;
onEstimateCost: () => void;
onResetStatus: () => void;
}
🤖 Prompt for AI Agents
In components/airdrop/steps/ConfirmationStep.tsx around lines 21 to 43, the
props interface uses `any` for airdropUsers, costEstimate, and status; replace
those `any` types with explicit interfaces to restore type-safety. Define an
AirdropUser interface (e.g., id/address, amount, optional metadata), a
CostEstimate interface (e.g., gas: number|string, fee: number|string, currency?:
string, breakdown?: Record<string, number>), and a Status interface (e.g.,
state: 'idle'|'pending'|'success'|'failed', message?: string, txHash?: string,
error?: Error|string), import or export them as needed, then update
ConfirmationStepProps to use these types and update any usage sites (maps,
property access) to match the new fields; run TypeScript to fix remaining type
errors and adjust code where property names differ.

Comment on lines +96 to +99
<Text fontSize="md" color="textSecondary">
{(parseFloat(totalAmount) / airdropUsers.length).toFixed(2)}{" "}
{selectedToken} per user
</Text>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent division by zero when calculating per-user amount.

If airdropUsers.length is 0, the calculation on line 97 will produce Infinity, breaking the UI display.

Apply this diff to add a guard:

-              <Text fontSize="md" color="textSecondary">
-                {(parseFloat(totalAmount) / airdropUsers.length).toFixed(2)}{" "}
-                {selectedToken} per user
-              </Text>
+              {airdropUsers.length > 0 && (
+                <Text fontSize="md" color="textSecondary">
+                  {(parseFloat(totalAmount) / airdropUsers.length).toFixed(2)}{" "}
+                  {selectedToken} per user
+                </Text>
+              )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Text fontSize="md" color="textSecondary">
{(parseFloat(totalAmount) / airdropUsers.length).toFixed(2)}{" "}
{selectedToken} per user
</Text>
{airdropUsers.length > 0 && (
<Text fontSize="md" color="textSecondary">
{(parseFloat(totalAmount) / airdropUsers.length).toFixed(2)}{" "}
{selectedToken} per user
</Text>
)}
🤖 Prompt for AI Agents
In components/airdrop/steps/ConfirmationStep.tsx around lines 96 to 99, the
per-user amount calculation can divide by zero when airdropUsers.length is 0;
update the UI to guard against this by checking airdropUsers.length > 0 before
performing the division and formatting (e.g., show (parseFloat(totalAmount) /
airdropUsers.length).toFixed(2) only when length > 0, otherwise render a safe
fallback like "0.00" or "-"), and also defensively handle non-numeric
totalAmount by defaulting parseFloat(totalAmount) to 0 when necessary.

Comment on lines +177 to +244
{!isHiveToken && (
<Button
onClick={onApprove}
isLoading={isApproving}
loadingText="Approving..."
disabled={!validation.isValid || parseFloat(totalAmount) <= 0}
leftIcon={isApproving ? undefined : <WarningIcon />}
colorScheme="orange"
size="md"
width="100%"
>
{isApproving
? "Approving..."
: `Approve ${selectedToken} Tokens First`}
</Button>
)}

{/* Validation Errors */}
{!validation.isValid && (
<Alert status="error">
<AlertIcon />
<Box>
<AlertTitle>Configuration Issues:</AlertTitle>
<AlertDescription>
<VStack align="start" spacing={1}>
{validation.errors.map((error, index) => (
<Text key={index} fontSize="sm">
• {error}
</Text>
))}
</VStack>
</AlertDescription>
</Box>
</Alert>
)}

{/* Wallet Connection Warning */}
{isHiveToken && !isHiveConnected && (
<Alert status="warning">
<AlertIcon />
<Box>
<AlertTitle>Hive Wallet Required!</AlertTitle>
<AlertDescription>
Please connect your Hive wallet (Keychain, HiveAuth, etc.) to
execute this airdrop.
</AlertDescription>
</Box>
</Alert>
)}

{!isHiveToken && !isEthereumConnected && (
<Alert status="warning">
<AlertIcon />
<Box>
<AlertTitle>Ethereum Wallet Required!</AlertTitle>
<AlertDescription>
Please connect your Ethereum wallet to execute this airdrop.
</AlertDescription>
</Box>
</Alert>
)}

{/* Transaction Status */}
{(status.state !== "idle" || isExecuting) && (
<TransactionStatusDisplay status={status} onReset={onResetStatus} />
)}
</VStack>
</>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing execute button for airdrop transaction.

The onExecute callback is provided (line 39, 60) but never invoked. There's no button to actually execute the airdrop after confirmation. Users can approve tokens (for ERC-20) but cannot proceed with the transaction.

Add an execute button after the approval button:

         {!isHiveToken && (
           <Button
             onClick={onApprove}
             isLoading={isApproving}
             loadingText="Approving..."
             disabled={!validation.isValid || parseFloat(totalAmount) <= 0}
             leftIcon={isApproving ? undefined : <WarningIcon />}
             colorScheme="orange"
             size="md"
             width="100%"
           >
             {isApproving
               ? "Approving..."
               : `Approve ${selectedToken} Tokens First`}
           </Button>
         )}
 
+        {/* Execute Airdrop Button */}
+        <Button
+          onClick={onExecute}
+          isLoading={isExecuting}
+          loadingText="Executing..."
+          disabled={
+            !validation.isValid ||
+            parseFloat(totalAmount) <= 0 ||
+            (isHiveToken ? !isHiveConnected : !isEthereumConnected) ||
+            status.state !== "idle"
+          }
+          leftIcon={<CheckIcon />}
+          colorScheme="green"
+          size="lg"
+          width="100%"
+        >
+          Execute Airdrop
+        </Button>
+
         {/* Validation Errors */}
🤖 Prompt for AI Agents
components/airdrop/steps/ConfirmationStep.tsx around lines 177 to 244: add an
"Execute" button directly after the Approve button so the provided onExecute
callback is invoked; render a Button that calls onExecute, shows isLoading when
isExecuting, has a loadingText like "Executing...", disables when
validation.isValid is false or parseFloat(totalAmount) <= 0 or (isHiveToken ?
!isHiveConnected : !isEthereumConnected) or isExecuting, and displays a clear
label such as "Execute Airdrop" (or "Execute Hive Airdrop" when isHiveToken if
you want) — ensure the button is visible for both Hive and non-Hive flows (i.e.,
render for both token types) and sits before the validation/warning alerts
already present.

## Install Packages
## Local Development

1. Copy `.env.local.example` to `.env.local` and fill in the values for your environment.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cross-reference with .gitignore issue.

The README instructs users to copy .env.local.example, but the .gitignore pattern .env* (line 30) will prevent this template file from being tracked in git. This is flagged in the .gitignore review.

Ensure the .env.local.example file is not ignored so new developers can use it as a template.

🤖 Prompt for AI Agents
In README.md around line 12, the README tells users to copy `.env.local.example`
but the repository `.gitignore` has a pattern `.env*` (line 30) that prevents
the example from being tracked; update the repo so the example template is not
ignored by either removing or narrowing the broad `.env*` rule or adding an
explicit exception for `.env.local.example` in `.gitignore`, then add and commit
the `.env.local.example` file so new developers can use it as a template.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants