zero-build-frontend

安装量: 53
排名: #13933

安装

npx skills add https://github.com/jamditis/claude-skills-journalism --skill zero-build-frontend

Zero-build frontend development

Patterns for building production-quality web applications without build tools, bundlers, or complex toolchains.

React via CDN (esm.sh) Basic setup

Zero-Build React App

React with htm (no JSX, no build) // index.js import React, { useState, useEffect, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import htm from 'htm';

// Bind htm to React.createElement const html = htm.bind(React.createElement);

// Components use html`` instead of JSX function App() { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState('');

useEffect(() => { loadData(); }, []);

async function loadData() { try { const response = await fetch('data/archive-data.json'); const data = await response.json(); setRecords(data.records); } catch (error) { console.error('Failed to load data:', error); } finally { setLoading(false); } }

const filtered = records.filter(r => r.title.toLowerCase().includes(search.toLowerCase()) );

if (loading) { return html<div class="flex items-center justify-center h-screen"> <div class="animate-spin w-8 h-8 border-4 border-brand-primary border-t-transparent rounded-full"></div> </div>; }

return html`

Archive Explorer

setSearch(e.target.value)} class="mt-2 w-full p-2 bg-gray-800 rounded border border-gray-600 focus:border-brand-primary outline-none" />

  <main class="p-4">
    <${RecordList} records=${filtered} />
  </main>
</div>

`; }

function RecordList({ records }) { return html<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> ${records.map(record => html <${RecordCard} key=${record.id} record=${record} /> )} </div>; }

function RecordCard({ record }) { return html<article class="p-4 bg-gray-800 rounded-lg border border-gray-700 hover:border-brand-primary transition-colors"> <h2 class="font-display text-lg mb-2">${record.title}</h2> <p class="text-sm text-gray-400 mb-2">${record.publication_date}</p> <p class="text-sm line-clamp-3">${record.summary}</p> <div class="mt-2 flex flex-wrap gap-1"> ${record.tags?.map(tag => html ${tag} )} </div> </article>; }

// Mount app const root = createRoot(document.getElementById('root')); root.render(html<${App} />);

Data caching with localStorage // services/cacheService.js

const CACHE_TTL = 60 * 60 * 1000; // 1 hour

export function getCached(key) { const cached = localStorage.getItem(key); if (!cached) return null;

try { const { data, timestamp } = JSON.parse(cached); if (Date.now() - timestamp > CACHE_TTL) { localStorage.removeItem(key); return null; } return data; } catch { localStorage.removeItem(key); return null; } }

export function setCache(key, data) { localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() })); }

export async function fetchWithCache(url, cacheKey) { // Check cache first const cached = getCached(cacheKey); if (cached) return cached;

// Fetch fresh data const response = await fetch(url); const data = await response.json();

// Cache for next time setCache(cacheKey, data);

return data; }

// Usage const records = await fetchWithCache('data/archive-data.json', 'archive-records');

Leaflet.js maps Basic map setup

Map application with clustering // js/app.js

class MapApp { constructor() { this.map = null; this.markers = null; this.data = []; this.filters = { year: null, county: null, status: null }; }

async init() { this.setupMap(); await this.loadData(); this.renderMarkers(); this.setupFilters(); }

setupMap() { // Initialize map centered on NJ this.map = L.map('map', { center: [40.0583, -74.4057], zoom: 8, scrollWheelZoom: false, // Disable mouse wheel zoom zoomControl: false // We'll add custom controls });

// Add tile layer (CARTO Voyager)
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
  attribution: '&copy; OpenStreetMap, &copy; CARTO',
  maxZoom: 19
}).addTo(this.map);

// Add custom zoom control (top-right)
L.control.zoom({ position: 'topright' }).addTo(this.map);

// Initialize marker cluster group
this.markers = L.markerClusterGroup({
  spiderfyOnMaxZoom: true,
  showCoverageOnHover: false,
  maxClusterRadius: 50,
  spiderLegPolylineOptions: { weight: 1.5, color: '#2dc8d2' }
});

this.map.addLayer(this.markers);

}

async loadData() { const response = await fetch('data/grantees.json'); this.data = await response.json(); }

renderMarkers() { this.markers.clearLayers();

const filtered = this.data.filter(item => {
  if (this.filters.year && item.year !== this.filters.year) return false;
  if (this.filters.county && item.county !== this.filters.county) return false;
  if (this.filters.status && item.status !== this.filters.status) return false;
  return true;
});

filtered.forEach(item => {
  if (!item.lat || !item.lng) return;

  const marker = L.marker([item.lat, item.lng], {
    icon: this.createIcon(item.status)
  });

  marker.bindPopup(this.createPopup(item));
  this.markers.addLayer(marker);
});

// Update count display
document.getElementById('count').textContent = filtered.length;

}

createIcon(status) { const colors = { 'Active': '#2dc8d2', 'Completed': '#666666', 'Pending': '#f34213' };

return L.divIcon({
  html: `<div style="background: ${colors[status] || '#2dc8d2'}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>`,
  className: 'custom-marker',
  iconSize: [16, 16],
  iconAnchor: [8, 8]
});

}

createPopup(item) { return <div class="popup-content"> <h3 class="font-bold text-lg">${item.name}</h3> <p class="text-sm text-gray-600">${item.county} County</p> <p class="text-sm mt-2">${item.description || ''}</p> <div class="mt-2"> <span class="px-2 py-1 text-xs rounded bg-gray-200">${item.status}</span> <span class="px-2 py-1 text-xs rounded bg-gray-200">${item.year}</span> </div> ${item.website ?Visit Website →: ''} </div>; }

setupFilters() { // Year filter const years = [...new Set(this.data.map(d => d.year))].sort(); const yearSelect = document.getElementById('year-filter'); years.forEach(year => { const option = document.createElement('option'); option.value = year; option.textContent = year; yearSelect.appendChild(option); });

yearSelect.addEventListener('change', (e) => {
  this.filters.year = e.target.value || null;
  this.renderMarkers();
});

// Similar for county, status filters...

} }

// Initialize on load document.addEventListener('DOMContentLoaded', () => { const app = new MapApp(); app.init(); });

Google Sheets as database Fetching published CSV // Google Sheets published as CSV const SHEET_URL = 'https://docs.google.com/spreadsheets/d/e/SPREADSHEET_ID/pub?gid=0&single=true&output=csv';

async function loadFromSheets() { const response = await fetch(SHEET_URL); const csv = await response.text();

// Parse with PapaParse (CDN) const { data, errors } = Papa.parse(csv, { header: true, skipEmptyLines: true, transformHeader: (h) => h.trim().toLowerCase().replace(/\s+/g, '_') });

if (errors.length > 0) { console.warn('CSV parsing errors:', errors); }

return data; }

Real-time state with localStorage class DataManager { constructor(sheetUrl, cacheKey) { this.sheetUrl = sheetUrl; this.cacheKey = cacheKey; this.data = []; this.localState = this.loadLocalState(); }

loadLocalState() { const stored = localStorage.getItem(${this.cacheKey}-state); return stored ? JSON.parse(stored) : {}; }

saveLocalState() { localStorage.setItem(${this.cacheKey}-state, JSON.stringify(this.localState)); }

async refresh() { const response = await fetch(this.sheetUrl); const csv = await response.text(); this.data = Papa.parse(csv, { header: true, skipEmptyLines: true }).data;

// Merge with local state
this.data.forEach(row => {
  const localData = this.localState[row.id];
  if (localData) {
    Object.assign(row, localData);
  }
});

return this.data;

}

updateLocal(id, updates) { this.localState[id] = { ...this.localState[id], ...updates }; this.saveLocalState();

// Update in-memory data too
const item = this.data.find(d => d.id === id);
if (item) Object.assign(item, updates);

} }

// Usage const manager = new DataManager(SHEET_URL, 'volunteer-data'); await manager.refresh();

// Mark task as complete (stored locally) manager.updateLocal('task-123', { completed: true, completed_at: new Date().toISOString() });

Browser extension (Manifest V3) manifest.json { "manifest_version": 3, "name": "PocketLink", "version": "1.0.0", "description": "Create shortlinks from right-click context menu",

"permissions": [ "contextMenus", "storage", "activeTab", "scripting", "notifications", "offscreen" ],

"background": { "service_worker": "background.js", "type": "module" },

"action": { "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } },

"options_page": "options.html",

"icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }

Service worker (background.js) // background.js - Service Worker

// Create context menu on install chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: 'create-shortlink', title: 'Create Shortlink', contexts: ['page', 'link'] }); });

// Handle context menu click chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId !== 'create-shortlink') return;

const url = info.linkUrl || info.pageUrl;

try { const shortUrl = await createShortlink(url); await copyToClipboard(shortUrl); showNotification('Shortlink Created', shortUrl); } catch (error) { showNotification('Error', error.message); } });

async function createShortlink(longUrl) { const { apiToken } = await chrome.storage.sync.get('apiToken'); if (!apiToken) throw new Error('API token not configured');

const response = await fetch('https://api-ssl.bitly.com/v4/shorten', { method: 'POST', headers: { 'Authorization': Bearer ${apiToken}, 'Content-Type': 'application/json' }, body: JSON.stringify({ long_url: longUrl }) });

if (!response.ok) throw new Error('API request failed');

const data = await response.json(); return data.link; }

// Clipboard methods (three fallback strategies)

// Method 1: Offscreen API (preferred) async function copyToClipboard(text) { try { await copyViaOffscreen(text); } catch { try { await copyViaContentScript(text); } catch { await copyViaPopup(text); } } }

async function copyViaOffscreen(text) { await chrome.offscreen.createDocument({ url: 'offscreen.html', reasons: ['CLIPBOARD'], justification: 'Copy shortlink to clipboard' });

await chrome.runtime.sendMessage({ type: 'copy', text }); await chrome.offscreen.closeDocument(); }

async function copyViaContentScript(text) { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (text) => navigator.clipboard.writeText(text), args: [text] }); }

function showNotification(title, message) { chrome.notifications.create({ type: 'basic', iconUrl: 'icons/icon48.png', title, message }); }

Options page

PocketLink Settings

// options.js document.addEventListener('DOMContentLoaded', async () => { const tokenInput = document.getElementById('apiToken'); const saveButton = document.getElementById('save'); const status = document.getElementById('status');

// Load saved token const { apiToken } = await chrome.storage.sync.get('apiToken'); if (apiToken) tokenInput.value = apiToken;

saveButton.addEventListener('click', async () => { const token = tokenInput.value.trim();

if (!token) {
  showStatus('Please enter an API token', 'error');
  return;
}

// Validate token by making test request
try {
  const response = await fetch('https://api-ssl.bitly.com/v4/user', {
    headers: { 'Authorization': `Bearer ${token}` }
  });

  if (!response.ok) throw new Error('Invalid token');

  await chrome.storage.sync.set({ apiToken: token });
  showStatus('Settings saved successfully!', 'success');
} catch {
  showStatus('Invalid API token', 'error');
}

});

function showStatus(message, type) { status.textContent = message; status.className = status ${type}; status.style.display = 'block'; setTimeout(() => { status.style.display = 'none'; }, 3000); } });

Cache busting for deployments

Deployment patterns Static hosting (FTP/SFTP)

Directory structure for WordPress wp-content deployment

wp-content/ └── archive-explorer/ ├── index.html ├── index.js ├── index.css ├── components/ │ ├── Sidebar.js │ ├── RecordList.js │ └── RecordCard.js └── data/ └── archive-data.json

Path management for subdirectory deployment // constants.js

// Auto-detect base path from current URL const getBasePath = () => { const path = window.location.pathname; const lastSlash = path.lastIndexOf('/'); return path.substring(0, lastSlash + 1); };

export const BASE_PATH = getBasePath(); export const DATA_URL = ${BASE_PATH}data/archive-data.json;

// Usage const response = await fetch(DATA_URL);

Performance tips Lazy load large JSON: Parse incrementally or paginate Use CSS containment: contain: layout style on repeated elements Debounce search input: Wait 300ms after typing stops Virtualize long lists: Only render visible items Preconnect to CDNs:

返回排行榜