Skip to content

Commit 0f774c3

Browse files
committed
Add @thesis-co/cent-react package
Including basic React components and hooks for display, input, differences, and exchange rates. All components use string-based parsing and FixedPointNumbers to preserve precision. Tests run with strict cent configuration to catch precision issues.
1 parent 6a4d1c8 commit 0f774c3

25 files changed

Lines changed: 3866 additions & 0 deletions

packages/cent-react/COPYRIGHT

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Copyright (c) 2026 Thesis, Inc.
2+
3+
All rights reserved.

packages/cent-react/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Thesis, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/cent-react/README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# @thesis-co/cent-react
2+
3+
React bindings for [@thesis-co/cent](https://www.npmjs.com/package/@thesis-co/cent) - display, input, and manage money values with ease.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @thesis-co/cent @thesis-co/cent-react
9+
```
10+
11+
## Quick Start
12+
13+
### Display Money
14+
15+
```tsx
16+
import { MoneyDisplay } from '@thesis-co/cent-react';
17+
import { Money } from '@thesis-co/cent';
18+
19+
// Basic usage
20+
<MoneyDisplay value={Money("$1234.56")} />
21+
// → "$1,234.56"
22+
23+
// Compact notation
24+
<MoneyDisplay value={Money("$1500000")} compact />
25+
// → "$1.5M"
26+
27+
// Crypto with satoshis
28+
<MoneyDisplay value={Money("0.001 BTC")} preferredUnit="sat" />
29+
// → "100,000 sats"
30+
31+
// Locale formatting
32+
<MoneyDisplay value={Money("€1234.56")} locale="de-DE" />
33+
// → "1.234,56 €"
34+
35+
// Null handling with placeholder
36+
<MoneyDisplay value={null} placeholder="" />
37+
// → "—"
38+
```
39+
40+
### Custom Parts Rendering
41+
42+
```tsx
43+
<MoneyDisplay value={Money("$99.99")}>
44+
{({ parts, isNegative }) => (
45+
<span className={isNegative ? 'text-red-500' : ''}>
46+
{parts.map((part, i) => (
47+
<span key={i} className={part.type}>
48+
{part.value}
49+
</span>
50+
))}
51+
</span>
52+
)}
53+
</MoneyDisplay>
54+
```
55+
56+
### Money Input
57+
58+
```tsx
59+
import { MoneyInput } from '@thesis-co/cent-react';
60+
import { Money } from '@thesis-co/cent';
61+
62+
function PaymentForm() {
63+
const [amount, setAmount] = useState<Money | null>(null);
64+
65+
return (
66+
<MoneyInput
67+
name="amount"
68+
value={amount}
69+
onChange={(e) => setAmount(e.target.value)}
70+
currency="USD"
71+
min="$1"
72+
max="$10000"
73+
placeholder="Enter amount"
74+
/>
75+
);
76+
}
77+
```
78+
79+
### With react-hook-form
80+
81+
```tsx
82+
import { Controller, useForm } from 'react-hook-form';
83+
import { MoneyInput } from '@thesis-co/cent-react';
84+
85+
function CheckoutForm() {
86+
const { control, handleSubmit } = useForm();
87+
88+
return (
89+
<form onSubmit={handleSubmit(onSubmit)}>
90+
<Controller
91+
name="amount"
92+
control={control}
93+
render={({ field }) => (
94+
<MoneyInput {...field} currency="USD" />
95+
)}
96+
/>
97+
</form>
98+
);
99+
}
100+
```
101+
102+
### useMoney Hook
103+
104+
```tsx
105+
import { useMoney, MoneyDisplay } from '@thesis-co/cent-react';
106+
107+
function TipCalculator() {
108+
const bill = useMoney({ currency: 'USD' });
109+
const tip = bill.money?.multiply(0.18) ?? null;
110+
111+
return (
112+
<div>
113+
<input {...bill.inputProps} placeholder="Bill amount" />
114+
{bill.error && <span className="error">{bill.error.message}</span>}
115+
116+
<p>Tip (18%): <MoneyDisplay value={tip} /></p>
117+
<p>Total: <MoneyDisplay value={bill.money?.add(tip ?? Money.zero('USD'))} /></p>
118+
</div>
119+
);
120+
}
121+
```
122+
123+
### MoneyProvider
124+
125+
Set default configuration for all descendant components:
126+
127+
```tsx
128+
import { MoneyProvider } from '@thesis-co/cent-react';
129+
130+
function App() {
131+
return (
132+
<MoneyProvider locale="de-DE" defaultCurrency="EUR">
133+
<YourApp />
134+
</MoneyProvider>
135+
);
136+
}
137+
```
138+
139+
### useExchangeRate Hook
140+
141+
```tsx
142+
import { useExchangeRate, MoneyDisplay } from '@thesis-co/cent-react';
143+
import { Money } from '@thesis-co/cent';
144+
145+
function CurrencyConverter() {
146+
const [usd, setUsd] = useState(Money.zero('USD'));
147+
148+
const { convert, isLoading, isStale, refetch } = useExchangeRate({
149+
from: 'USD',
150+
to: 'EUR',
151+
pollInterval: 60000, // Refresh every minute
152+
staleThreshold: 300000, // Stale after 5 minutes
153+
});
154+
155+
const eur = convert(usd);
156+
157+
return (
158+
<div>
159+
<MoneyInput value={usd} onChange={(e) => setUsd(e.target.value)} currency="USD" />
160+
161+
{isLoading ? (
162+
<span>Loading...</span>
163+
) : (
164+
<MoneyDisplay value={eur} />
165+
)}
166+
167+
{isStale && (
168+
<button onClick={refetch}>Rate may be outdated. Refresh?</button>
169+
)}
170+
</div>
171+
);
172+
}
173+
```
174+
175+
**Note:** `useExchangeRate` requires an `exchangeRateResolver` to be provided via `MoneyProvider`:
176+
177+
```tsx
178+
<MoneyProvider
179+
exchangeRateResolver={async (from, to) => {
180+
const response = await fetch(`/api/rates/${from}/${to}`);
181+
const data = await response.json();
182+
return new ExchangeRate(from, to, data.rate);
183+
}}
184+
>
185+
<App />
186+
</MoneyProvider>
187+
```
188+
189+
### MoneyDiff
190+
191+
Display the difference between two money values:
192+
193+
```tsx
194+
import { MoneyDiff } from '@thesis-co/cent-react';
195+
import { Money } from '@thesis-co/cent';
196+
197+
// Basic difference
198+
<MoneyDiff value={Money("$120")} compareTo={Money("$100")} />
199+
// → "+$20.00"
200+
201+
// With percentage change
202+
<MoneyDiff
203+
value={Money("$120")}
204+
compareTo={Money("$100")}
205+
showPercentage
206+
/>
207+
// → "+$20.00 (+20.00%)"
208+
209+
// Custom rendering
210+
<MoneyDiff value={newPrice} compareTo={oldPrice}>
211+
{({ direction, formatted }) => (
212+
<span className={direction === 'increase' ? 'text-green-500' : 'text-red-500'}>
213+
{formatted.difference}
214+
</span>
215+
)}
216+
</MoneyDiff>
217+
```
218+
219+
## API Reference
220+
221+
### Components
222+
223+
| Component | Description |
224+
|-----------|-------------|
225+
| `MoneyDisplay` | Display formatted money values |
226+
| `MoneyInput` | Controlled input for money values |
227+
| `MoneyDiff` | Display difference between two values |
228+
| `MoneyProvider` | Context provider for default configuration |
229+
230+
### Hooks
231+
232+
| Hook | Description |
233+
|------|-------------|
234+
| `useMoney` | Manage money state with validation |
235+
| `useExchangeRate` | Fetch and manage exchange rates |
236+
| `useMoneyConfig` | Access MoneyProvider context |
237+
238+
## Requirements
239+
240+
- React 17.0.0 or later
241+
- @thesis-co/cent 0.0.5 or later

packages/cent-react/jest.config.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'jsdom',
5+
testMatch: ['**/test/**/*.test.ts', '**/test/**/*.test.tsx'],
6+
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
7+
transform: {
8+
'^.+\\.tsx?$': [
9+
'ts-jest',
10+
{
11+
tsconfig: './tsconfig.json',
12+
},
13+
],
14+
},
15+
}

packages/cent-react/package.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@thesis-co/cent-react",
3+
"version": "0.0.1",
4+
"description": "React bindings for @thesis-co/cent - display, input, and manage money values",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"files": [
8+
"dist"
9+
],
10+
"repository": {
11+
"type": "git",
12+
"url": "https://github.com/thesis/cent.git",
13+
"directory": "packages/cent-react"
14+
},
15+
"keywords": [
16+
"react",
17+
"money",
18+
"currency",
19+
"finance",
20+
"input",
21+
"form",
22+
"cents"
23+
],
24+
"author": "Matt Luongo (@mhluongo)",
25+
"license": "MIT",
26+
"publishConfig": {
27+
"access": "public"
28+
},
29+
"scripts": {
30+
"lint": "pnpx @biomejs/biome check",
31+
"lint:fix": "pnpx @biomejs/biome check --write",
32+
"build": "tsc",
33+
"test": "jest",
34+
"prepublishOnly": "pnpm run build && pnpm run test && pnpm run lint"
35+
},
36+
"devDependencies": {
37+
"@thesis-co/cent": "workspace:*",
38+
"@testing-library/jest-dom": "^6.4.0",
39+
"@testing-library/react": "^14.2.0",
40+
"@testing-library/user-event": "^14.5.0",
41+
"@types/jest": "^29.5.12",
42+
"@types/node": "^20.11.24",
43+
"@types/react": "^18.2.0",
44+
"@types/react-dom": "^18.2.0",
45+
"jest": "^29.7.0",
46+
"jest-environment-jsdom": "^29.7.0",
47+
"react": "^18.2.0",
48+
"react-dom": "^18.2.0",
49+
"ts-jest": "^29.1.2"
50+
},
51+
"peerDependencies": {
52+
"@thesis-co/cent": ">=0.0.5",
53+
"react": ">=17.0.0"
54+
}
55+
}

0 commit comments

Comments
 (0)