Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Contributing to OnePulse

Thank you for your interest in contributing to OnePulse! This guide will help you understand how to contribute effectively to the project.

## Getting Started

1. **Fork the repository** on GitHub
2. **Clone your fork** locally
3. **Install dependencies** with `npm install`
4. **Create a new branch** for your feature/fix: `git checkout -b feature/your-feature-name`

## Development Setup

```bash
# Install dependencies
npm install

# Run the development server
npm run dev

# Check code quality
npm run check

# Automatically fix code issues
npm run fix

# Type checking
npm run typecheck
```

## Code Standards

This project follows the **Ultracite** (Biome preset) code standards. Key principles:

### Style & Formatting

- **Automatic formatting**: Run `npm run fix` before committing
- **Type safety**: Always provide explicit types when enhancing clarity
- **Self-documenting code**: Write clear variable/function names, use JSDoc for complex logic
- **Accessibility**: Include ARIA labels, semantic HTML, and proper keyboard support

### Architecture Guidelines

#### Component Structure

- Use functional components with React 19+ patterns
- Leverage `use*` hooks for complex logic separation
- Place components in meaningful subdirectories
- Keep components focused on a single responsibility

#### Hooks

- Extract complex component logic into custom hooks
- Hook files should start with `use-` prefix
- Use `readonly` for props to prevent mutations
- Return explicit types from hooks

#### Error Handling

- Use the `handleError` function from `@/lib/error-handling`
- Provide user-friendly error messages via `ERROR_MESSAGES` constants
- Log errors to stderr for debugging
- Use try-catch in async operations

#### Validation

- Use validation utilities from `@/lib/validation`
- Always validate user input, especially addresses and numeric values
- Use type guards for runtime safety
- Never trust client-side data implicitly

### TypeScript

- Use strict mode (enforced in tsconfig)
- Prefer explicit over implicit types
- Use `unknown` instead of `any`
- Create meaningful type aliases for complex structures
- Export types alongside implementations

### React & JSX

- Use `"use client"` directive in client components
- Call hooks at the top level (never conditionally)
- Specify stable `key` props in lists (prefer unique IDs over indices)
- Use semantic HTML and ARIA attributes
- Memoize expensive computations with `useMemo`
- Prevent unnecessary renders with `React.memo`

### Performance

- Lazy load components using `React.lazy`
- Use images efficiently with `next/image`
- Avoid inline function definitions in props
- Batch state updates when possible
- Profile with React DevTools before optimizing

### Security

- **Never hardcode secrets** - use environment variables
- **Validate all inputs** - especially addresses and contract calls
- **Prevent XSS** - use `.textContent` instead of `.innerHTML`
- **Avoid eval()** and dynamic code execution
- **Use parameterized queries** for database operations
- **Add SSRF protection** when handling user-provided URLs

## Commit Guidelines

```bash
# Use descriptive, lowercase commit messages
git commit -m "feat: add validation utilities for ethereum addresses"
git commit -m "fix: improve error handling in gm-chain-card component"
git commit -m "docs: add JSDoc comments to countdown component"
git commit -m "refactor: extract chain logic into custom hook"
```

### Commit Message Format

- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation changes
- **refactor**: Code refactoring without feature/fix
- **perf**: Performance improvements
- **test**: Adding or updating tests
- **chore**: Dependency updates or tooling changes

## Pull Request Process

1. **Push to your fork**
```bash
git push origin feature/your-feature-name
```
Comment on lines +128 to +131
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor markdown formatting issue: missing blank line before fenced code block.

Add a blank line before the code block to comply with markdown lint rules (MD031).

📝 Suggested fix
 2. **Create a Pull Request** with:
+
    ```bash
    git push origin feature/your-feature-name
    ```
📝 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
1. **Push to your fork**
```bash
git push origin feature/your-feature-name
```
1. **Push to your fork**
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

129-129: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
In `@CONTRIBUTING.md` around lines 128 - 131, Add a blank line immediately before
the fenced code block that contains the git push example in CONTRIBUTING.md so
the code block is separated from the preceding list item (ensure there is an
empty line before the line starting with ```bash and the example line "git push
origin feature/your-feature-name") to satisfy MD031 markdown linting.


2. **Create a Pull Request** with:
- Clear title describing the change
- Detailed description of what changed and why
- Reference any related issues (e.g., `Closes #123`)
- Screenshots for UI changes

3. **Ensure all checks pass**:
- ✅ Code formatting (`npm run check`)
- ✅ TypeScript compilation (`npm run typecheck`)
- ✅ No console errors or warnings

4. **Request review** from maintainers

5. **Respond to feedback** and make requested changes

## What to Contribute

### Great First Issues

- Improving error messages
- Adding JSDoc comments to existing functions
- Creating utility functions with tests
- Fixing accessibility issues
- Improving TypeScript types

### Feature Suggestions

- New validation utilities
- Enhanced error handling
- Performance optimizations
- Better component composition
- Additional configuration options

### Bug Fixes

- Document the bug clearly
- Include steps to reproduce if possible
- Link to any relevant error logs
- Test your fix thoroughly

## Testing

While we don't have formal tests yet, ensure:

1. **Manual testing** in development mode
2. **Cross-browser testing** (Chrome, Firefox, Safari)
3. **Mobile testing** (Farcaster Mini App context)
4. **Type checking** passes without errors
5. **Code quality** passes linting

## Documentation

- Add JSDoc comments to all exported functions
- Update README.md for significant changes
- Include inline comments for non-obvious logic
- Document any new environment variables
- Provide examples for complex features

## Project Structure Reference

```
app/ # Next.js app router and pages
├── api/ # API routes
├── layout.tsx # Root layout
└── page.tsx # Home page

components/ # React components
├── gm-chain-card/ # Per-chain GM functionality
├── providers/ # Context providers
└── ui/ # Reusable UI components

lib/ # Utilities and helpers
├── constants.ts # Configuration constants
├── error-handling.ts # Error handling utilities
├── utils.ts # General utilities
└── validation.ts # Input validation

types/ # TypeScript type definitions
hooks/ # Custom React hooks
```
Comment on lines +193 to +212
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: Add language specifier to the fenced code block.

The project structure diagram should use a language specifier (e.g., text or plaintext) to satisfy markdown lint rules (MD040).

📝 Suggested fix
-```
+```text
 app/               # Next.js app router and pages
📝 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
```
app/ # Next.js app router and pages
├── api/ # API routes
├── layout.tsx # Root layout
└── page.tsx # Home page
components/ # React components
├── gm-chain-card/ # Per-chain GM functionality
├── providers/ # Context providers
└── ui/ # Reusable UI components
lib/ # Utilities and helpers
├── constants.ts # Configuration constants
├── error-handling.ts # Error handling utilities
├── utils.ts # General utilities
└── validation.ts # Input validation
types/ # TypeScript type definitions
hooks/ # Custom React hooks
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

193-193: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@CONTRIBUTING.md` around lines 193 - 212, The fenced project-structure code
block in CONTRIBUTING.md is missing a language specifier which triggers MD040;
update the opening fence for the block that starts with "app/               #
Next.js app router and pages" to include a plaintext specifier (e.g., change
"```" to "```text") so the diagram is treated as plain text by the markdown
linter.


## Environment Variables

Required environment variables:
- None yet (configured via contracts and constants)

Optional for development:
- `NEXT_PUBLIC_*` - Exposed to browser

## Questions?

- Check existing issues and discussions
- Review the codebase and inline comments
- Reach out to maintainers on Farcaster (@pirosb3, @linda, @deodad)

## Code of Conduct

- Be respectful and inclusive
- Provide constructive feedback
- Focus on the code, not the person
- Help others learn and grow

---

Thank you for contributing to OnePulse! 🚀
11 changes: 9 additions & 2 deletions components/countdown.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { ClockIcon } from "lucide-react";
import { useCountdown } from "@/hooks/use-countdown";

/**
* Countdown component that displays time until the next GM reset
* Shows a live countdown in HH:MM:SS format
* Uses a pill-shaped design with hover effects
*
* @returns The rendered countdown component
*/
export function Countdown() {
const { text } = useCountdown();

return (
<div className="group mx-auto flex w-fit items-center gap-2.5 rounded-full border border-border/60 bg-linear-to-b from-muted/40 to-muted/60 px-4 py-2.5 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-border hover:shadow-md">
<div className="flex items-center gap-2">
<ClockIcon className="size-3.5" />
<ClockIcon className="size-3.5" aria-hidden="true" />
<span className="align-middle font-medium text-muted-foreground text-xs">
Next reset in
</span>
<span className="font-mono font-semibold text-xs tabular-nums tracking-tight">
<span className="font-mono font-semibold text-xs tabular-nums tracking-tight" aria-live="polite" aria-label={`Time until next reset: ${text}`}>
{text}
Comment on lines +21 to 22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Throttle live-region announcements to avoid per-second screen reader spam (Line 21).
With aria-live="polite" on text that updates every second, assistive tech may announce continuously. Consider hiding the fast-updating text from AT and adding a separate, less-frequently changing live region (e.g., minute-level).

♻️ Proposed fix (screen reader-friendly live region)
 export function Countdown() {
   const { text } = useCountdown();
+  const [hours, minutes] = text.split(":");
+  const a11yText = `${hours}:${minutes}`;

   return (
     <div className="group mx-auto flex w-fit items-center gap-2.5 rounded-full border border-border/60 bg-linear-to-b from-muted/40 to-muted/60 px-4 py-2.5 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-border hover:shadow-md">
       <div className="flex items-center gap-2">
         <ClockIcon className="size-3.5" aria-hidden="true" />
         <span className="align-middle font-medium text-muted-foreground text-xs">
           Next reset in
         </span>
-        <span className="font-mono font-semibold text-xs tabular-nums tracking-tight" aria-live="polite" aria-label={`Time until next reset: ${text}`}>
+        <span className="font-mono font-semibold text-xs tabular-nums tracking-tight" aria-hidden="true">
           {text}
         </span>
+        <span className="sr-only" aria-live="polite" aria-atomic="true">
+          Time until next reset: {a11yText}
+        </span>
       </div>
     </div>
   );
 }

Based on learnings, avoid rapid ARIA updates that can overwhelm assistive technologies.

🤖 Prompt for AI Agents
In `@components/countdown.tsx` around lines 21 - 22, The per-second updating span
(the text variable rendered inside the Countdown component) currently has
aria-live="polite" and will cause screen readers to announce every second;
change that span to aria-hidden="true" to hide the fast-updating text from AT,
then add a separate visually-hidden/live region element (e.g.,
minuteAnnouncement or ariaAnnouncement state) that uses aria-live="polite" and
is only updated on less frequent boundaries (minute changes or throttled via a
useEffect that updates when Math.floor(seconds/60) changes) so screen readers
get sparse, meaningful announcements instead of per-second spam.

</span>
</div>
Expand Down
8 changes: 8 additions & 0 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ import { useHeaderLogic } from "@/components/header/use-header-logic";
import { UserInfo } from "@/components/user-info";
import { minikitConfig } from "@/minikit.config";

/**
* Header component for OnePulse
* Displays the app name when not connected and user info when connected
* Fixed at the top of the viewport with z-index 20
*
* @returns The rendered header component
*/
export function Header(): JSX.Element {
const { address, shouldShowUserInfo } = useHeaderLogic();

return (
<div className="fixed top-0 right-0 left-0 z-20 mx-auto h-16 w-full max-w-lg bg-transparent">
<div className="flex h-16 items-center justify-between rounded-b-lg border border-border bg-background px-4 shadow-lg">
Expand Down
24 changes: 24 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Central export point for lib utilities
* Provides organized access to all utility functions and types
*/

// Error handling and validation
export * from "./error-handling";
export * from "./validation";

// Utility functions
export * from "./utils";
export * from "./constants";

// Type utilities
export type { SupportedChain, ChainId } from "./constants";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant type export.

If this barrel file is kept, the explicit type re-export on line 15 is redundant since line 12 already does export * from "./constants", which includes all exports from that module.

📝 Remove redundant export
 // Utility functions
 export * from "./utils";
 export * from "./constants";
-
-// Type utilities
-export type { SupportedChain, ChainId } from "./constants";
🤖 Prompt for AI Agents
In `@lib/index.ts` at line 15, The file currently does both "export * from
\"./constants\"" and an explicit "export type { SupportedChain, ChainId }" which
is redundant; remove the explicit type re-export (the "export type {
SupportedChain, ChainId }" line) so the barrel relies on the existing "export *
from \"./constants\"" export, and run a quick type-check to confirm nothing else
relies on that explicit line.


// ENS utilities
export * from "./ens-utils";

// Smart contract utilities
export * from "./contracts";

// Font utilities
export * from "./fonts";
Comment on lines +1 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Barrel file anti-pattern: violates project coding guidelines.

This file re-exports everything from multiple modules, which the project's coding guidelines explicitly prohibit: "Avoid barrel files (index files that re-export everything)."

Barrel files can cause:

  • Poor tree-shaking (larger bundles)
  • Circular dependency issues
  • Slower IDE performance and type checking

Consider removing this file and having consumers import directly from the specific modules they need (e.g., import { isValidAddress } from "@/lib/validation").

Based on learnings, the project guidelines state to avoid barrel files in **/index.{ts,tsx,js,jsx}.

🤖 Prompt for AI Agents
In `@lib/index.ts` around lines 1 - 24, This barrel file re-exports everything and
must be removed: delete lib/index.ts (the re-exporting of "./error-handling",
"./validation", "./utils", "./constants", the type export of
SupportedChain/ChainId, "./ens-utils", "./contracts", and "./fonts") and update
all consumers to import directly from the specific modules (e.g., import {
isValidAddress } from "validation", import { SupportedChain, ChainId } from
"constants", import { createContractHelper } from "contracts", etc.); search the
repo for imports referencing the barrel (e.g., imports from "lib" or "@/lib")
and replace them with direct module imports, then run the TypeScript build/tests
to ensure no broken imports remain.

54 changes: 54 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,57 @@ export function isSponsoredOnChain(sponsored: boolean): boolean {
export function getChainIconName(): string {
return "base";
}

/**
* Format a number with a specific number of decimal places
*/
export function formatNumber(value: number, decimals: number = 2): string {
return value.toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}

/**
* Truncate an Ethereum address to display format (0x1234...5678)
*/
export function truncateAddress(address: string, chars: number = 4): string {
if (!address) return "";
if (address.length <= chars * 2 + 2) return address;
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
}

/**
* Wait for a specified duration (in milliseconds)
* Useful for rate limiting and testing
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

/**
* Create a memoized version of a computation result
* Only recomputes if dependencies change
*/
export function memoize<T>(
computeFn: () => T,
dependencies: readonly unknown[]
): {
current: T;
dependencies: readonly unknown[];
} {
return {
current: computeFn(),
dependencies,
};
}
Comment on lines +109 to +120
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Misleading function name: memoize doesn't actually memoize.

This function computes the value immediately on every call and returns a snapshot object. It lacks any caching mechanism—callers must manually track previous results and compare dependencies, which defeats the purpose of a memoization utility.

Consider either:

  1. Renaming to createMemoEntry or createComputedSnapshot to reflect its actual behavior
  2. Implementing actual memoization with internal cache storage
♻️ Option 1: Rename to reflect actual behavior
-/**
- * Create a memoized version of a computation result
- * Only recomputes if dependencies change
- */
-export function memoize<T>(
+/**
+ * Create a snapshot of a computed value with its dependencies
+ * Useful for manual comparison in subsequent calls
+ */
+export function createComputedSnapshot<T>(
   computeFn: () => T,
   dependencies: readonly unknown[]
 ): {
   current: T;
   dependencies: readonly unknown[];
 } {
   return {
     current: computeFn(),
     dependencies,
   };
 }
♻️ Option 2: Implement actual memoization
/**
 * Create a memoized function that caches its result
 * Only recomputes when called with different dependencies
 */
export function memoize<T>(
  computeFn: () => T,
  dependencies: readonly unknown[]
): () => T {
  let cachedDependencies: readonly unknown[] = dependencies;
  let cachedValue: T = computeFn();

  return () => {
    if (!arraysEqual([...cachedDependencies], [...dependencies])) {
      cachedDependencies = dependencies;
      cachedValue = computeFn();
    }
    return cachedValue;
  };
}
🤖 Prompt for AI Agents
In `@lib/utils.ts` around lines 109 - 120, The function memoize currently executes
computeFn immediately and returns a snapshot object rather than caching results;
either rename memoize to something like createMemoEntry or
createComputedSnapshot to reflect that it returns { current, dependencies }, or
implement real memoization inside memoize by storing cachedDependencies and
cachedValue and returning a function (or cached value) that only recomputes
computeFn when dependencies change; update callers expecting memoize to now
either call the returned getter (if implementing a function) or use the new name
to avoid semantic confusion (refer to the memoize export and any call sites
using memoize/current/dependencies to apply the change).


/**
* Check if two arrays are equal (shallow comparison)
*/
export function arraysEqual<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) return false;
return a.every((item, index) => item === b[index]);
}
Loading