Initial commit

This commit is contained in:
2026-03-18 18:26:24 +08:00
commit c784d07796
60 changed files with 3756 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import Anthropic from '@anthropic-ai/sdk';
import { getConfig } from '@/lib/db';
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
export async function getModel(): Promise<string> {
return (await getConfig('openai_model')) || 'claude-3-5-sonnet-20241022';
}
export async function generate(
systemPrompt: string,
userPrompt: string
): Promise<{ text: string; tokens: number }> {
const model = await getModel();
const response = await anthropic.messages.create({
model,
max_tokens: 4096,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
});
const text = response.content
.filter((b) => b.type === 'text')
.map((b) => (b as Anthropic.TextBlock).text)
.join('');
const tokens = (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0);
return { text, tokens };
}
export default anthropic;
+64
View File
@@ -0,0 +1,64 @@
export const funderRewriterMaster = (input: Record<string, string>) => `
You are an expert grant writer who specialises in tailoring NGO proposals to specific funders' priorities and language.
Rewrite the following proposal text to align with the target funder's priorities:
Original Text:
${input.original_text}
Target Funder: ${input.funder_name}
Funder Type: ${input.funder_type}
Funder Priorities: ${input.funder_priorities}
Grant Size: ${input.grant_size}
Sector Focus: ${input.sector}
Rewrite the text to:
1. **Mirror the funder's language** — Use terminology and framing the funder prefers
2. **Highlight alignment** — Emphasise how the project meets their specific priorities
3. **Adjust tone** — Match the formality level (institutional vs. foundation vs. corporate)
4. **Strengthen impact claims** — Frame outcomes in terms the funder values
5. **Add strategic keywords** — Include terms from the funder's own strategy documents
Provide the rewritten text with annotations explaining key changes made and why.
`;
export const funderRewriterImprove = (section: string, feedback: string) => `
You are an expert grant writer. Improve the following rewritten section based on feedback.
Current Rewritten Section:
${section}
Feedback:
${feedback}
Refine the text to better address the feedback while maintaining alignment with the target funder's language and priorities.
`;
export const funderRewriterValidate = (rewrite: string) => `
You are a senior grants manager reviewing a funder-tailored proposal rewrite.
Review this rewritten proposal:
${rewrite}
Assess:
1. **Funder Alignment** — Does the language match the target funder's style?
2. **Strategic Fit** — Are the funder's priorities clearly addressed?
3. **Persuasiveness** — Is the case compelling and well-argued?
4. **Authenticity** — Does it feel genuine, not forced or generic?
5. **Clarity** — Is the writing clear, concise, and jargon-appropriate?
6. **Compliance** — Does it meet typical funder expectations for structure?
Score each area out of 10 and provide specific improvement suggestions.
`;
export const funderRewriterFinalize = (rewrite: string, improvements: string) => `
You are an expert grant writer producing the final funder-tailored version.
Rewritten proposal:
${rewrite}
Improvements incorporated:
${improvements}
Produce the final, polished version that is ready for submission. Ensure the funder alignment is seamless and the writing is compelling.
`;
+24
View File
@@ -0,0 +1,24 @@
import { ToolName } from '@/lib/types';
import { logframeMaster, logframeImprove, logframeValidate, logframeFinalize } from './logframe';
import { funderRewriterMaster, funderRewriterImprove, funderRewriterValidate, funderRewriterFinalize } from './funder-rewriter';
import { smartIndicatorsMaster, smartIndicatorsImprove, smartIndicatorsValidate, smartIndicatorsFinalize } from './smart-indicators';
import { theoryOfChangeMaster, theoryOfChangeImprove, theoryOfChangeValidate, theoryOfChangeFinalize } from './theory-of-change';
type PromptFn = (input: Record<string, string>) => string;
type SectionPromptFn = (section: string, feedback: string) => string;
type ValidateFn = (content: string) => string;
type FinalizeFn = (content: string, improvements: string) => string;
interface ToolPrompts {
master: PromptFn;
improve: SectionPromptFn;
validate: ValidateFn;
finalize: FinalizeFn;
}
export const prompts: Record<ToolName, ToolPrompts> = {
logframe: { master: logframeMaster, improve: logframeImprove, validate: logframeValidate, finalize: logframeFinalize },
'funder-rewriter': { master: funderRewriterMaster, improve: funderRewriterImprove, validate: funderRewriterValidate, finalize: funderRewriterFinalize },
'smart-indicators': { master: smartIndicatorsMaster, improve: smartIndicatorsImprove, validate: smartIndicatorsValidate, finalize: smartIndicatorsFinalize },
'theory-of-change': { master: theoryOfChangeMaster, improve: theoryOfChangeImprove, validate: theoryOfChangeValidate, finalize: theoryOfChangeFinalize },
};
+72
View File
@@ -0,0 +1,72 @@
export const logframeMaster = (input: Record<string, string>) => `
You are an expert international development consultant specialising in logical frameworks (logframes) for NGO grant applications.
Create a complete, professional logframe matrix for the following project:
Project Title: ${input.project_title}
Organisation: ${input.organisation}
Sector: ${input.sector}
Target Beneficiaries: ${input.beneficiaries}
Geographic Location: ${input.location}
Project Duration: ${input.duration}
Budget Range: ${input.budget}
Project Summary: ${input.summary}
Generate a complete logframe with:
1. **Goal (Impact)** — The high-level development objective this project contributes to
2. **Purpose (Outcome)** — The specific change the project will achieve
3. **Outputs** — 3-5 concrete deliverables/results
4. **Activities** — 2-3 key activities per output
5. **Indicators** — SMART indicators for each level (goal, purpose, outputs)
6. **Means of Verification** — How each indicator will be measured
7. **Assumptions** — Key assumptions at each level
Format as a structured logframe matrix using markdown tables. Be specific, measurable, and realistic.
`;
export const logframeImprove = (section: string, feedback: string) => `
You are an expert logframe consultant. Improve the following section based on the feedback provided.
Current Section:
${section}
Feedback:
${feedback}
Provide an improved version that addresses the feedback while maintaining professional quality and logical consistency with a standard logframe matrix.
`;
export const logframeValidate = (logframe: string) => `
You are a senior M&E (Monitoring & Evaluation) specialist reviewing a logframe for quality assurance.
Review this logframe:
${logframe}
Provide a detailed quality assessment covering:
1. **Logical Consistency** — Does the causal chain (activities → outputs → purpose → goal) hold?
2. **SMART Indicators** — Are indicators Specific, Measurable, Achievable, Relevant, Time-bound?
3. **Means of Verification** — Are MOVs realistic and cost-effective?
4. **Assumptions** — Are assumptions clearly stated and reasonable?
5. **Completeness** — Are there gaps or missing elements?
6. **Donor Readiness** — Would this pass scrutiny from major donors (DFID, USAID, EU)?
Score each area out of 10 and provide specific recommendations for improvement.
`;
export const logframeFinalize = (logframe: string, improvements: string) => `
You are an expert logframe consultant producing the final version of a logframe.
Original logframe:
${logframe}
Improvements and feedback incorporated:
${improvements}
Produce the final, polished logframe matrix. Ensure:
- All indicators are fully SMART
- Means of verification are practical and specific
- Assumptions are clearly articulated
- The logical chain is coherent and compelling
- Language is professional and donor-ready
- Format is clean markdown with proper table structure
`;
+73
View File
@@ -0,0 +1,73 @@
export const smartIndicatorsMaster = (input: Record<string, string>) => `
You are an M&E (Monitoring & Evaluation) specialist who designs SMART indicators for NGO programmes.
Design a comprehensive set of SMART indicators for:
Project/Programme: ${input.project_title}
Sector: ${input.sector}
Outcomes to Measure: ${input.outcomes}
Target Population: ${input.target_population}
Timeframe: ${input.timeframe}
Data Collection Capacity: ${input.data_capacity}
Generate:
1. **Impact Indicators** (2-3) — Long-term change indicators
2. **Outcome Indicators** (3-5) — Medium-term results indicators
3. **Output Indicators** (5-8) — Direct deliverable indicators
4. **Process Indicators** (3-4) — Implementation quality indicators
For each indicator provide:
- **Indicator statement** (precise, measurable wording)
- **Baseline** (suggested baseline approach)
- **Target** (realistic target with timeframe)
- **Data source** (where/how to collect)
- **Frequency** (how often to measure)
- **Disaggregation** (gender, age, location, etc.)
Format as a clear structured table.
`;
export const smartIndicatorsImprove = (section: string, feedback: string) => `
You are an M&E specialist. Improve these indicators based on feedback.
Current Indicators:
${section}
Feedback:
${feedback}
Refine the indicators to be more SMART (Specific, Measurable, Achievable, Relevant, Time-bound) based on the feedback.
`;
export const smartIndicatorsValidate = (indicators: string) => `
You are a senior M&E advisor reviewing a set of SMART indicators.
Review these indicators:
${indicators}
Assess each indicator against SMART criteria:
1. **Specific** — Is it clear what is being measured?
2. **Measurable** — Can it be quantified or objectively verified?
3. **Achievable** — Is the target realistic given context?
4. **Relevant** — Does it measure what matters for the programme?
5. **Time-bound** — Is there a clear timeframe?
Also assess:
6. **Data feasibility** — Can the data realistically be collected?
7. **Completeness** — Are all key results areas covered?
8. **Disaggregation** — Is equity adequately captured?
Score each indicator and provide specific improvement recommendations.
`;
export const smartIndicatorsFinalize = (indicators: string, improvements: string) => `
You are an M&E specialist producing the final indicator framework.
Draft indicators:
${indicators}
Improvements to incorporate:
${improvements}
Produce the final, polished indicator framework ready for donor submission. Ensure all indicators are fully SMART and data collection is feasible.
`;
+69
View File
@@ -0,0 +1,69 @@
export const theoryOfChangeMaster = (input: Record<string, string>) => `
You are a Theory of Change specialist who helps NGOs articulate their impact pathways.
Develop a comprehensive Theory of Change for:
Organisation/Project: ${input.project_title}
Mission: ${input.mission}
Problem Statement: ${input.problem}
Target Population: ${input.target_population}
Geographic Context: ${input.context}
Timeframe: ${input.timeframe}
Generate a complete Theory of Change including:
1. **Problem Analysis** — Root causes and contributing factors
2. **Long-term Change (Impact)** — The ultimate vision of change
3. **Medium-term Outcomes** (3-4) — Changes that lead to impact
4. **Short-term Outcomes** (4-6) — Early changes from activities
5. **Outputs** — What the project directly produces
6. **Activities/Interventions** — What the project does
7. **Inputs** — Resources required
8. **Assumptions** — What must hold true at each level
9. **Evidence Base** — What evidence supports each causal link
10. **Causal Pathways** — Clear narrative connecting each level
Format with clear headers and a narrative that explains the causal logic. Include a text-based pathway diagram.
`;
export const theoryOfChangeImprove = (section: string, feedback: string) => `
You are a Theory of Change specialist. Improve this section based on feedback.
Current Section:
${section}
Feedback:
${feedback}
Strengthen the causal logic, evidence base, and clarity based on the feedback provided.
`;
export const theoryOfChangeValidate = (toc: string) => `
You are a senior programme strategist reviewing a Theory of Change.
Review this Theory of Change:
${toc}
Assess:
1. **Causal Logic** — Are the pathways from activities to impact clear and plausible?
2. **Evidence Base** — Is there evidence supporting the causal assumptions?
3. **Assumptions** — Are assumptions explicit, testable, and reasonable?
4. **Completeness** — Are all levels fully articulated?
5. **Coherence** — Does everything fit together logically?
6. **Testability** — Could this ToC be tested/validated through M&E?
7. **Stakeholder Perspective** — Are affected communities reflected?
Score each area out of 10 and provide specific recommendations.
`;
export const theoryOfChangeFinalize = (toc: string, improvements: string) => `
You are a Theory of Change specialist producing the final version.
Draft Theory of Change:
${toc}
Improvements to incorporate:
${improvements}
Produce the final, polished Theory of Change ready for donor submission. Ensure causal pathways are clear, assumptions explicit, and the narrative compelling.
`;
+10
View File
@@ -0,0 +1,10 @@
import { getSession } from '@auth0/nextjs-auth0';
import { NextResponse } from 'next/server';
export async function requireAdmin(): Promise<NextResponse | null> {
const session = await getSession();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return null;
}
+40
View File
@@ -0,0 +1,40 @@
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000,
});
export async function query<T = Record<string, unknown>>(
text: string,
params?: unknown[]
): Promise<T[]> {
const result = await pool.query(text, params);
return result.rows as T[];
}
export async function getConfig(key: string): Promise<string | null> {
const rows = await query<{ value: string }>(
'SELECT value FROM admin_config WHERE key = $1',
[key]
);
return rows[0]?.value ?? null;
}
export async function getAllConfig(): Promise<Record<string, string>> {
const rows = await query<{ key: string; value: string }>(
'SELECT key, value FROM admin_config ORDER BY key'
);
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
}
export async function setConfig(key: string, value: string): Promise<void> {
await query(
`INSERT INTO admin_config (key, value, updated_at) VALUES ($1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`,
[key, value]
);
}
export default pool;
+16
View File
@@ -0,0 +1,16 @@
import { readFileSync } from 'fs';
import { Pool } from 'pg';
import { join } from 'path';
async function migrate() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const sql = readFileSync(join(__dirname, 'schema.sql'), 'utf-8');
await pool.query(sql);
console.log('✅ Migration complete');
await pool.end();
}
migrate().catch((e) => {
console.error('Migration failed:', e);
process.exit(1);
});
+64
View File
@@ -0,0 +1,64 @@
-- NGO Toolkit Lab — Database Schema
-- Run: psql $DATABASE_URL < lib/db/schema.sql
CREATE TABLE IF NOT EXISTS admin_config (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS leads (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
org_name VARCHAR(255),
tool VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- One free generation per email globally
CREATE UNIQUE INDEX IF NOT EXISTS leads_email_unique ON leads(email);
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tool VARCHAR(50) NOT NULL,
email VARCHAR(255),
input JSONB NOT NULL DEFAULT '{}',
output TEXT,
is_paid BOOLEAN DEFAULT FALSE,
payment_id VARCHAR(255),
improvements_used INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS usage (
id SERIAL PRIMARY KEY,
session_id UUID REFERENCES sessions(id),
tool VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
tokens_used INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
-- Default config
INSERT INTO admin_config (key, value) VALUES
('price_currency', 'EUR'),
('free_tier_enabled', 'true'),
('cap_improve', '10'),
('cap_analyse', '3'),
('openai_model', 'claude-3-5-sonnet-20241022'), -- rename key kept for compat, value is Claude model
('lemonsqueezy_store_id', ''),
-- Per-tool prices (cents) and LemonSqueezy variant IDs
('price_logframe', '900'),
('variant_logframe', ''),
('price_funder-rewriter', '900'),
('variant_funder-rewriter', ''),
('price_smart-indicators', '900'),
('variant_smart-indicators', ''),
('price_theory-of-change', '900'),
('variant_theory-of-change', '')
ON CONFLICT (key) DO NOTHING;
CREATE INDEX IF NOT EXISTS idx_sessions_tool ON sessions(tool);
CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(email);
CREATE INDEX IF NOT EXISTS idx_leads_email ON leads(email);
CREATE INDEX IF NOT EXISTS idx_usage_session ON usage(session_id);
+109
View File
@@ -0,0 +1,109 @@
import {
Document,
Paragraph,
TextRun,
HeadingLevel,
Packer,
AlignmentType,
} from 'docx';
export async function generateDocx(
content: string,
toolName: string
): Promise<Buffer> {
const lines = content.split('\n');
const children: Paragraph[] = [];
// Title
children.push(
new Paragraph({
children: [
new TextRun({
text: `NGO Toolkit Lab — ${toolName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())}`,
bold: true,
size: 32,
color: '1a6b3c',
}),
],
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
spacing: { after: 400 },
})
);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
children.push(new Paragraph({ spacing: { after: 100 } }));
continue;
}
if (trimmed.startsWith('### ')) {
children.push(
new Paragraph({
children: [new TextRun({ text: trimmed.slice(4), bold: true, size: 24 })],
heading: HeadingLevel.HEADING_3,
spacing: { before: 200, after: 100 },
})
);
} else if (trimmed.startsWith('## ')) {
children.push(
new Paragraph({
children: [new TextRun({ text: trimmed.slice(3), bold: true, size: 28, color: '1a6b3c' })],
heading: HeadingLevel.HEADING_2,
spacing: { before: 300, after: 150 },
})
);
} else if (trimmed.startsWith('# ')) {
children.push(
new Paragraph({
children: [new TextRun({ text: trimmed.slice(2), bold: true, size: 32, color: '1a6b3c' })],
heading: HeadingLevel.HEADING_1,
spacing: { before: 400, after: 200 },
})
);
} else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
children.push(
new Paragraph({
children: [new TextRun({ text: trimmed.slice(2), size: 22 })],
bullet: { level: 0 },
})
);
} else if (trimmed.startsWith('**') && trimmed.endsWith('**')) {
children.push(
new Paragraph({
children: [new TextRun({ text: trimmed.slice(2, -2), bold: true, size: 22 })],
})
);
} else {
children.push(
new Paragraph({
children: [new TextRun({ text: trimmed, size: 22 })],
spacing: { after: 100 },
})
);
}
}
// Footer
children.push(
new Paragraph({
children: [
new TextRun({
text: 'Generated by NGO Toolkit Lab — ngotoolkit.com',
italics: true,
size: 18,
color: '888888',
}),
],
alignment: AlignmentType.CENTER,
spacing: { before: 600 },
})
);
const doc = new Document({
sections: [{ children }],
});
return Buffer.from(await Packer.toBuffer(doc));
}
+54
View File
@@ -0,0 +1,54 @@
import * as XLSX from 'xlsx';
export async function generateExcel(
content: string,
toolName: string
): Promise<Buffer> {
const wb = XLSX.utils.book_new();
// Parse markdown tables if present
const lines = content.split('\n');
const tableRows: string[][] = [];
const textRows: string[][] = [];
let inTable = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
// Skip separator rows
if (trimmed.match(/^\|[\s-:|]+\|$/)) continue;
inTable = true;
const cells = trimmed
.split('|')
.slice(1, -1)
.map((c) => c.trim());
tableRows.push(cells);
} else {
if (inTable) inTable = false;
if (trimmed) {
textRows.push([trimmed]);
}
}
}
// Create table sheet if tables found
if (tableRows.length > 0) {
const ws = XLSX.utils.aoa_to_sheet(tableRows);
XLSX.utils.book_append_sheet(wb, ws, 'Framework');
}
// Create content sheet
const title = toolName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
const contentRows = [
[`NGO Toolkit Lab — ${title}`],
[''],
...textRows,
[''],
['Generated by NGO Toolkit Lab'],
];
const contentWs = XLSX.utils.aoa_to_sheet(contentRows);
XLSX.utils.book_append_sheet(wb, contentWs, 'Full Content');
const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
return Buffer.from(buf);
}
+103
View File
@@ -0,0 +1,103 @@
// Simple PDF generation using a text-to-PDF approach
// @react-pdf/renderer requires React component rendering which is complex in API routes
// Using a simpler approach with manual PDF construction
export async function generatePdf(
content: string,
toolName: string
): Promise<Buffer> {
// Simple PDF using minimal PDF spec
const title = toolName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
const header = `NGO Toolkit Lab — ${title}`;
// Clean content for PDF
const cleanContent = content
.replace(/\*\*/g, '')
.replace(/#{1,3}\s/g, '')
.replace(/\|/g, ' | ');
const lines = cleanContent.split('\n').filter((l) => l.trim());
// Build minimal PDF
const objects: string[] = [];
let objectCount = 0;
const offsets: number[] = [];
const addObject = (content: string): number => {
objectCount++;
offsets.push(0); // placeholder
objects.push(content);
return objectCount;
};
// Catalog
addObject('1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj');
// Pages
addObject('2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj');
// Build page content
let textContent = `BT\n/F1 16 Tf\n50 800 Td\n(${escPdf(header)}) Tj\n`;
textContent += `/F1 10 Tf\n0 -30 Td\n`;
let y = 770;
for (const line of lines) {
if (y < 50) break; // Simple single-page for now
const safeLine = escPdf(line.substring(0, 100));
textContent += `0 -14 Td\n(${safeLine}) Tj\n`;
y -= 14;
}
// Footer
textContent += `0 -30 Td\n/F1 8 Tf\n(Generated by NGO Toolkit Lab) Tj\n`;
textContent += 'ET';
const streamBytes = Buffer.from(textContent, 'utf-8');
// Page content stream
addObject(
`4 0 obj\n<< /Length ${streamBytes.length} >>\nstream\n${textContent}\nendstream\nendobj`
);
// Font
addObject('5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj');
// Page
// Replace object 3 — insert at position 2
objects.splice(
2,
0,
`3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj`
);
offsets.splice(2, 0, 0);
objectCount = objects.length;
// Build PDF
let pdf = '%PDF-1.4\n';
for (let i = 0; i < objects.length; i++) {
offsets[i] = pdf.length;
pdf += objects[i] + '\n';
}
const xrefOffset = pdf.length;
pdf += 'xref\n';
pdf += `0 ${objectCount + 1}\n`;
pdf += '0000000000 65535 f \n';
for (let i = 0; i < objectCount; i++) {
pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
}
pdf += 'trailer\n';
pdf += `<< /Size ${objectCount + 1} /Root 1 0 R >>\n`;
pdf += 'startxref\n';
pdf += `${xrefOffset}\n`;
pdf += '%%EOF';
return Buffer.from(pdf, 'utf-8');
}
function escPdf(text: string): string {
return text
.replace(/\\/g, '\\\\')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)');
}
+48
View File
@@ -0,0 +1,48 @@
import { createHmac } from 'crypto';
export function verifyWebhookSignature(
payload: string,
signature: string
): boolean {
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET || '';
const hmac = createHmac('sha256', secret);
const digest = hmac.update(payload).digest('hex');
return signature === digest;
}
export async function createCheckout(
variantId: string,
sessionId: string,
email?: string
): Promise<string> {
const response = await fetch('https://api.lemonsqueezy.com/v1/checkouts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LEMONSQUEEZY_API_KEY}`,
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json',
},
body: JSON.stringify({
data: {
type: 'checkouts',
attributes: {
checkout_data: {
email: email || undefined,
custom: { session_id: sessionId },
},
},
relationships: {
store: {
data: { type: 'stores', id: process.env.LEMONSQUEEZY_STORE_ID },
},
variant: {
data: { type: 'variants', id: variantId },
},
},
},
}),
});
const data = await response.json();
return data.data.attributes.url;
}
+66
View File
@@ -0,0 +1,66 @@
import { ToolConfig } from './types';
export const tools: ToolConfig[] = [
{
slug: 'logframe',
name: 'Logframe Generator',
description: 'Generate professional logical framework matrices for grant applications in minutes.',
icon: '📊',
fields: [
{ name: 'project_title', label: 'Project Title', type: 'text', required: true, placeholder: 'e.g. Clean Water Access Programme' },
{ name: 'organisation', label: 'Organisation', type: 'text', required: true, placeholder: 'e.g. WaterAid' },
{ name: 'sector', label: 'Sector', type: 'select', required: true, options: ['Health', 'Education', 'WASH', 'Livelihoods', 'Protection', 'Food Security', 'Climate', 'Governance', 'Other'] },
{ name: 'beneficiaries', label: 'Target Beneficiaries', type: 'textarea', required: true, placeholder: 'Describe who benefits and estimated numbers' },
{ name: 'location', label: 'Geographic Location', type: 'text', required: true, placeholder: 'e.g. Rural Kenya, Kakamega County' },
{ name: 'duration', label: 'Project Duration', type: 'text', required: true, placeholder: 'e.g. 24 months' },
{ name: 'budget', label: 'Budget Range', type: 'text', placeholder: 'e.g. £500,000 - £750,000' },
{ name: 'summary', label: 'Project Summary', type: 'textarea', required: true, placeholder: 'Describe the project in 2-3 paragraphs' },
],
},
{
slug: 'funder-rewriter',
name: 'Funder Rewriter',
description: 'Tailor your proposal language to match specific funders\' priorities and terminology.',
icon: '✍️',
fields: [
{ name: 'original_text', label: 'Original Proposal Text', type: 'textarea', required: true, placeholder: 'Paste your existing proposal text here' },
{ name: 'funder_name', label: 'Target Funder', type: 'text', required: true, placeholder: 'e.g. DFID, USAID, Gates Foundation' },
{ name: 'funder_type', label: 'Funder Type', type: 'select', required: true, options: ['Bilateral (Government)', 'Multilateral (UN/World Bank)', 'Foundation', 'Corporate', 'Trust', 'Other'] },
{ name: 'funder_priorities', label: 'Funder Priorities', type: 'textarea', required: true, placeholder: 'List the funder\'s key strategic priorities' },
{ name: 'grant_size', label: 'Grant Size', type: 'text', placeholder: 'e.g. £100,000 - £500,000' },
{ name: 'sector', label: 'Sector', type: 'text', required: true, placeholder: 'e.g. Health, Education' },
],
},
{
slug: 'smart-indicators',
name: 'SMART Indicators',
description: 'Design comprehensive SMART indicator frameworks for M&E systems.',
icon: '🎯',
fields: [
{ name: 'project_title', label: 'Project/Programme Name', type: 'text', required: true, placeholder: 'e.g. Maternal Health Improvement Programme' },
{ name: 'sector', label: 'Sector', type: 'select', required: true, options: ['Health', 'Education', 'WASH', 'Livelihoods', 'Protection', 'Food Security', 'Climate', 'Governance', 'Other'] },
{ name: 'outcomes', label: 'Outcomes to Measure', type: 'textarea', required: true, placeholder: 'List the key outcomes you need indicators for' },
{ name: 'target_population', label: 'Target Population', type: 'textarea', required: true, placeholder: 'Who are you measuring change for?' },
{ name: 'timeframe', label: 'Timeframe', type: 'text', required: true, placeholder: 'e.g. 3 years' },
{ name: 'data_capacity', label: 'Data Collection Capacity', type: 'select', required: true, options: ['Basic (paper-based)', 'Moderate (some digital)', 'Advanced (full digital M&E system)'] },
],
},
{
slug: 'theory-of-change',
name: 'Theory of Change',
description: 'Build compelling theories of change with clear causal pathways and evidence.',
icon: '🔄',
fields: [
{ name: 'project_title', label: 'Project/Organisation Name', type: 'text', required: true, placeholder: 'e.g. Youth Empowerment Initiative' },
{ name: 'mission', label: 'Mission Statement', type: 'textarea', required: true, placeholder: 'What is your organisation/project mission?' },
{ name: 'problem', label: 'Problem Statement', type: 'textarea', required: true, placeholder: 'What problem are you addressing? Include root causes.' },
{ name: 'target_population', label: 'Target Population', type: 'textarea', required: true, placeholder: 'Who are you trying to help?' },
{ name: 'context', label: 'Geographic/Social Context', type: 'textarea', required: true, placeholder: 'Describe the context in which you operate' },
{ name: 'timeframe', label: 'Timeframe', type: 'text', required: true, placeholder: 'e.g. 5 years' },
],
},
];
export function getToolBySlug(slug: string): ToolConfig | undefined {
return tools.find((t) => t.slug === slug);
}
+48
View File
@@ -0,0 +1,48 @@
export type ToolName = 'logframe' | 'funder-rewriter' | 'smart-indicators' | 'theory-of-change';
export interface Session {
id: string;
tool: ToolName;
email: string | null;
input: Record<string, string>;
output: string | null;
is_paid: boolean;
payment_id: string | null;
improvements_used: number;
created_at: string;
updated_at: string;
}
export interface Lead {
id: number;
email: string;
org_name: string | null;
tool: string;
created_at: string;
}
export interface AdminConfig {
price_amount: string;
price_currency: string;
free_tier_enabled: string;
max_improvements: string;
openai_model: string;
lemonsqueezy_variant_id: string;
}
export interface ToolConfig {
slug: ToolName;
name: string;
description: string;
icon: string;
fields: FormField[];
}
export interface FormField {
name: string;
label: string;
type: 'text' | 'textarea' | 'select';
placeholder?: string;
options?: string[];
required?: boolean;
}