Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a6e83bf
security: restore npm audit fix changes to package-lock.json
bernie-developer Dec 27, 2025
19666e2
feat: make search bar sticky on scroll
bernie-developer Dec 27, 2025
722c3a0
chore: remove pnpm-lock.yaml to use npm exclusively
bernie-developer Dec 27, 2025
49fc4bc
feat: add CoinMarketCap integration with Top 100 and Active filters
bernie-developer Dec 27, 2025
6670da8
feat: graceful degradation when API key is not configured
bernie-developer Dec 27, 2025
1d01749
style: increase width of API key info message
bernie-developer Dec 27, 2025
f68b80d
fix: remove Dutch placeholder from API key configuration
bernie-developer Dec 27, 2025
e3fabf4
fix: improve filter logic for icons without symbols
bernie-developer Dec 27, 2025
332bb90
fix: remove visible code comment and add API debugging
bernie-developer Dec 27, 2025
7aa6751
feat: implement proper active coin detection using CoinMarketCap map API
bernie-developer Dec 27, 2025
1f37d45
feat: optimize active coins cache to weekly checks
bernie-developer Dec 27, 2025
b0fc466
debug: add extensive logging for active coins API
bernie-developer Dec 27, 2025
637259e
fix: filter coins by is_active field in API response
bernie-developer Dec 27, 2025
50830fd
fix: reduce batch size to 10 symbols and improve error logging
bernie-developer Dec 27, 2025
6eb957f
fix: increase delay to 2.5s between API calls for rate limiting
bernie-developer Dec 27, 2025
e1e10e6
feat: implement static JSON-based active coins system
bernie-developer Dec 27, 2025
9b0ac44
chore: add dotenv and generate initial active coins data
bernie-developer Dec 27, 2025
0178bb8
fix: improve active coins detection with retry logic
bernie-developer Dec 27, 2025
bc15d07
docs: update README with filtering features and API configuration
bernie-developer Dec 27, 2025
6f26725
docs: remove specific numbers to keep README evergreen
bernie-developer Dec 27, 2025
533687b
chore: remove unused cache duration config for active coins
bernie-developer Dec 27, 2025
263c062
style: change action button colors from indigo to blue
bernie-developer Dec 27, 2025
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
6 changes: 6 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# CoinMarketCap API Configuration
# Get your API key from: https://coinmarketcap.com/api/
COINMARKETCAP_API_KEY=your_api_key_here

# Cache duration for Top 100 data (default: 24 hours = 86400000 ms)
CMC_CACHE_DURATION=86400000
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
.next/
node_modules/

# Environment variables (NEVER commit API keys!)
.env
.env.local
.env*.local
.env.development.local
.env.test.local
.env.production.local

# OS files
.DS_Store
*.swp
*.swo
47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ This project is built using a modern web development stack, focusing on performa

This application comes packed with a variety of features to enhance the user experience.
* **Extensive Icon Library**: Browse a large collection of cryptocurrency icons in SVG format. This ensures a wide range of options for users.
* **Search Functionality**: Easily find icons by name or symbol using a responsive search bar. This greatly improves icon discovery.
* **Search Functionality**: Easily find icons by name or symbol using a responsive search bar with sticky positioning. This greatly improves icon discovery.
* **Smart Filtering**: Filter coins by market data:
* **Top 100 Only**: Show only the top 100 cryptocurrencies by market cap
* **Active Only**: Display only actively traded cryptocurrencies (verified via CoinMarketCap)
* **Icon Preview**: View a larger version of each icon for detailed inspection. This allows users to see the details before downloading.
* **Copy SVG Code**: Quickly copy the SVG code of any icon to your clipboard for direct use in projects. This streamlines the integration process.
* **Download Icons**: Download individual SVG icon files. This offers flexibility for offline use or custom modifications.
Expand Down Expand Up @@ -57,11 +60,47 @@ To run this project locally, follow these steps. These steps will guide you thro

Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.

## CoinMarketCap API Configuration (Optional)

The filtering features work out of the box with pre-generated data. However, if you want to update the active coins data yourself:

1. **Get a CoinMarketCap API key**: Sign up at [CoinMarketCap API](https://coinmarketcap.com/api/) (free tier available)

2. **Create a `.env.local` file** in the project root:

```bash
COINMARKETCAP_API_KEY=your_api_key_here
CMC_CACHE_DURATION=86400000
```

3. **Update active coins data** (optional, only when you want to refresh):

```bash
npm run update-active-coins
```

This script will:
* Check all cryptocurrency symbols from the icon library
* Query CoinMarketCap API to verify which coins are actively traded
* Generate updated JSON files in `/public/data/`
* Take approximately 5-10 minutes to complete (depending on library size)

**Note**: The app works perfectly without an API key. Filter features will use the pre-generated static data files.

## Project Structure

* `components/`: Reusable React components (e.g., `IconCard`, `SearchBar`, `PreviewModal`).
* `hooks/`: Custom React hooks for logic encapsulation (e.g., `useCryptoIcons`, `useToast`).
* `components/`: Reusable React components (e.g., `IconCard`, `SearchBar`, `FilterBar`, `PreviewModal`).
* `hooks/`: Custom React hooks for logic encapsulation (e.g., `useCryptoIcons`, `useMarketData`, `useToast`).
* `pages/`: Next.js pages and API routes (e.g., `index.tsx` for the main page, `api/icons.ts` for serving icon data).
* `public/icons/`: Directory containing all the SVG cryptocurrency icon files.
* `pages/api/`: API routes for data fetching:
* `coinmarketcap/top100.ts`: Fetches top 100 coins by market cap (with 24h cache)
* `active-coins.ts`: Serves pre-generated active coins data
* `public/icons/`: Directory containing SVG cryptocurrency icon files.
* `public/data/`: Static JSON files for filtering:
* `active-coins.json`: List of actively traded cryptocurrencies
* `inactive-coins.json`: Inactive or unknown coins
* `all-coins.json`: Complete list of all symbols from the icon library
* `scripts/`: Maintenance scripts:
* `update-active-coins.cjs`: Updates active coins data from CoinMarketCap API
* `styles/`: Global styles and Tailwind CSS configuration.
* `types/`: TypeScript type definitions.
95 changes: 95 additions & 0 deletions components/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';

interface FilterBarProps {
showTop100Only: boolean;
onToggleTop100: () => void;
showActiveOnly: boolean;
onToggleActive: () => void;
isLoading?: boolean;
apiKeyConfigured?: boolean;
hasActiveData?: boolean; // New: hide Active button if no data
}

export const FilterBar: React.FC<FilterBarProps> = ({
showTop100Only,
onToggleTop100,
showActiveOnly,
onToggleActive,
isLoading = false,
apiKeyConfigured = true,
hasActiveData = true,
}) => {
const isDisabled = isLoading || !apiKeyConfigured;

// Don't show filter bar if both filters are unavailable
if (!apiKeyConfigured && !hasActiveData) {
return null;
}

return (
<div className="flex flex-col items-center mb-6">
<div className="flex flex-wrap gap-3 items-center justify-center">
<div className="flex gap-3">
{/* Top 100 Filter Button */}
{apiKeyConfigured && (
<button
onClick={onToggleTop100}
disabled={isDisabled}
className={`
px-4 py-2 rounded-lg font-medium transition-all duration-200
${showTop100Only && apiKeyConfigured
? 'bg-indigo-600 text-white shadow-md hover:bg-indigo-700'
: 'bg-white text-gray-700 border border-gray-300 hover:border-indigo-400 hover:text-indigo-600'
}
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<span className="flex items-center gap-2">
<span className="text-lg">🏆</span>
<span>Top 100 Only</span>
</span>
</button>
)}

{/* Active Coins Filter Button */}
{hasActiveData && (
<button
onClick={onToggleActive}
disabled={isDisabled || !hasActiveData}
className={`
px-4 py-2 rounded-lg font-medium transition-all duration-200
${showActiveOnly && hasActiveData
? 'bg-green-600 text-white shadow-md hover:bg-green-700'
: 'bg-white text-gray-700 border border-gray-300 hover:border-green-400 hover:text-green-600'
}
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<span className="flex items-center gap-2">
<span className="text-lg">✓</span>
<span>Active Only</span>
</span>
</button>
)}
</div>

{/* Loading indicator */}
{isLoading && (
<span className="text-sm text-gray-500 italic">
Loading market data...
</span>
)}
</div>

{/* API Key not configured message */}
{!apiKeyConfigured && !isLoading && (
<div className="mt-3 text-xs text-gray-500 text-center max-w-2xl">
<span className="inline-flex items-center gap-1">
<span>ℹ️</span>
<span>Filter features require a CoinMarketCap API key. Add your key to <code className="bg-gray-100 px-1 rounded">.env.local</code> to enable filtering.</span>
</span>
</div>
)}
</div>
);
};
6 changes: 3 additions & 3 deletions components/IconCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,21 @@ export const IconCard: React.FC<IconCardProps> = ({
<div className="grid grid-cols-3 gap-2">
<button
onClick={() => onPreview(icon)}
className="flex items-center justify-center p-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-all duration-200 shadow-sm hover:shadow-md"
className="flex items-center justify-center p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all duration-200 shadow-sm hover:shadow-md"
title="Preview"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={handleCopyClick}
className="flex items-center justify-center p-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-all duration-200 shadow-sm hover:shadow-md"
className="flex items-center justify-center p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all duration-200 shadow-sm hover:shadow-md"
title="Copy SVG"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={() => onDownload(icon)}
className="flex items-center justify-center p-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-all duration-200 shadow-sm hover:shadow-md"
className="flex items-center justify-center p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all duration-200 shadow-sm hover:shadow-md"
title="Download"
>
<Download className="w-4 h-4" />
Expand Down
122 changes: 122 additions & 0 deletions hooks/useMarketData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useState, useEffect } from 'react';

interface CoinData {
id: number;
name: string;
symbol: string;
cmc_rank: number;
is_active: number;
}

interface MarketData {
coins: CoinData[];
timestamp: number;
}

interface ActiveCoinsData {
activeSymbols: string[];
timestamp: number;
totalChecked: number;
apiCallsMade: number;
}

interface UseMarketDataResult {
marketData: MarketData | null;
loading: boolean;
error: string | null;
isTop100Coin: (symbol: string) => boolean;
isActiveCoin: (symbol: string) => boolean;
apiKeyConfigured: boolean;
hasActiveData: boolean; // New: whether we have active coins data
}

export const useMarketData = (): UseMarketDataResult => {
const [marketData, setMarketData] = useState<MarketData | null>(null);
const [activeCoinsData, setActiveCoinsData] = useState<ActiveCoinsData | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [apiKeyConfigured, setApiKeyConfigured] = useState<boolean>(true);

useEffect(() => {
const fetchMarketData = async () => {
try {
// Fetch both top 100 and active coins data in parallel
const [top100Response, activeCoinsResponse] = await Promise.all([
fetch('/api/coinmarketcap/top100'),
fetch('/api/active-coins') // New simplified API
]);

if (!top100Response.ok || !activeCoinsResponse.ok) {
throw new Error(`HTTP error! status: ${top100Response.status} / ${activeCoinsResponse.status}`);
}

const [top100Result, activeCoinsResult] = await Promise.all([
top100Response.json(),
activeCoinsResponse.json()
]);

// Check if API key is configured
if (!top100Result.success || !activeCoinsResult.success) {
if (top100Result.error === 'API_KEY_NOT_CONFIGURED' || activeCoinsResult.error === 'API_KEY_NOT_CONFIGURED') {
setApiKeyConfigured(false);
setError(null); // No error message, just disabled features
} else {
throw new Error(top100Result.error || activeCoinsResult.error || 'Failed to fetch market data');
}
} else {
setMarketData(top100Result.data);

// Convert new format to old format for compatibility
if (activeCoinsResult.data) {
setActiveCoinsData({
activeSymbols: activeCoinsResult.data.symbols || [],
timestamp: Date.parse(activeCoinsResult.data.timestamp) || Date.now(),
totalChecked: activeCoinsResult.data.total || 0,
apiCallsMade: 0 // Static file, no API calls
});
}

setApiKeyConfigured(true);
setError(null);
}
} catch (err) {
console.error('Failed to load market data:', err);
setError('Failed to load market data. Filter features may be limited.');
} finally {
setLoading(false);
}
};

fetchMarketData();
}, []);

// Helper function to check if a coin is in top 100
const isTop100Coin = (symbol: string): boolean => {
if (!marketData) return true; // If no data, show all

const normalizedSymbol = symbol.toUpperCase().trim();
return marketData.coins.some(
coin => coin.symbol.toUpperCase() === normalizedSymbol
);
};

// Helper function to check if a coin is active
const isActiveCoin = (symbol: string): boolean => {
if (!activeCoinsData) return true; // If no data, show all

const normalizedSymbol = symbol.toUpperCase().trim();

// Check if symbol is in the active coins list
return activeCoinsData.activeSymbols.includes(normalizedSymbol);
};

return {
marketData,
loading,
error,
isTop100Coin,
isActiveCoin,
apiKeyConfigured,
hasActiveData: activeCoinsData !== null && activeCoinsData.activeSymbols.length > 0,
};
};
Loading