This repository contains the Skatehive 3.0 web application. It is a Next.js based community site for the Skatehive (Hive blockchain) community.
- Node.js 20.x
- Package manager: pnpm 9.x (lockfile version 9). Always use
pnpmfor installs and scripts. - Next.js 15.3.2
- Chakra UI 2.10.9 (icons 2.2.4)
- Tailwind CSS 4
- React Query for data caching
- Wagmi and Viem for Ethereum connectivity
- Aioha for Hive authentication and wallet support
- TypeScript is enabled via
tsconfig.json. - The project deploys on Vercel using the default Next.js build.
- Copy
.env.local.exampleto.env.localand update values. - Install dependencies:
pnpm install
- Start the dev server:
pnpm dev
- Run lint checks with
pnpm lint.
The main provider tree is defined in app/providers.tsx.
Key packages and their roles:
- Aioha – registers Hive auth options and manages wallets
- Chakra UI – theme and global styles through
ThemeProvider - React Query – data caching via
QueryClientProvider - Wagmi with Viem – Ethereum RPC connectivity
- UserProvider – stores Hive user information
Keep provider logic modular. New providers should live in their own modules under app/ or contexts/.
When adding a dependency, verify the package and maintainer to avoid typosquatted packages. Check with pnpm info <pkg> and inspect its repository. Run pnpm audit after install and review subdependencies using pnpm list <pkg>.
For coding rules, file structure, and patterns, see RULES.md.
CRITICAL - Apostrophes in JSX:
- NEVER use apostrophes (
') in JSX text content - this causesreact/no-unescaped-entitieslinting errors - Instead, reword to avoid contractions or use HTML entities (
') - This is a very common and recurring mistake that breaks builds
Examples:
❌ WRONG:
<Text>User's profile</Text>
<Alert>They'll receive an email</Alert>
<Text>Don't use apostrophes</Text>✅ CORRECT:
<Text>User profile</Text>
<Alert>An email will be sent</Alert>
<Text>Do not use apostrophes</Text>Other JSX escape rules:
- Use
"for quotes in text content - Use
&for ampersands in text content - Use
<and>for angle brackets in text content
The application uses a centralized theming system via Chakra UI with multiple pre-defined themes.
- Theme files: All themes are located in
/themes/directory - Theme provider: The
ThemeProviderinapp/themeProvider.tsxmanages theme state - Available themes: 18+ themes including
hackerPlus(default),forest,bluesky,cyberpunk,nounish,windows95,gruvbox-nogg, etc.
Each theme file must export a Chakra UI theme object using extendTheme() with these required color tokens:
colors: {
background: string, // Main background color
text: string, // Primary text color
primary: string, // Primary brand color
secondary: string, // Secondary brand color
accent: string, // Accent/highlight color
muted: string, // Muted background
border: string, // Border color
error: string, // Error state
success: string, // Success state
warning: string, // Warning state
panel: string, // Panel background
panelHover: string, // Panel hover state
inputBg: string, // Input background
inputBorder: string, // Input border
inputText: string, // Input text
inputPlaceholder: string,// Input placeholder
dim: string, // Dimmed text
subtle: string, // Subtle backgrounds (usually rgba)
}Themes may also define:
fonts– heading, body, mono font familiesfontSizes,fontWeights,lineHeightsborders,shadows,radiicomponents– Chakra component style overrides (Button, Input, Card, etc.)
DO:
- Use semantic color tokens:
bg="background",color="primary",borderColor="border" - Use Chakra props:
<Box bg="panel" borderColor="border" /> - Access theme via
useTheme()hook when needed - Test components with multiple themes
DON'T:
- Hard-code hex colors or specific color values
- Use color values that aren't defined in the theme
- Assume a specific color scheme (dark/light)
- Create a new file in
/themes/yourtheme.ts - Use
extendTheme()and include all required color tokens - Import and register in
app/themeProvider.tsx:- Import:
import yourTheme from "@/themes/yourtheme" - Add to
themeMap:yourtheme: yourTheme
- Import:
- Export from
/themes/index.ts
Users can switch themes at runtime. The selected theme is persisted in localStorage. The fallback theme is hackerPlus. Theme can be overridden via APP_CONFIG.THEME_OVERRIDE or APP_CONFIG.DEFAULT_THEME.
Skatehive supports multiple languages: English, Portuguese (Brazil), Spanish, and Luganda. All human-facing strings must be translation-compatible to maintain consistency across languages.
- Translation files: Located in
lib/i18n/locales/with separate files for each language (en.ts,pt-BR.ts,es.ts,lg.ts) - Translation hook: Use
useTranslations(namespace)from@/lib/i18n/hooksin client components - Centralized exports: All translations registered in
lib/i18n/translations.ts - Context provider:
LocaleContextincontexts/LocaleContext.tsxmanages language state
DO:
- Use the
useTranslationshook for all user-facing strings in client components - Organize strings by namespace (e.g.,
notifications,auction,chat) - Add new translation keys to ALL language files (
en.ts,pt-BR.ts,es.ts,lg.ts) simultaneously - Store static strings in translation files, not hardcoded in components
Example:
'use client';
import { useTranslations } from '@/lib/i18n/hooks';
export function MyComponent() {
const t = useTranslations('myfeature');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}Then in translation files:
// lib/i18n/locales/en.ts
myfeature: {
title: 'My Feature Title',
description: 'Feature description text',
}DON'T:
- Hard-code user-facing strings directly in components
- Add strings to only one or two language files
- Use string concatenation for translatable content
- Store UI text in component files instead of translation files
- Create the namespace in ALL language files (
en.ts,pt-BR.ts,es.ts,lg.ts) - Import and export the namespace in
lib/i18n/translations.ts - Use
useTranslations('namespaceName')in components - Run
pnpm lintto verify the setup
- en – English (🇺🇸)
- pt-BR – Portuguese (Brazil) (🇧🇷)
- es – Spanish (🇪🇸)
- lg – Luganda (🇺🇬)
The userbase system enables "lite" users (email-only or wallet-only accounts) to participate without Hive blockchain keys. These users post through a shared default Hive account with their identity preserved via an overlay system.
- Lite users – Users authenticated via email magic link or Ethereum wallet, without Hive keys
- Soft posts – Posts published under a default Hive account (
skateuser) but attributed to a userbase user - Safe user – HMAC hash of
user_idstored in post metadata for secure identity lookup - Overlay system – React hooks that fetch userbase profiles to display instead of "skateuser"
| File | Purpose |
|---|---|
hooks/useSoftPostOverlay.ts |
Hook for fetching/caching soft post overlays |
lib/userbase/safeUserMetadata.ts |
Extracts safe_user hash from post metadata |
app/api/userbase/soft-posts/route.ts |
API for fetching overlay data |
contexts/UserbaseAuthContext.tsx |
Userbase authentication state |
All userbase tables are prefixed with userbase_ and defined in sql/migrations/:
userbase_users– User profiles (display_name, handle, avatar_url)userbase_identities– Linked identities (email, wallet, Hive account)userbase_soft_posts– Registry of posts made through the default accountuserbase_soft_votes– Registry of votes made through the default account
import useSoftPostOverlay from "@/hooks/useSoftPostOverlay";
import { extractSafeUser } from "@/lib/userbase/safeUserMetadata";
function PostItem({ discussion }) {
const safeUser = extractSafeUser(discussion.json_metadata);
const softPost = useSoftPostOverlay(discussion.author, discussion.permlink, safeUser);
const displayAuthor = softPost?.user.display_name || discussion.author;
const displayAvatar = softPost?.user.avatar_url || defaultAvatar;
}For detailed documentation, see docs/USERBASE_SOFT_POSTS.md.
The feed supports inline "mini-app" cards for special URLs. When a user posts a link to a supported platform, it renders as a rich interactive card instead of a plain link.
| Platform | URL Pattern | Component | Features |
|---|---|---|---|
| Zora Coins | zora.co/.../coin/... |
ZoraCoinEmbed |
Coin price, market cap, CTA |
| Builder DAO | nouns.build/.../vote/... |
BuilderProposalPreview |
Proposal status, vote counts |
| Snapshot | snapshot.box/#/... |
SnapshotProposalPreview |
Vote status, choices |
| POIDH Bounties | skatehive.app/bounties/poidh/... |
BountyPreview |
Reward in ETH+USD, claim modal |
Adding a new embed type follows this pipeline:
- URL detection (
lib/markdown/MarkdownProcessor.ts) — Regex matches URLs and converts to placeholders like[[TYPE:data]] - Placeholder extraction (
extractVideoPlaceholders) — Split regex updated to recognize new type - Renderer split (
components/markdown/EnhancedMarkdownRenderer.tsx) — Splits content on placeholder boundaries - Placeholder handler — Maps placeholder type to the correct React component
- Embed component — Self-contained component that fetches its own data and renders the card
- Add URL regex + placeholder conversion in
MarkdownProcessor.ts(follow theconvertPoidhBountyLinksToPlaceholderspattern) - Add the new type to the
VideoPlaceholdertype union - Update
extractVideoPlaceholdersregex to include new type - In
EnhancedMarkdownRenderer.tsx:- Add new type to the split regex
- Add handler in the placeholder matching section
- Add cleanup regex to strip raw URLs that were already converted
- Create the embed component (fetch data via React Query, render card)
components/bounties/BountyPreview.tsx is a full-featured embed:
- Fetches bounty data from
/api/poidh/bounties/[chainId]/[id] - Fetches ETH price (shared
queryKey: ["eth-price"]across instances) - Shows reward as "Win X ETH ~ ($Y USD)"
- Displays bounty image if available (natural aspect ratio)
- Inline CLAIM modal with full upload flow (IPFS upload, description, title)
- Cross-posts claims to Hive if user has Hive identity
- Entire card is clickable NextLink to bounty detail page
When a feature needs to cross-post content to Hive, use the dual-path approach:
// Path 1: User has Keychain (aioha)
if (aiohaUser) {
await aioha.comment(
null, HIVE_CONFIG.COMMUNITY_TAG, permlink, title, body,
{ tags: [...], app: "Skatehive App 3.0", image: [...] }
);
}
// Path 2: User has userbase identity (stored posting key)
else {
await fetch("/api/userbase/hive/comment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
parent_author: "", parent_permlink: HIVE_CONFIG.COMMUNITY_TAG,
permlink, title, body,
json_metadata: { tags: [...], app: "Skatehive App 3.0", image: [...] },
beneficiaries: [], type: "post",
}),
});
}Key hooks for detecting Hive identity:
useHiveUser()fromcontexts/UserContext— Keychain usersuseUserbaseHiveIdentity()fromhooks/useUserbaseHiveIdentity— Userbase users with linked HiveuseAioha()from@aioha/react-ui— Aioha wallet instance
Use generatePermlink() and generateVideoIframeMarkdown() from lib/markdown/composeUtils.ts.
hooks/useMarketPrices.ts provides centralized price data:
- Fetches HIVE, HBD, and ETH prices from CoinGecko
- Auto-refreshes every 5 minutes
- Fallback prices: HIVE=$0.21, HBD=$1.00, ETH=$2500
- Used by bounty sorting, USD conversions, and feed embeds
For ETH-only price needs, use a shared React Query key:
const { data: ethPrice } = useQuery<number>({
queryKey: ["eth-price"],
queryFn: async () => { /* fetch from CoinGecko */ },
staleTime: 5 * 60 * 1000,
});lib/utils/ipfsUpload.ts provides uploadToIpfsSmart():
- Unified upload function for images and videos
- Auto-routes files >4MB directly to Pinata for reliability
- Returns
{ url, cid }on success - Used by post composer, bounty claims, and profile uploads
components/shared/SkateModal.tsx — Standard modal wrapper used across the app.
Props: isOpen, onClose, title, children, size (default "md")
components/shared/CommunityTotalPayout.tsx — Sidebar widget that:
- Shows total HBD community payout (from HiveHub stats API)
- Alternates with total open bounties USD value every 4 seconds
- Scramble animation on transitions
- Click navigates to
/bountieswhen showing bounty total, opens magazine modal otherwise
The homepage loading chain is sequential:
UserbaseAuthProvider → useUserbaseHiveIdentity → useHiveAccount → VoteWeightProvider
Known optimization opportunities:
- Feed source: PROD uses 6-9 sequential Hive RPCs; DEV uses a single API call. Consider using API as primary.
- Duplicate identity fetches: Both
useUserbaseHiveIdentityandLinkedIdentityProviderfetch/api/userbase/identitiesindependently. - Leaderboard: Fetched eagerly but only shown in AirdropModal — could be lazy-loaded.
- Identity caching: Use
stale-while-revalidatepattern for identity fetches.