@vocoweb/meter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/index.d.mts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +88 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +15 -0
- package/dist/react.d.ts +15 -0
- package/dist/react.js +79 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +57 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 VocoWeb
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @vocoweb/meter
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@vocoweb/meter)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Production-ready usage tracking and limits for B2B SaaS.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`@vocoweb/meter` is the "Meter" module that makes usage-based pricing simple. Track usage, enforce limits, and display beautiful progress barsโall with a single function call.
|
|
11
|
+
|
|
12
|
+
**Key Features:**
|
|
13
|
+
- ๐ **Usage Tracking** - Track any metric (API calls, PDFs, storage, etc.)
|
|
14
|
+
- ๐ฏ **Limit Enforcement** - Automatic 402 errors when limits exceeded
|
|
15
|
+
- ๐ **Auto Reset** - Monthly/daily/weekly reset periods
|
|
16
|
+
- ๐ **Usage UI** - Pre-built progress bar component
|
|
17
|
+
- โก **High Performance** - Built on Supabase with row-level security
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @vocoweb/meter
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### Server-Side
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { meter } from '@vocoweb/meter';
|
|
31
|
+
|
|
32
|
+
// Increment usage
|
|
33
|
+
export async function POST(request: Request) {
|
|
34
|
+
const user = await auth.requireUser(request);
|
|
35
|
+
|
|
36
|
+
// Check limit before generating
|
|
37
|
+
await meter.checkLimit(user.id, 'pdf_generated');
|
|
38
|
+
|
|
39
|
+
// Generate PDF
|
|
40
|
+
const pdf = await generatePDF(data);
|
|
41
|
+
|
|
42
|
+
// Increment counter
|
|
43
|
+
await meter.increment(user.id, 'pdf_generated', 1);
|
|
44
|
+
|
|
45
|
+
return Response.json({ pdf });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get current usage
|
|
49
|
+
export async function GET(request: Request) {
|
|
50
|
+
const user = await auth.requireUser(request);
|
|
51
|
+
const usage = await meter.getUsage(user.id, 'pdf_generated');
|
|
52
|
+
|
|
53
|
+
return Response.json({
|
|
54
|
+
used: usage.current,
|
|
55
|
+
limit: usage.limit,
|
|
56
|
+
percentage: (usage.current / usage.limit) * 100,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Client-Side (React)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { VocoUsageBar } from '@vocoweb/meter/react';
|
|
65
|
+
|
|
66
|
+
export default function Dashboard() {
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<h1>Dashboard</h1>
|
|
70
|
+
|
|
71
|
+
<VocoUsageBar
|
|
72
|
+
metric="pdf_generated"
|
|
73
|
+
label="PDFs Generated This Month"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<VocoUsageBar
|
|
77
|
+
metric="api_calls"
|
|
78
|
+
label="API Calls"
|
|
79
|
+
color="blue"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### Server Methods
|
|
89
|
+
|
|
90
|
+
- `increment(userId, metric, amount)` - Increment usage counter
|
|
91
|
+
- `checkLimit(userId, metric)` - Check if within limit (throws 402 if exceeded)
|
|
92
|
+
- `getUsage(userId, metric)` - Get current usage statistics
|
|
93
|
+
- `resetUsage(userId, metric)` - Reset usage counter
|
|
94
|
+
- `setLimit(userId, metric, limit)` - Update user's limit
|
|
95
|
+
|
|
96
|
+
### React Components
|
|
97
|
+
|
|
98
|
+
- `<VocoUsageBar metric="name" label="Label" />` - Usage progress bar
|
|
99
|
+
|
|
100
|
+
## Configuration Example
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// vocoweb.config.ts
|
|
104
|
+
export const config = {
|
|
105
|
+
limits: {
|
|
106
|
+
pdf_generated: {
|
|
107
|
+
free: 10,
|
|
108
|
+
pro: 100,
|
|
109
|
+
enterprise: 1000
|
|
110
|
+
},
|
|
111
|
+
api_calls: {
|
|
112
|
+
free: 1000,
|
|
113
|
+
pro: 10000,
|
|
114
|
+
enterprise: 100000
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
resetPeriod: 'monthly', // 'daily', 'weekly', 'monthly'
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Environment Variables
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Supabase (Required)
|
|
125
|
+
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
|
|
126
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
127
|
+
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Database Schema
|
|
131
|
+
|
|
132
|
+
```sql
|
|
133
|
+
CREATE TABLE usage_metrics (
|
|
134
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
135
|
+
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
136
|
+
metric TEXT NOT NULL,
|
|
137
|
+
current_usage INTEGER DEFAULT 0,
|
|
138
|
+
limit_value INTEGER NOT NULL,
|
|
139
|
+
reset_at TIMESTAMPTZ NOT NULL,
|
|
140
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
141
|
+
UNIQUE(user_id, metric)
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT ยฉ VocoWeb
|
|
148
|
+
|
|
149
|
+
## Support
|
|
150
|
+
|
|
151
|
+
- Email: legal@vocoweb.in
|
|
152
|
+
- Documentation: [GitHub Wiki](https://github.com/vocoweb/vocoweb-meter/wiki)
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
**Built with care by [VocoWeb](https://vocoweb.in)**
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for @vocoweb/meter
|
|
3
|
+
*/
|
|
4
|
+
type MetricName = string;
|
|
5
|
+
type ResetPeriod = 'daily' | 'weekly' | 'monthly' | 'never';
|
|
6
|
+
interface UsageMetric {
|
|
7
|
+
id: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
metric: MetricName;
|
|
10
|
+
currentUsage: number;
|
|
11
|
+
limitValue: number;
|
|
12
|
+
resetAt: Date;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
updatedAt?: Date;
|
|
15
|
+
}
|
|
16
|
+
interface UsageStats {
|
|
17
|
+
current: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
percentage: number;
|
|
20
|
+
remaining: number;
|
|
21
|
+
}
|
|
22
|
+
interface IncrementOptions {
|
|
23
|
+
userId: string;
|
|
24
|
+
metric: MetricName;
|
|
25
|
+
amount?: number;
|
|
26
|
+
}
|
|
27
|
+
interface SetLimitOptions {
|
|
28
|
+
userId: string;
|
|
29
|
+
metric: MetricName;
|
|
30
|
+
limit: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Server-side functions for @vocoweb/meter
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Increment usage counter
|
|
39
|
+
*/
|
|
40
|
+
declare function increment(userId: string, metric: MetricName, amount?: number): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if user is within limit (throws 402 if exceeded)
|
|
43
|
+
*/
|
|
44
|
+
declare function checkLimit(userId: string, metric: MetricName): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Get current usage statistics
|
|
47
|
+
*/
|
|
48
|
+
declare function getUsage(userId: string, metric: MetricName): Promise<UsageStats>;
|
|
49
|
+
/**
|
|
50
|
+
* Reset usage counter
|
|
51
|
+
*/
|
|
52
|
+
declare function resetUsage(userId: string, metric: MetricName): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Set user's limit
|
|
55
|
+
*/
|
|
56
|
+
declare function setLimit(userId: string, metric: MetricName, limit: number): Promise<void>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @vocoweb/meter
|
|
60
|
+
* Production-ready usage tracking and limits
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main meter API
|
|
65
|
+
*/
|
|
66
|
+
declare const meter: {
|
|
67
|
+
increment: typeof increment;
|
|
68
|
+
checkLimit: typeof checkLimit;
|
|
69
|
+
getUsage: typeof getUsage;
|
|
70
|
+
resetUsage: typeof resetUsage;
|
|
71
|
+
setLimit: typeof setLimit;
|
|
72
|
+
};
|
|
73
|
+
declare const VERSION = "1.0.0";
|
|
74
|
+
|
|
75
|
+
export { type IncrementOptions, type MetricName, type ResetPeriod, type SetLimitOptions, type UsageMetric, type UsageStats, VERSION, checkLimit, getUsage, increment, meter, resetUsage, setLimit };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for @vocoweb/meter
|
|
3
|
+
*/
|
|
4
|
+
type MetricName = string;
|
|
5
|
+
type ResetPeriod = 'daily' | 'weekly' | 'monthly' | 'never';
|
|
6
|
+
interface UsageMetric {
|
|
7
|
+
id: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
metric: MetricName;
|
|
10
|
+
currentUsage: number;
|
|
11
|
+
limitValue: number;
|
|
12
|
+
resetAt: Date;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
updatedAt?: Date;
|
|
15
|
+
}
|
|
16
|
+
interface UsageStats {
|
|
17
|
+
current: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
percentage: number;
|
|
20
|
+
remaining: number;
|
|
21
|
+
}
|
|
22
|
+
interface IncrementOptions {
|
|
23
|
+
userId: string;
|
|
24
|
+
metric: MetricName;
|
|
25
|
+
amount?: number;
|
|
26
|
+
}
|
|
27
|
+
interface SetLimitOptions {
|
|
28
|
+
userId: string;
|
|
29
|
+
metric: MetricName;
|
|
30
|
+
limit: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Server-side functions for @vocoweb/meter
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Increment usage counter
|
|
39
|
+
*/
|
|
40
|
+
declare function increment(userId: string, metric: MetricName, amount?: number): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if user is within limit (throws 402 if exceeded)
|
|
43
|
+
*/
|
|
44
|
+
declare function checkLimit(userId: string, metric: MetricName): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Get current usage statistics
|
|
47
|
+
*/
|
|
48
|
+
declare function getUsage(userId: string, metric: MetricName): Promise<UsageStats>;
|
|
49
|
+
/**
|
|
50
|
+
* Reset usage counter
|
|
51
|
+
*/
|
|
52
|
+
declare function resetUsage(userId: string, metric: MetricName): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Set user's limit
|
|
55
|
+
*/
|
|
56
|
+
declare function setLimit(userId: string, metric: MetricName, limit: number): Promise<void>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @vocoweb/meter
|
|
60
|
+
* Production-ready usage tracking and limits
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main meter API
|
|
65
|
+
*/
|
|
66
|
+
declare const meter: {
|
|
67
|
+
increment: typeof increment;
|
|
68
|
+
checkLimit: typeof checkLimit;
|
|
69
|
+
getUsage: typeof getUsage;
|
|
70
|
+
resetUsage: typeof resetUsage;
|
|
71
|
+
setLimit: typeof setLimit;
|
|
72
|
+
};
|
|
73
|
+
declare const VERSION = "1.0.0";
|
|
74
|
+
|
|
75
|
+
export { type IncrementOptions, type MetricName, type ResetPeriod, type SetLimitOptions, type UsageMetric, type UsageStats, VERSION, checkLimit, getUsage, increment, meter, resetUsage, setLimit };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var supabaseJs = require('@supabase/supabase-js');
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
var getSupabaseClient = () => {
|
|
7
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
8
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
9
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
10
|
+
throw new Error("Supabase credentials not configured");
|
|
11
|
+
}
|
|
12
|
+
return supabaseJs.createClient(supabaseUrl, supabaseKey);
|
|
13
|
+
};
|
|
14
|
+
async function increment(userId, metric, amount = 1) {
|
|
15
|
+
const supabase = getSupabaseClient();
|
|
16
|
+
const { data: existing } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
|
|
17
|
+
if (existing) {
|
|
18
|
+
const { error } = await supabase.from("usage_metrics").update({ current_usage: existing.current_usage + amount }).eq("id", existing.id);
|
|
19
|
+
if (error) throw new Error(`Failed to increment usage: ${error.message}`);
|
|
20
|
+
} else {
|
|
21
|
+
const { error } = await supabase.from("usage_metrics").insert({
|
|
22
|
+
user_id: userId,
|
|
23
|
+
metric,
|
|
24
|
+
current_usage: amount,
|
|
25
|
+
limit_value: 100,
|
|
26
|
+
// Default limit
|
|
27
|
+
reset_at: getNextResetDate("monthly")
|
|
28
|
+
});
|
|
29
|
+
if (error) throw new Error(`Failed to create metric: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function checkLimit(userId, metric) {
|
|
33
|
+
const stats = await getUsage(userId, metric);
|
|
34
|
+
if (stats.current >= stats.limit) {
|
|
35
|
+
throw new Error(`Usage limit exceeded for ${metric}`, {
|
|
36
|
+
cause: { code: 402, metric, stats }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function getUsage(userId, metric) {
|
|
41
|
+
const supabase = getSupabaseClient();
|
|
42
|
+
const { data, error } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
|
|
43
|
+
if (error || !data) {
|
|
44
|
+
return { current: 0, limit: 100, percentage: 0, remaining: 100 };
|
|
45
|
+
}
|
|
46
|
+
const current = data.current_usage;
|
|
47
|
+
const limit = data.limit_value;
|
|
48
|
+
const percentage = current / limit * 100;
|
|
49
|
+
const remaining = Math.max(0, limit - current);
|
|
50
|
+
return { current, limit, percentage, remaining };
|
|
51
|
+
}
|
|
52
|
+
async function resetUsage(userId, metric) {
|
|
53
|
+
const supabase = getSupabaseClient();
|
|
54
|
+
const { error } = await supabase.from("usage_metrics").update({ current_usage: 0, reset_at: getNextResetDate("monthly") }).eq("user_id", userId).eq("metric", metric);
|
|
55
|
+
if (error) throw new Error(`Failed to reset usage: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
async function setLimit(userId, metric, limit) {
|
|
58
|
+
const supabase = getSupabaseClient();
|
|
59
|
+
const { error } = await supabase.from("usage_metrics").update({ limit_value: limit }).eq("user_id", userId).eq("metric", metric);
|
|
60
|
+
if (error) throw new Error(`Failed to set limit: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
function getNextResetDate(period) {
|
|
63
|
+
const date = /* @__PURE__ */ new Date();
|
|
64
|
+
switch (period) {
|
|
65
|
+
case "daily":
|
|
66
|
+
date.setDate(date.getDate() + 1);
|
|
67
|
+
break;
|
|
68
|
+
case "weekly":
|
|
69
|
+
date.setDate(date.getDate() + 7);
|
|
70
|
+
break;
|
|
71
|
+
case "monthly":
|
|
72
|
+
date.setMonth(date.getMonth() + 1);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
return date.toISOString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/index.ts
|
|
79
|
+
var meter = {
|
|
80
|
+
increment,
|
|
81
|
+
checkLimit,
|
|
82
|
+
getUsage,
|
|
83
|
+
resetUsage,
|
|
84
|
+
setLimit
|
|
85
|
+
};
|
|
86
|
+
var VERSION = "1.0.0";
|
|
87
|
+
|
|
88
|
+
exports.VERSION = VERSION;
|
|
89
|
+
exports.checkLimit = checkLimit;
|
|
90
|
+
exports.getUsage = getUsage;
|
|
91
|
+
exports.increment = increment;
|
|
92
|
+
exports.meter = meter;
|
|
93
|
+
exports.resetUsage = resetUsage;
|
|
94
|
+
exports.setLimit = setLimit;
|
|
95
|
+
//# sourceMappingURL=index.js.map
|
|
96
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/index.ts"],"names":["createClient"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Server-side functions for @vocoweb/meter\n */\n\nimport { createClient } from '@supabase/supabase-js';\nimport type { MetricName, UsageStats } from './types';\n\nconst getSupabaseClient = () => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n throw new Error('Supabase credentials not configured');\n }\n\n return createClient(supabaseUrl, supabaseKey);\n};\n\n/**\n * Increment usage counter\n */\nexport async function increment(userId: string, metric: MetricName, amount: number = 1): Promise<void> {\n const supabase = getSupabaseClient();\n\n // Get or create metric\n const { data: existing } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (existing) {\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: existing.current_usage + amount })\n .eq('id', existing.id);\n\n if (error) throw new Error(`Failed to increment usage: ${error.message}`);\n } else {\n // Create new metric with default limit\n const { error } = await supabase\n .from('usage_metrics')\n .insert({\n user_id: userId,\n metric,\n current_usage: amount,\n limit_value: 100, // Default limit\n reset_at: getNextResetDate('monthly'),\n });\n\n if (error) throw new Error(`Failed to create metric: ${error.message}`);\n }\n}\n\n/**\n * Check if user is within limit (throws 402 if exceeded)\n */\nexport async function checkLimit(userId: string, metric: MetricName): Promise<void> {\n const stats = await getUsage(userId, metric);\n\n if (stats.current >= stats.limit) {\n throw new Error(`Usage limit exceeded for ${metric}`, {\n cause: { code: 402, metric, stats },\n });\n }\n}\n\n/**\n * Get current usage statistics\n */\nexport async function getUsage(userId: string, metric: MetricName): Promise<UsageStats> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (error || !data) {\n return { current: 0, limit: 100, percentage: 0, remaining: 100 };\n }\n\n const current = data.current_usage;\n const limit = data.limit_value;\n const percentage = (current / limit) * 100;\n const remaining = Math.max(0, limit - current);\n\n return { current, limit, percentage, remaining };\n}\n\n/**\n * Reset usage counter\n */\nexport async function resetUsage(userId: string, metric: MetricName): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: 0, reset_at: getNextResetDate('monthly') })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to reset usage: ${error.message}`);\n}\n\n/**\n * Set user's limit\n */\nexport async function setLimit(userId: string, metric: MetricName, limit: number): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ limit_value: limit })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to set limit: ${error.message}`);\n}\n\n/**\n * Get next reset date based on period\n */\nfunction getNextResetDate(period: 'daily' | 'weekly' | 'monthly'): string {\n const date = new Date();\n\n switch (period) {\n case 'daily':\n date.setDate(date.getDate() + 1);\n break;\n case 'weekly':\n date.setDate(date.getDate() + 7);\n break;\n case 'monthly':\n date.setMonth(date.getMonth() + 1);\n break;\n }\n\n return date.toISOString();\n}\n","/**\n * @vocoweb/meter\n * Production-ready usage tracking and limits\n */\n\n// Types\nexport * from './types';\n\n// Server-side\nexport * from './server';\n\n// Import for unified API\nimport * as serverMeter from './server';\n\n/**\n * Main meter API\n */\nexport const meter = {\n increment: serverMeter.increment,\n checkLimit: serverMeter.checkLimit,\n getUsage: serverMeter.getUsage,\n resetUsage: serverMeter.resetUsage,\n setLimit: serverMeter.setLimit,\n};\n\n// Version\nexport const VERSION = '1.0.0';\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
var getSupabaseClient = () => {
|
|
5
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
6
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
7
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
8
|
+
throw new Error("Supabase credentials not configured");
|
|
9
|
+
}
|
|
10
|
+
return createClient(supabaseUrl, supabaseKey);
|
|
11
|
+
};
|
|
12
|
+
async function increment(userId, metric, amount = 1) {
|
|
13
|
+
const supabase = getSupabaseClient();
|
|
14
|
+
const { data: existing } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
|
|
15
|
+
if (existing) {
|
|
16
|
+
const { error } = await supabase.from("usage_metrics").update({ current_usage: existing.current_usage + amount }).eq("id", existing.id);
|
|
17
|
+
if (error) throw new Error(`Failed to increment usage: ${error.message}`);
|
|
18
|
+
} else {
|
|
19
|
+
const { error } = await supabase.from("usage_metrics").insert({
|
|
20
|
+
user_id: userId,
|
|
21
|
+
metric,
|
|
22
|
+
current_usage: amount,
|
|
23
|
+
limit_value: 100,
|
|
24
|
+
// Default limit
|
|
25
|
+
reset_at: getNextResetDate("monthly")
|
|
26
|
+
});
|
|
27
|
+
if (error) throw new Error(`Failed to create metric: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function checkLimit(userId, metric) {
|
|
31
|
+
const stats = await getUsage(userId, metric);
|
|
32
|
+
if (stats.current >= stats.limit) {
|
|
33
|
+
throw new Error(`Usage limit exceeded for ${metric}`, {
|
|
34
|
+
cause: { code: 402, metric, stats }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function getUsage(userId, metric) {
|
|
39
|
+
const supabase = getSupabaseClient();
|
|
40
|
+
const { data, error } = await supabase.from("usage_metrics").select("*").eq("user_id", userId).eq("metric", metric).single();
|
|
41
|
+
if (error || !data) {
|
|
42
|
+
return { current: 0, limit: 100, percentage: 0, remaining: 100 };
|
|
43
|
+
}
|
|
44
|
+
const current = data.current_usage;
|
|
45
|
+
const limit = data.limit_value;
|
|
46
|
+
const percentage = current / limit * 100;
|
|
47
|
+
const remaining = Math.max(0, limit - current);
|
|
48
|
+
return { current, limit, percentage, remaining };
|
|
49
|
+
}
|
|
50
|
+
async function resetUsage(userId, metric) {
|
|
51
|
+
const supabase = getSupabaseClient();
|
|
52
|
+
const { error } = await supabase.from("usage_metrics").update({ current_usage: 0, reset_at: getNextResetDate("monthly") }).eq("user_id", userId).eq("metric", metric);
|
|
53
|
+
if (error) throw new Error(`Failed to reset usage: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
async function setLimit(userId, metric, limit) {
|
|
56
|
+
const supabase = getSupabaseClient();
|
|
57
|
+
const { error } = await supabase.from("usage_metrics").update({ limit_value: limit }).eq("user_id", userId).eq("metric", metric);
|
|
58
|
+
if (error) throw new Error(`Failed to set limit: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
function getNextResetDate(period) {
|
|
61
|
+
const date = /* @__PURE__ */ new Date();
|
|
62
|
+
switch (period) {
|
|
63
|
+
case "daily":
|
|
64
|
+
date.setDate(date.getDate() + 1);
|
|
65
|
+
break;
|
|
66
|
+
case "weekly":
|
|
67
|
+
date.setDate(date.getDate() + 7);
|
|
68
|
+
break;
|
|
69
|
+
case "monthly":
|
|
70
|
+
date.setMonth(date.getMonth() + 1);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return date.toISOString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/index.ts
|
|
77
|
+
var meter = {
|
|
78
|
+
increment,
|
|
79
|
+
checkLimit,
|
|
80
|
+
getUsage,
|
|
81
|
+
resetUsage,
|
|
82
|
+
setLimit
|
|
83
|
+
};
|
|
84
|
+
var VERSION = "1.0.0";
|
|
85
|
+
|
|
86
|
+
export { VERSION, checkLimit, getUsage, increment, meter, resetUsage, setLimit };
|
|
87
|
+
//# sourceMappingURL=index.mjs.map
|
|
88
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Server-side functions for @vocoweb/meter\n */\n\nimport { createClient } from '@supabase/supabase-js';\nimport type { MetricName, UsageStats } from './types';\n\nconst getSupabaseClient = () => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n throw new Error('Supabase credentials not configured');\n }\n\n return createClient(supabaseUrl, supabaseKey);\n};\n\n/**\n * Increment usage counter\n */\nexport async function increment(userId: string, metric: MetricName, amount: number = 1): Promise<void> {\n const supabase = getSupabaseClient();\n\n // Get or create metric\n const { data: existing } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (existing) {\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: existing.current_usage + amount })\n .eq('id', existing.id);\n\n if (error) throw new Error(`Failed to increment usage: ${error.message}`);\n } else {\n // Create new metric with default limit\n const { error } = await supabase\n .from('usage_metrics')\n .insert({\n user_id: userId,\n metric,\n current_usage: amount,\n limit_value: 100, // Default limit\n reset_at: getNextResetDate('monthly'),\n });\n\n if (error) throw new Error(`Failed to create metric: ${error.message}`);\n }\n}\n\n/**\n * Check if user is within limit (throws 402 if exceeded)\n */\nexport async function checkLimit(userId: string, metric: MetricName): Promise<void> {\n const stats = await getUsage(userId, metric);\n\n if (stats.current >= stats.limit) {\n throw new Error(`Usage limit exceeded for ${metric}`, {\n cause: { code: 402, metric, stats },\n });\n }\n}\n\n/**\n * Get current usage statistics\n */\nexport async function getUsage(userId: string, metric: MetricName): Promise<UsageStats> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('usage_metrics')\n .select('*')\n .eq('user_id', userId)\n .eq('metric', metric)\n .single();\n\n if (error || !data) {\n return { current: 0, limit: 100, percentage: 0, remaining: 100 };\n }\n\n const current = data.current_usage;\n const limit = data.limit_value;\n const percentage = (current / limit) * 100;\n const remaining = Math.max(0, limit - current);\n\n return { current, limit, percentage, remaining };\n}\n\n/**\n * Reset usage counter\n */\nexport async function resetUsage(userId: string, metric: MetricName): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ current_usage: 0, reset_at: getNextResetDate('monthly') })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to reset usage: ${error.message}`);\n}\n\n/**\n * Set user's limit\n */\nexport async function setLimit(userId: string, metric: MetricName, limit: number): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('usage_metrics')\n .update({ limit_value: limit })\n .eq('user_id', userId)\n .eq('metric', metric);\n\n if (error) throw new Error(`Failed to set limit: ${error.message}`);\n}\n\n/**\n * Get next reset date based on period\n */\nfunction getNextResetDate(period: 'daily' | 'weekly' | 'monthly'): string {\n const date = new Date();\n\n switch (period) {\n case 'daily':\n date.setDate(date.getDate() + 1);\n break;\n case 'weekly':\n date.setDate(date.getDate() + 7);\n break;\n case 'monthly':\n date.setMonth(date.getMonth() + 1);\n break;\n }\n\n return date.toISOString();\n}\n","/**\n * @vocoweb/meter\n * Production-ready usage tracking and limits\n */\n\n// Types\nexport * from './types';\n\n// Server-side\nexport * from './server';\n\n// Import for unified API\nimport * as serverMeter from './server';\n\n/**\n * Main meter API\n */\nexport const meter = {\n increment: serverMeter.increment,\n checkLimit: serverMeter.checkLimit,\n getUsage: serverMeter.getUsage,\n resetUsage: serverMeter.resetUsage,\n setLimit: serverMeter.setLimit,\n};\n\n// Version\nexport const VERSION = '1.0.0';\n"]}
|
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VocoUsageBar Component
|
|
5
|
+
* Usage progress bar
|
|
6
|
+
*/
|
|
7
|
+
interface VocoUsageBarProps {
|
|
8
|
+
metric: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
color?: 'blue' | 'green' | 'red' | 'purple';
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function VocoUsageBar({ metric, label, color, className, }: VocoUsageBarProps): react_jsx_runtime.JSX.Element;
|
|
14
|
+
|
|
15
|
+
export { VocoUsageBar };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VocoUsageBar Component
|
|
5
|
+
* Usage progress bar
|
|
6
|
+
*/
|
|
7
|
+
interface VocoUsageBarProps {
|
|
8
|
+
metric: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
color?: 'blue' | 'green' | 'red' | 'purple';
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function VocoUsageBar({ metric, label, color, className, }: VocoUsageBarProps): react_jsx_runtime.JSX.Element;
|
|
14
|
+
|
|
15
|
+
export { VocoUsageBar };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
function _interopNamespace(e) {
|
|
7
|
+
if (e && e.__esModule) return e;
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
25
|
+
|
|
26
|
+
// src/components/VocoUsageBar.tsx
|
|
27
|
+
function VocoUsageBar({
|
|
28
|
+
metric,
|
|
29
|
+
label,
|
|
30
|
+
color = "blue",
|
|
31
|
+
className = ""
|
|
32
|
+
}) {
|
|
33
|
+
const [usage, setUsage] = React__namespace.useState({ current: 0, limit: 0, percentage: 0 });
|
|
34
|
+
const [loading, setLoading] = React__namespace.useState(true);
|
|
35
|
+
React__namespace.useEffect(() => {
|
|
36
|
+
async function fetchUsage() {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`/api/usage?metric=${metric}`);
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
setUsage(data);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Failed to fetch usage:", error);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
fetchUsage();
|
|
48
|
+
}, [metric]);
|
|
49
|
+
if (loading) {
|
|
50
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-full animate-pulse rounded bg-gray-200" });
|
|
51
|
+
}
|
|
52
|
+
const colors = {
|
|
53
|
+
blue: "bg-blue-600",
|
|
54
|
+
green: "bg-green-600",
|
|
55
|
+
red: "bg-red-600",
|
|
56
|
+
purple: "bg-purple-600"
|
|
57
|
+
};
|
|
58
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 ${className}`, children: [
|
|
59
|
+
label && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm", children: [
|
|
60
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: label }),
|
|
61
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600", children: [
|
|
62
|
+
usage.current,
|
|
63
|
+
" / ",
|
|
64
|
+
usage.limit
|
|
65
|
+
] })
|
|
66
|
+
] }),
|
|
67
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-full overflow-hidden rounded-full bg-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
68
|
+
"div",
|
|
69
|
+
{
|
|
70
|
+
className: `h-full transition-all ${colors[color]}`,
|
|
71
|
+
style: { width: `${Math.min(usage.percentage, 100)}%` }
|
|
72
|
+
}
|
|
73
|
+
) })
|
|
74
|
+
] });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
exports.VocoUsageBar = VocoUsageBar;
|
|
78
|
+
//# sourceMappingURL=react.js.map
|
|
79
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/VocoUsageBar.tsx"],"names":["React","jsx","jsxs"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgBO,SAAS,YAAA,CAAa;AAAA,EACzB,MAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA,GAAQ,MAAA;AAAA,EACR,SAAA,GAAY;AAChB,CAAA,EAAsB;AAClB,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAUA,gBAAA,CAAA,QAAA,CAAS,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,UAAA,EAAY,CAAA,EAAG,CAAA;AAChF,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAUA,0BAAS,IAAI,CAAA;AAEjD,EAAMA,2BAAU,MAAM;AAClB,IAAA,eAAe,UAAA,GAAa;AACxB,MAAA,IAAI;AACA,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,kBAAA,EAAqB,MAAM,CAAA,CAAE,CAAA;AACrD,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACjB,SAAS,KAAA,EAAO;AACZ,QAAA,OAAA,CAAQ,KAAA,CAAM,0BAA0B,KAAK,CAAA;AAAA,MACjD,CAAA,SAAE;AACE,QAAA,UAAA,CAAW,KAAK,CAAA;AAAA,MACpB;AAAA,IACJ;AAEA,IAAA,UAAA,EAAW;AAAA,EACf,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,IAAI,OAAA,EAAS;AACT,IAAA,uBAAOC,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8CAAA,EAA+C,CAAA;AAAA,EACzE;AAEA,EAAA,MAAM,MAAA,GAAS;AAAA,IACX,IAAA,EAAM,aAAA;AAAA,IACN,KAAA,EAAO,cAAA;AAAA,IACP,GAAA,EAAK,YAAA;AAAA,IACL,MAAA,EAAQ;AAAA,GACZ;AAEA,EAAA,uBACIC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,CAAA,UAAA,EAAa,SAAS,CAAA,CAAA,EACjC,QAAA,EAAA;AAAA,IAAA,KAAA,oBACGA,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2CAAA,EACX,QAAA,EAAA;AAAA,sBAAAD,cAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,aAAA,EAAe,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,sBACrCC,eAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,eAAA,EACX,QAAA,EAAA;AAAA,QAAA,KAAA,CAAM,OAAA;AAAA,QAAQ,KAAA;AAAA,QAAI,KAAA,CAAM;AAAA,OAAA,EAC7B;AAAA,KAAA,EACJ,CAAA;AAAA,oBAEJD,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qDAAA,EACX,QAAA,kBAAAA,cAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACG,SAAA,EAAW,CAAA,sBAAA,EAAyB,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,QACjD,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,UAAA,EAAY,GAAG,CAAC,CAAA,CAAA,CAAA;AAAI;AAAA,KAC1D,EACJ;AAAA,GAAA,EACJ,CAAA;AAER","file":"react.js","sourcesContent":["/**\n * VocoUsageBar Component\n * Usage progress bar\n */\n\n'use client';\n\nimport * as React from 'react';\n\nexport interface VocoUsageBarProps {\n metric: string;\n label?: string;\n color?: 'blue' | 'green' | 'red' | 'purple';\n className?: string;\n}\n\nexport function VocoUsageBar({\n metric,\n label,\n color = 'blue',\n className = '',\n}: VocoUsageBarProps) {\n const [usage, setUsage] = React.useState({ current: 0, limit: 0, percentage: 0 });\n const [loading, setLoading] = React.useState(true);\n\n React.useEffect(() => {\n async function fetchUsage() {\n try {\n const res = await fetch(`/api/usage?metric=${metric}`);\n const data = await res.json();\n setUsage(data);\n } catch (error) {\n console.error('Failed to fetch usage:', error);\n } finally {\n setLoading(false);\n }\n }\n\n fetchUsage();\n }, [metric]);\n\n if (loading) {\n return <div className=\"h-4 w-full animate-pulse rounded bg-gray-200\" />;\n }\n\n const colors = {\n blue: 'bg-blue-600',\n green: 'bg-green-600',\n red: 'bg-red-600',\n purple: 'bg-purple-600',\n };\n\n return (\n <div className={`space-y-2 ${className}`}>\n {label && (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"font-medium\">{label}</span>\n <span className=\"text-gray-600\">\n {usage.current} / {usage.limit}\n </span>\n </div>\n )}\n <div className=\"h-2 w-full overflow-hidden rounded-full bg-gray-200\">\n <div\n className={`h-full transition-all ${colors[color]}`}\n style={{ width: `${Math.min(usage.percentage, 100)}%` }}\n />\n </div>\n </div>\n );\n}\n"]}
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/components/VocoUsageBar.tsx
|
|
5
|
+
function VocoUsageBar({
|
|
6
|
+
metric,
|
|
7
|
+
label,
|
|
8
|
+
color = "blue",
|
|
9
|
+
className = ""
|
|
10
|
+
}) {
|
|
11
|
+
const [usage, setUsage] = React.useState({ current: 0, limit: 0, percentage: 0 });
|
|
12
|
+
const [loading, setLoading] = React.useState(true);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
async function fetchUsage() {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`/api/usage?metric=${metric}`);
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
setUsage(data);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("Failed to fetch usage:", error);
|
|
21
|
+
} finally {
|
|
22
|
+
setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
fetchUsage();
|
|
26
|
+
}, [metric]);
|
|
27
|
+
if (loading) {
|
|
28
|
+
return /* @__PURE__ */ jsx("div", { className: "h-4 w-full animate-pulse rounded bg-gray-200" });
|
|
29
|
+
}
|
|
30
|
+
const colors = {
|
|
31
|
+
blue: "bg-blue-600",
|
|
32
|
+
green: "bg-green-600",
|
|
33
|
+
red: "bg-red-600",
|
|
34
|
+
purple: "bg-purple-600"
|
|
35
|
+
};
|
|
36
|
+
return /* @__PURE__ */ jsxs("div", { className: `space-y-2 ${className}`, children: [
|
|
37
|
+
label && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm", children: [
|
|
38
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: label }),
|
|
39
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-600", children: [
|
|
40
|
+
usage.current,
|
|
41
|
+
" / ",
|
|
42
|
+
usage.limit
|
|
43
|
+
] })
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 w-full overflow-hidden rounded-full bg-gray-200", children: /* @__PURE__ */ jsx(
|
|
46
|
+
"div",
|
|
47
|
+
{
|
|
48
|
+
className: `h-full transition-all ${colors[color]}`,
|
|
49
|
+
style: { width: `${Math.min(usage.percentage, 100)}%` }
|
|
50
|
+
}
|
|
51
|
+
) })
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { VocoUsageBar };
|
|
56
|
+
//# sourceMappingURL=react.mjs.map
|
|
57
|
+
//# sourceMappingURL=react.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/VocoUsageBar.tsx"],"names":[],"mappings":";;;;AAgBO,SAAS,YAAA,CAAa;AAAA,EACzB,MAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA,GAAQ,MAAA;AAAA,EACR,SAAA,GAAY;AAChB,CAAA,EAAsB;AAClB,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAU,KAAA,CAAA,QAAA,CAAS,EAAE,OAAA,EAAS,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,UAAA,EAAY,CAAA,EAAG,CAAA;AAChF,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAU,eAAS,IAAI,CAAA;AAEjD,EAAM,gBAAU,MAAM;AAClB,IAAA,eAAe,UAAA,GAAa;AACxB,MAAA,IAAI;AACA,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,kBAAA,EAAqB,MAAM,CAAA,CAAE,CAAA;AACrD,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACjB,SAAS,KAAA,EAAO;AACZ,QAAA,OAAA,CAAQ,KAAA,CAAM,0BAA0B,KAAK,CAAA;AAAA,MACjD,CAAA,SAAE;AACE,QAAA,UAAA,CAAW,KAAK,CAAA;AAAA,MACpB;AAAA,IACJ;AAEA,IAAA,UAAA,EAAW;AAAA,EACf,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,IAAI,OAAA,EAAS;AACT,IAAA,uBAAO,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8CAAA,EAA+C,CAAA;AAAA,EACzE;AAEA,EAAA,MAAM,MAAA,GAAS;AAAA,IACX,IAAA,EAAM,aAAA;AAAA,IACN,KAAA,EAAO,cAAA;AAAA,IACP,GAAA,EAAK,YAAA;AAAA,IACL,MAAA,EAAQ;AAAA,GACZ;AAEA,EAAA,uBACI,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,CAAA,UAAA,EAAa,SAAS,CAAA,CAAA,EACjC,QAAA,EAAA;AAAA,IAAA,KAAA,oBACG,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2CAAA,EACX,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,aAAA,EAAe,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,sBACrC,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,eAAA,EACX,QAAA,EAAA;AAAA,QAAA,KAAA,CAAM,OAAA;AAAA,QAAQ,KAAA;AAAA,QAAI,KAAA,CAAM;AAAA,OAAA,EAC7B;AAAA,KAAA,EACJ,CAAA;AAAA,oBAEJ,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qDAAA,EACX,QAAA,kBAAA,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACG,SAAA,EAAW,CAAA,sBAAA,EAAyB,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,QACjD,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,UAAA,EAAY,GAAG,CAAC,CAAA,CAAA,CAAA;AAAI;AAAA,KAC1D,EACJ;AAAA,GAAA,EACJ,CAAA;AAER","file":"react.mjs","sourcesContent":["/**\n * VocoUsageBar Component\n * Usage progress bar\n */\n\n'use client';\n\nimport * as React from 'react';\n\nexport interface VocoUsageBarProps {\n metric: string;\n label?: string;\n color?: 'blue' | 'green' | 'red' | 'purple';\n className?: string;\n}\n\nexport function VocoUsageBar({\n metric,\n label,\n color = 'blue',\n className = '',\n}: VocoUsageBarProps) {\n const [usage, setUsage] = React.useState({ current: 0, limit: 0, percentage: 0 });\n const [loading, setLoading] = React.useState(true);\n\n React.useEffect(() => {\n async function fetchUsage() {\n try {\n const res = await fetch(`/api/usage?metric=${metric}`);\n const data = await res.json();\n setUsage(data);\n } catch (error) {\n console.error('Failed to fetch usage:', error);\n } finally {\n setLoading(false);\n }\n }\n\n fetchUsage();\n }, [metric]);\n\n if (loading) {\n return <div className=\"h-4 w-full animate-pulse rounded bg-gray-200\" />;\n }\n\n const colors = {\n blue: 'bg-blue-600',\n green: 'bg-green-600',\n red: 'bg-red-600',\n purple: 'bg-purple-600',\n };\n\n return (\n <div className={`space-y-2 ${className}`}>\n {label && (\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"font-medium\">{label}</span>\n <span className=\"text-gray-600\">\n {usage.current} / {usage.limit}\n </span>\n </div>\n )}\n <div className=\"h-2 w-full overflow-hidden rounded-full bg-gray-200\">\n <div\n className={`h-full transition-all ${colors[color]}`}\n style={{ width: `${Math.min(usage.percentage, 100)}%` }}\n />\n </div>\n </div>\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vocoweb/meter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready usage tracking and limits for B2B SaaS",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./react": {
|
|
15
|
+
"import": "./dist/react.mjs",
|
|
16
|
+
"types": "./dist/react.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"test": "vitest",
|
|
28
|
+
"lint": "eslint src",
|
|
29
|
+
"type-check": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"usage-tracking",
|
|
34
|
+
"metering",
|
|
35
|
+
"limits",
|
|
36
|
+
"quotas",
|
|
37
|
+
"usage-based-pricing",
|
|
38
|
+
"saas"
|
|
39
|
+
],
|
|
40
|
+
"author": "VocoWeb <legal@vocoweb.in>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/vocoweb/vocoweb-meter.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/vocoweb/vocoweb-meter#readme",
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"next": ">=14.0.0",
|
|
49
|
+
"react": ">=18.0.0",
|
|
50
|
+
"react-dom": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"next": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
59
|
+
"class-variance-authority": "^0.7.0",
|
|
60
|
+
"clsx": "^2.1.0",
|
|
61
|
+
"lucide-react": "^0.468.0",
|
|
62
|
+
"tailwind-merge": "^2.2.0",
|
|
63
|
+
"zod": "^3.22.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^20.11.0",
|
|
67
|
+
"@types/react": "^18.2.0",
|
|
68
|
+
"@types/react-dom": "^18.2.0",
|
|
69
|
+
"eslint": "^8.56.0",
|
|
70
|
+
"tsup": "^8.0.0",
|
|
71
|
+
"typescript": "^5.3.0",
|
|
72
|
+
"vitest": "^1.2.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
}
|
|
80
|
+
}
|