Skip to content
Merged
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
35 changes: 23 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
VITE_BACKEND_URL=/api
# UTM settings for shared profile links (optional)
# Set any of these to enable UTM parameters on shared profile URLs.
VITE_UTM_SOURCE=twitter
VITE_UTM_MEDIUM=social
VITE_UTM_CAMPAIGN=share_profile
#VITE_UTM_TERM=
#VITE_UTM_CONTENT=
# Access Layer Client — environment variables
# Copy this file to `.env` and adjust values as needed:
# cp .env.example .env
# All client-exposed variables must be prefixed with VITE_ (see https://vitejs.dev/guide/env-and-mode).
# See CONTRIBUTING.md ("Environment variables") for which vars are required vs optional.

# --- Required (sensible defaults provided) ---

# Base URL for the backend API. Use the local backend during development.
VITE_BACKEND_URL=http://localhost:3000/api/v1

# Chain ID selected by default on load. 84532 = Base Sepolia testnet.
VITE_DEFAULT_CHAIN_ID=84532

# RPC URL for a local Anvil node (chain 31337). Used when developing against a local chain.
VITE_ANVIL_RPC_URL=http://127.0.0.1:8545

# RPC URL for the Base Sepolia testnet (chain 84532). The public default works out of the box.
VITE_BASE_SEPOLIA_RPC_URL=https://sepolia.base.org

# --- Optional ---

# RPC URL for the Ethereum Sepolia testnet (chain 11155111). Get one from Alchemy, Infura, or another provider.
VITE_SEPOLIA_RPC_URL=

# RPC URL for Ethereum mainnet (chain 1). Only needed when testing against mainnet.
VITE_MAINNET_RPC_URL=

# UTM settings for shared profile links (optional)
# Set any of these to enable UTM parameters on shared profile URLs.
# Remove or comment out to disable UTM tracking.
# Example configuration:
# UTM parameters appended to shared profile links (optional).
# Set any of these to enable UTM tracking; remove or leave blank to disable.
VITE_UTM_SOURCE=accesslayer
VITE_UTM_MEDIUM=share
VITE_UTM_CAMPAIGN=profile-sharing
Expand Down
42 changes: 41 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ Thanks for contributing to the frontend for Access Layer, a Stellar-native creat
## Local setup

1. Install Node.js 20+ and `pnpm`.
2. Copy `.env.example` to `.env` and add any local values you need.
2. Copy `.env.example` to `.env` and adjust values as needed (see [Environment variables](#environment-variables)):

```bash
cp .env.example .env
```

3. Install dependencies:

```bash
Expand All @@ -25,6 +30,41 @@ pnpm install
pnpm dev
```

## Environment variables

All client-exposed variables are prefixed with `VITE_` so Vite can expose them to the
browser. The defaults in `.env.example` are enough to run the client locally — you only
need to fill in optional values for the networks you actually want to test against.
Validation lives in [`src/utils/env.utils.ts`](./src/utils/env.utils.ts).

### Required (defaults provided)

| Variable | Description |
| --- | --- |
| `VITE_BACKEND_URL` | Base URL for the backend API. Point this at your local backend during development (e.g. `http://localhost:3000/api/v1`). |
| `VITE_DEFAULT_CHAIN_ID` | Chain ID selected by default on load. `84532` is Base Sepolia, the recommended testnet. |
| `VITE_ANVIL_RPC_URL` | RPC URL for a local [Anvil](https://book.getfoundry.sh/anvil/) node (chain `31337`), used when developing against a local chain. |
| `VITE_BASE_SEPOLIA_RPC_URL` | RPC URL for the Base Sepolia testnet (chain `84532`). The public default `https://sepolia.base.org` works without an account. |

### Optional

| Variable | Description |
| --- | --- |
| `VITE_SEPOLIA_RPC_URL` | RPC URL for the Ethereum Sepolia testnet (chain `11155111`). Only needed when testing on Sepolia. |
| `VITE_MAINNET_RPC_URL` | RPC URL for Ethereum mainnet (chain `1`). Only needed when testing against mainnet. |
| `VITE_UTM_SOURCE`, `VITE_UTM_MEDIUM`, `VITE_UTM_CAMPAIGN`, `VITE_UTM_TERM`, `VITE_UTM_CONTENT` | UTM parameters appended to shared profile links. Leave blank to disable UTM tracking. |

### Where to get testnet RPC URLs

- **Base Sepolia** — the public endpoint `https://sepolia.base.org` is preconfigured and
needs no account. For higher rate limits, create a free Base Sepolia endpoint at
[Alchemy](https://www.alchemy.com/) or [Infura](https://www.infura.io/).
- **Ethereum Sepolia** — create a free Sepolia endpoint at
[Alchemy](https://www.alchemy.com/) or [Infura](https://www.infura.io/), or use a public
endpoint from [Chainlist](https://chainlist.org/?testnets=true&search=sepolia).
- **Local Anvil** — no URL to fetch; run `anvil` from [Foundry](https://book.getfoundry.sh/)
and it serves the default `http://127.0.0.1:8545`.

## Verification commands

Run these before opening a pull request:
Expand Down
69 changes: 69 additions & 0 deletions src/components/common/CreatorPageErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Component, type ErrorInfo, type ReactNode } from 'react';
import { Link } from 'react-router';
import { AlertCircle, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';

interface Props {
children: ReactNode;
}

interface State {
hasError: boolean;
}

/**
* Error boundary scoped to creator detail routes. When a creator page throws
* during render, this catches the error and shows a fallback with a link back
* to the creator list instead of crashing the whole app to a blank screen.
*/
class CreatorPageErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};

public static getDerivedStateFromError(): State {
return { hasError: true };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
if (import.meta.env.DEV) {
console.error('Error rendering creator page:', error, errorInfo);
}
}

public render() {
if (this.state.hasError) {
return (
<main
className="flex min-h-screen flex-col items-center justify-center gap-6 bg-[#06111f] px-6 py-16 text-center text-white"
role="alert"
aria-live="assertive"
>
<div className="flex flex-col items-center gap-3">
<AlertCircle className="size-10 text-amber-400" aria-hidden="true" />
<h1 className="font-grotesque text-3xl font-black tracking-tight sm:text-4xl">
This creator page could not load
</h1>
<p className="max-w-md font-jakarta text-base leading-7 text-white/70">
Something went wrong while loading this creator. The rest of the
marketplace is still available.
</p>
</div>
<Button
asChild
className="h-12 rounded-xl bg-amber-400 px-5 font-jakarta font-black text-slate-950 hover:bg-amber-300"
>
<Link to="/creators">
<ArrowLeft className="size-4" aria-hidden="true" />
Back to creators
</Link>
</Button>
</main>
);
}

return this.props.children;
}
}

export default CreatorPageErrorBoundary;
60 changes: 60 additions & 0 deletions src/components/common/__tests__/CreatorPageErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { ReactNode } from 'react';
import { describe, expect, it, vi, afterEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import CreatorPageErrorBoundary from '@/components/common/CreatorPageErrorBoundary';

// Mock component that throws a render error on demand
const BuggyCreatorPage = ({ shouldThrow = false }: { shouldThrow?: boolean }) => {
if (shouldThrow) {
throw new Error('Creator page render error');
}
return <div>Creator profile content</div>;
};

const renderInRouter = (ui: ReactNode) =>
render(<MemoryRouter>{ui}</MemoryRouter>);

describe('CreatorPageErrorBoundary', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('renders children when no error occurs', () => {
renderInRouter(
<CreatorPageErrorBoundary>
<BuggyCreatorPage />
</CreatorPageErrorBoundary>
);

expect(screen.getByText('Creator profile content')).toBeInTheDocument();
});

it('renders fallback UI instead of crashing when a creator page throws', () => {
// Suppress React's expected error logging for the thrown error
vi.spyOn(console, 'error').mockImplementation(() => {});

renderInRouter(
<CreatorPageErrorBoundary>
<BuggyCreatorPage shouldThrow={true} />
</CreatorPageErrorBoundary>
);

expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(/could not load/i)).toBeInTheDocument();
expect(screen.queryByText('Creator profile content')).not.toBeInTheDocument();
});

it('fallback includes a link back to the creator list', () => {
vi.spyOn(console, 'error').mockImplementation(() => {});

renderInRouter(
<CreatorPageErrorBoundary>
<BuggyCreatorPage shouldThrow={true} />
</CreatorPageErrorBoundary>
);

const link = screen.getByRole('link', { name: /back to creators/i });
expect(link).toHaveAttribute('href', '/creators');
});
});
Loading