Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f39cbfa
Add interactive pricing library demo with 12 sections
jpinho Mar 21, 2026
a029162
Add node-compile-cache to .gitignore
jpinho Mar 21, 2026
3458945
Add demo screenshots for all 12 sections
jpinho Mar 21, 2026
183b0ad
Ignore capture script in demo screenshots
jpinho Mar 21, 2026
a047604
Add explicit energy use case sections: Electricity, Gas, House Connec…
jpinho Mar 21, 2026
6cc0345
Add demo screenshots for energy use case sections
jpinho Mar 21, 2026
e85bed0
Fix non-commodity section to show products (solar, wallbox, heat pump…
jpinho Mar 21, 2026
46467c5
Add screenshots for all 16 demo sections
jpinho Mar 21, 2026
294ac07
Add syntax highlighting to CodeBlock component
jpinho Mar 21, 2026
1fde23c
add Usage code sections to all demo examples and rebrand as epilot Pr…
jpinho Mar 21, 2026
6318161
Move Energy & Utility Use Cases to top of sidebar
jpinho Mar 21, 2026
8327ce9
Group non-use-case sections under Capabilities in sidebar
jpinho Mar 21, 2026
e566412
Fix undefined values in tax breakdown table
jpinho Mar 21, 2026
dbe7152
Add author credit to sidebar footer
jpinho Mar 21, 2026
4c6ba8f
update locks
jpinho Mar 21, 2026
d01fd63
Redesign playground UI with sales-ready tariff card aesthetic
claude Mar 22, 2026
0450d01
Add demo/test-results to gitignore
claude Mar 22, 2026
dae87cd
Add UI redesign screenshots for preview
claude Mar 22, 2026
e9c475c
Fix demo UI issues: card heights, slider styling, icons, and bugs
claude Mar 22, 2026
219a290
Fix sliders: remove appearance-none that conflicts with accent-* colors
claude Mar 23, 2026
a31780a
Replace all unicode escape sequences with actual emoji characters
claude Mar 23, 2026
49f6f59
code format
jpinho Mar 23, 2026
550d4b6
Merge branch 'claude/pricing-demo-interactive-IRQnH' of github.com:ep…
jpinho Mar 23, 2026
84e1912
remove sshots
jpinho Mar 23, 2026
bcb37db
Merge branch 'main' into claude/pricing-demo-interactive-IRQnH
jpinho Mar 23, 2026
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
184 changes: 114 additions & 70 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { useState } from 'react';
import { OverviewDemo } from './sections/OverviewDemo';
import { PerUnitDemo } from './sections/PerUnitDemo';
import { TieredVolumeDemo } from './sections/TieredVolumeDemo';
import { TieredGraduatedDemo } from './sections/TieredGraduatedDemo';
import { TieredFlatFeeDemo } from './sections/TieredFlatFeeDemo';
import { TaxDemo } from './sections/TaxDemo';
import { DiscountDemo } from './sections/DiscountDemo';
import { useState, useEffect, useCallback } from 'react';
import { CompositePriceDemo } from './sections/CompositePriceDemo';
import { RecurringBillingDemo } from './sections/RecurringBillingDemo';
import { CurrencyDemo } from './sections/CurrencyDemo';
import { DiscountDemo } from './sections/DiscountDemo';
import { DynamicTariffDemo } from './sections/DynamicTariffDemo';
import { GetAGDemo } from './sections/GetAGDemo';
import { ElectricityDemo } from './sections/ElectricityDemo';
import { GasDemo } from './sections/GasDemo';
import { GetAGDemo } from './sections/GetAGDemo';
import { HouseConnectionDemo } from './sections/HouseConnectionDemo';
import { NonCommodityDemo } from './sections/NonCommodityDemo';
import { OverviewDemo } from './sections/OverviewDemo';
import { PerUnitDemo } from './sections/PerUnitDemo';
import { RecurringBillingDemo } from './sections/RecurringBillingDemo';
import { TaxDemo } from './sections/TaxDemo';
import { TieredFlatFeeDemo } from './sections/TieredFlatFeeDemo';
import { TieredGraduatedDemo } from './sections/TieredGraduatedDemo';
import { TieredVolumeDemo } from './sections/TieredVolumeDemo';

type SectionItem = {
id: string;
Expand All @@ -35,30 +35,30 @@ function isGroup(s: Section): s is SectionGroup {
}

const sections: Section[] = [
{ id: 'overview', label: 'Overview', icon: '\uD83C\uDFE0', component: OverviewDemo },
{ id: 'overview', label: 'Overview', icon: '🏠', component: OverviewDemo },
{
group: 'Energy & Utility Use Cases',
group: 'Energy Products',
items: [
{ id: 'electricity', label: 'Electricity', icon: '\u26A1', component: ElectricityDemo },
{ id: 'gas', label: 'Gas', icon: '\uD83D\uDD25', component: GasDemo },
{ id: 'house-connection', label: 'House Connection', icon: '\uD83C\uDFE1', component: HouseConnectionDemo },
{ id: 'non-commodity', label: 'Non-Commodity', icon: '\uD83D\uDCCB', component: NonCommodityDemo },
{ id: 'electricity', label: 'Electricity', icon: '', component: ElectricityDemo },
{ id: 'gas', label: 'Gas', icon: '🔥', component: GasDemo },
{ id: 'house-connection', label: 'House Connection', icon: '🏡', component: HouseConnectionDemo },
{ id: 'non-commodity', label: 'Products & Add-ons', icon: '☀️', component: NonCommodityDemo },
],
},
{
group: 'Capabilities',
group: 'Pricing Models',
items: [
{ id: 'per-unit', label: 'Per Unit', icon: '\uD83D\uDCE6', component: PerUnitDemo },
{ id: 'tiered-volume', label: 'Tiered Volume', icon: '\uD83D\uDCCA', component: TieredVolumeDemo },
{ id: 'tiered-graduated', label: 'Tiered Graduated', icon: '\uD83D\uDCC8', component: TieredGraduatedDemo },
{ id: 'tiered-flatfee', label: 'Tiered Flat Fee', icon: '\uD83C\uDFF7\uFE0F', component: TieredFlatFeeDemo },
{ id: 'tax', label: 'Tax Handling', icon: '\uD83E\uDDFE', component: TaxDemo },
{ id: 'discounts', label: 'Discounts & Coupons', icon: '\uD83C\uDF9F\uFE0F', component: DiscountDemo },
{ id: 'composite', label: 'Composite Pricing', icon: '\uD83E\uDDE9', component: CompositePriceDemo },
{ id: 'recurring', label: 'Recurring Billing', icon: '\uD83D\uDD04', component: RecurringBillingDemo },
{ id: 'currency', label: 'Currency & Formatting', icon: '\uD83D\uDCB1', component: CurrencyDemo },
{ id: 'dynamic-tariff', label: 'Dynamic Tariff', icon: '\u26A1', component: DynamicTariffDemo },
{ id: 'getag', label: 'GetAG Energy', icon: '\uD83D\uDD0C', component: GetAGDemo },
{ id: 'per-unit', label: 'Per Unit', icon: '📦', component: PerUnitDemo },
{ id: 'tiered-volume', label: 'Tiered Volume', icon: '📊', component: TieredVolumeDemo },
{ id: 'tiered-graduated', label: 'Tiered Graduated', icon: '📈', component: TieredGraduatedDemo },
{ id: 'tiered-flatfee', label: 'Tiered Flat Fee', icon: '🏷️', component: TieredFlatFeeDemo },
{ id: 'tax', label: 'Tax Handling', icon: '🧾', component: TaxDemo },
{ id: 'discounts', label: 'Discounts & Coupons', icon: '🎟️', component: DiscountDemo },
{ id: 'composite', label: 'Composite Pricing', icon: '🧩', component: CompositePriceDemo },
{ id: 'recurring', label: 'Recurring Billing', icon: '🔄', component: RecurringBillingDemo },
{ id: 'currency', label: 'Currency & Formatting', icon: '💱', component: CurrencyDemo },
{ id: 'dynamic-tariff', label: 'Dynamic Tariff', icon: '', component: DynamicTariffDemo },
{ id: 'getag', label: 'GetAG Energy', icon: '🔌', component: GetAGDemo },
],
},
];
Expand All @@ -77,90 +77,134 @@ function getAllSections(): SectionItem[] {

const allSections = getAllSections();

const isMobile = () => window.matchMedia('(max-width: 767px)').matches;

export default function App() {
const [activeSection, setActiveSection] = useState('overview');
const [sidebarOpen, setSidebarOpen] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(() => !isMobile());

useEffect(() => {
const mq = window.matchMedia('(max-width: 767px)');
const handler = (e: MediaQueryListEvent) => {
if (e.matches) setSidebarOpen(false);
};
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);

const navigate = useCallback((id: string) => {
setActiveSection(id);
if (isMobile()) setSidebarOpen(false);
}, []);

const ActiveComponent = allSections.find((s) => s.id === activeSection)?.component ?? OverviewDemo;
const activeItem = allSections.find((s) => s.id === activeSection);

return (
<div className="flex h-screen overflow-hidden">
<div className="flex h-screen overflow-hidden bg-gray-50">
{/* Sidebar */}
<aside
className={`${sidebarOpen ? 'w-64' : 'w-0 overflow-hidden'} flex-shrink-0 bg-white border-r border-gray-200 flex flex-col transition-all duration-200`}
className={`${sidebarOpen ? 'w-72' : 'w-0 overflow-hidden'} flex-shrink-0 bg-white border-r border-gray-100 flex flex-col transition-all duration-300`}
>
<div className="p-4 border-b border-gray-200">
<h1 className="text-lg font-bold text-gray-900">epilot Pricing</h1>
<p className="text-xs text-gray-500 mt-0.5">Pricing Playground</p>
{/* Brand */}
<div className="p-5 border-b border-gray-100">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl gradient-primary flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div>
<h1 className="text-base font-extrabold text-gray-900 tracking-tight">epilot Pricing</h1>
<p className="text-[10px] font-medium text-gray-400 uppercase tracking-widest">Interactive Playground</p>
</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto py-2">

{/* Nav */}
<nav className="flex-1 overflow-y-auto py-3 px-3">
{sections.map((section) => {
if (isGroup(section)) {
return (
<div key={section.group}>
<div className="px-4 pt-4 pb-1">
<p className="text-[10px] font-bold uppercase tracking-wider text-gray-400">
{section.group}
</p>
<div key={section.group} className="mt-5 first:mt-0">
<p className="px-3 pb-2 text-[10px] font-bold uppercase tracking-widest text-gray-300">
{section.group}
</p>
<div className="space-y-0.5">
{section.items.map((item) => (
<button
key={item.id}
onClick={() => navigate(item.id)}
className={`w-full text-left px-3 py-2.5 rounded-xl text-sm flex items-center gap-3 transition-all duration-200 ${
activeSection === item.id
? 'bg-primary-50 text-primary-700 font-semibold shadow-sm'
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'
}`}
>
<span className="text-base w-6 text-center">{item.icon}</span>
<span>{item.label}</span>
</button>
))}
</div>
{section.items.map((item) => (
<button
key={item.id}
onClick={() => setActiveSection(item.id)}
className={`w-full text-left px-4 pl-6 py-2.5 text-sm flex items-center gap-3 transition-colors ${
activeSection === item.id
? 'bg-primary-50 text-primary-700 font-medium border-r-2 border-primary-600'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<span className="text-base">{item.icon}</span>
{item.label}
</button>
))}
</div>
);
}

return (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`w-full text-left px-4 py-2.5 text-sm flex items-center gap-3 transition-colors ${
onClick={() => navigate(section.id)}
className={`w-full text-left px-3 py-2.5 rounded-xl text-sm flex items-center gap-3 transition-all duration-200 ${
activeSection === section.id
? 'bg-primary-50 text-primary-700 font-medium border-r-2 border-primary-600'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
? 'bg-primary-50 text-primary-700 font-semibold shadow-sm'
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'
}`}
>
<span className="text-base">{section.icon}</span>
{section.label}
<span className="text-base w-6 text-center">{section.icon}</span>
<span>{section.label}</span>
</button>
);
})}
</nav>
<div className="p-4 border-t border-gray-200 text-xs text-gray-400">
<p>@epilot/pricing v5.4.0</p>
<p className="mt-1">Made with love by <a href="https://github.com/jpinho" target="_blank" rel="noopener noreferrer" className="text-primary-600 hover:underline">@jpinho</a></p>

{/* Footer */}
<div className="p-4 border-t border-gray-100">
<div className="flex items-center gap-2 text-xs text-gray-400">
<div className="w-2 h-2 rounded-full bg-emerald-400"></div>
<span className="font-mono">v5.4.0</span>
</div>
<p className="text-[10px] text-gray-300 mt-1.5">
Made with care by{' '}
<a
href="https://github.com/jpinho"
target="_blank"
rel="noopener noreferrer"
className="text-primary-500 hover:text-primary-600 font-medium"
>
@jpinho
</a>
</p>
</div>
</aside>

{/* Main content */}
<main className="flex-1 overflow-y-auto">
<div className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b border-gray-200 px-6 py-3 flex items-center gap-3">
<div className="sticky top-0 z-10 bg-white/70 backdrop-blur-xl border-b border-gray-100 px-6 py-3 flex items-center gap-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-500"
className="p-2 rounded-xl hover:bg-gray-100 text-gray-400 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<h2 className="text-sm font-semibold text-gray-700">
{activeItem?.icon} {activeItem?.label}
</h2>
<div className="flex items-center gap-2">
<span className="text-lg">{activeItem?.icon}</span>
<h2 className="text-sm font-bold text-gray-700">{activeItem?.label}</h2>
</div>
</div>
<div className="p-6 max-w-6xl mx-auto">
<ActiveComponent onNavigate={setActiveSection} />
<div className="p-8 max-w-7xl mx-auto">
<ActiveComponent onNavigate={navigate} />
</div>
</main>
</div>
Expand Down
71 changes: 62 additions & 9 deletions demo/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,63 @@ interface Token {
}

const JS_KEYWORDS = new Set([
'import', 'from', 'export', 'default', 'const', 'let', 'var', 'function',
'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
'continue', 'new', 'this', 'class', 'extends', 'async', 'await', 'try',
'catch', 'throw', 'typeof', 'instanceof', 'in', 'of', 'true', 'false',
'null', 'undefined', 'void', 'type', 'interface', 'enum', 'as',
'import',
'from',
'export',
'default',
'const',
'let',
'var',
'function',
'return',
'if',
'else',
'for',
'while',
'do',
'switch',
'case',
'break',
'continue',
'new',
'this',
'class',
'extends',
'async',
'await',
'try',
'catch',
'throw',
'typeof',
'instanceof',
'in',
'of',
'true',
'false',
'null',
'undefined',
'void',
'type',
'interface',
'enum',
'as',
]);

const BUILTIN = new Set([
'console', 'Math', 'JSON', 'Array', 'Object', 'String', 'Number',
'Boolean', 'Promise', 'Map', 'Set', 'Date', 'Error', 'RegExp',
'console',
'Math',
'JSON',
'Array',
'Object',
'String',
'Number',
'Boolean',
'Promise',
'Map',
'Set',
'Date',
'Error',
'RegExp',
]);

function tokenize(code: string): Token[] {
Expand Down Expand Up @@ -137,15 +184,21 @@ export function CodeBlock({ code, title, language = 'typescript' }: CodeBlockPro
// Simple bash highlighting: just color comments and strings
return code.split('\n').map((line, i) => {
if (line.trimStart().startsWith('#')) {
return <div key={i}><span className="text-gray-500 italic">{line}</span></div>;
return (
<div key={i}>
<span className="text-gray-500 italic">{line}</span>
</div>
);
}
return <div key={i}>{line}</div>;
});
}

const tokens = tokenize(code);
return tokens.map((token, i) => (
<span key={i} className={tokenColors[token.type]}>{token.value}</span>
<span key={i} className={tokenColors[token.type]}>
{token.value}
</span>
));
}, [code, language]);

Expand Down
Loading
Loading