Translate all hardcoded English strings to use i18n translation keys: Components: - TransactionDetailModal: payment details, refunds, technical info - ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup - StripeApiKeysForm: API key management - DomainPurchase: domain registration flow - Sidebar: navigation labels - Schedule/Sidebar, PendingSidebar: scheduler UI - MasqueradeBanner: masquerade status - Dashboard widgets: metrics, capacity, customers, tickets - Marketing: PricingTable, PluginShowcase, BenefitsSection - ConfirmationModal, ServiceList: common UI Pages: - Staff: invitation flow, role management - Customers: form placeholders - Payments: transactions, payouts, billing - BookingSettings: URL and redirect configuration - TrialExpired: upgrade prompts and features - PlatformSettings, PlatformBusinesses: admin UI - HelpApiDocs: API documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1787 lines
70 KiB
TypeScript
1787 lines
70 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
ArrowLeft,
|
|
Copy,
|
|
Check,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
ExternalLink,
|
|
Key,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
import { useTestTokensForDocs } from '../hooks/useApiTokens';
|
|
import { API_BASE_URL } from '../api/config';
|
|
|
|
// =============================================================================
|
|
// TYPES & CONSTANTS
|
|
// =============================================================================
|
|
|
|
type CodeLanguage = 'curl' | 'python' | 'php' | 'ruby' | 'perl' | 'json' | 'http' | 'javascript' | 'java' | 'go' | 'csharp';
|
|
|
|
interface LanguageConfig {
|
|
label: string;
|
|
icon: string;
|
|
}
|
|
|
|
const LANGUAGES: Record<CodeLanguage, LanguageConfig> = {
|
|
curl: { label: 'cURL', icon: '>' },
|
|
python: { label: 'Python', icon: 'py' },
|
|
php: { label: 'PHP', icon: 'php' },
|
|
ruby: { label: 'Ruby', icon: 'rb' },
|
|
perl: { label: 'Perl', icon: 'pl' },
|
|
javascript: { label: 'Node.js', icon: 'js' },
|
|
java: { label: 'Java', icon: 'java' },
|
|
go: { label: 'Go', icon: 'go' },
|
|
csharp: { label: '.NET', icon: 'cs' },
|
|
json: { label: 'JSON', icon: '{}' },
|
|
http: { label: 'HTTP', icon: 'H' },
|
|
};
|
|
|
|
// Default test credentials (used when no tokens are available)
|
|
const DEFAULT_TEST_API_KEY = 'ss_test_<your_test_token_here>';
|
|
const DEFAULT_TEST_WEBHOOK_SECRET = 'whsec_test_abc123def456ghi789jkl012mno345pqr678';
|
|
const SANDBOX_URL = 'https://sandbox.smoothschedule.com/v1';
|
|
|
|
// Multi-language code interface
|
|
interface MultiLangCode {
|
|
curl: string;
|
|
python: string;
|
|
php: string;
|
|
ruby: string;
|
|
perl: string;
|
|
javascript: string;
|
|
java: string;
|
|
go: string;
|
|
csharp: string;
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONTEXT FOR SHARED LANGUAGE STATE
|
|
// =============================================================================
|
|
|
|
const LanguageContext = React.createContext<{
|
|
activeLanguage: keyof MultiLangCode;
|
|
setActiveLanguage: (lang: keyof MultiLangCode) => void;
|
|
}>({
|
|
activeLanguage: 'curl',
|
|
setActiveLanguage: () => {},
|
|
});
|
|
|
|
// =============================================================================
|
|
// SYNTAX HIGHLIGHTING
|
|
// =============================================================================
|
|
|
|
const highlightSyntax = (code: string, language: CodeLanguage): React.ReactNode => {
|
|
const patterns: Record<string, Array<{ pattern: RegExp; className: string }>> = {
|
|
json: [
|
|
{ pattern: /"([^"\\]|\\.)*"(?=\s*:)/g, className: 'text-purple-400' },
|
|
{ pattern: /"([^"\\]|\\.)*"(?!\s*:)/g, className: 'text-green-400' },
|
|
{ pattern: /\b(true|false|null)\b/g, className: 'text-orange-400' },
|
|
{ pattern: /\b(-?\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
],
|
|
python: [
|
|
{ pattern: /#.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(import|from|class|def|return|if|else|elif|for|while|try|except|with|as|None|True|False|self|async|await)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*"|f"([^"\\]|\\.)*"|f'([^'\\]|\\.)*')/g, className: 'text-green-400' },
|
|
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
{ pattern: /\b([A-Z][a-zA-Z0-9_]*)\b/g, className: 'text-yellow-400' },
|
|
{ pattern: /@\w+/g, className: 'text-pink-400' },
|
|
],
|
|
php: [
|
|
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(class|function|public|private|protected|return|if|else|elseif|foreach|for|while|new|throw|try|catch|extends|implements|use|namespace|array|string|int|bool|void)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /\$[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-orange-400' },
|
|
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
{ pattern: /->/g, className: 'text-pink-400' },
|
|
],
|
|
ruby: [
|
|
{ pattern: /#.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(class|def|end|return|if|else|elsif|unless|while|do|require|module|attr_accessor|attr_reader|attr_writer|private|protected|public|new|nil|true|false|self|yield|raise|begin|rescue)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /:[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-cyan-400' },
|
|
{ pattern: /@[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-orange-400' },
|
|
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
],
|
|
perl: [
|
|
{ pattern: /#.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(use|my|sub|return|if|else|elsif|unless|while|for|foreach|package|strict|warnings|die|print|say)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /\$[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-orange-400' },
|
|
{ pattern: /@[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-yellow-400' },
|
|
{ pattern: /%[a-zA-Z_][a-zA-Z0-9_]*/g, className: 'text-pink-400' },
|
|
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
],
|
|
curl: [
|
|
{ pattern: /\bcurl\b/g, className: 'text-purple-400' },
|
|
{ pattern: /-[A-Za-z]+/g, className: 'text-cyan-400' },
|
|
{ pattern: /('([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /https?:\/\/[^\s"']+/g, className: 'text-yellow-400' },
|
|
{ pattern: /\\$/gm, className: 'text-gray-500' },
|
|
],
|
|
javascript: [
|
|
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(const|let|var|function|class|return|if|else|for|while|async|await|new|this|import|export|from|default|throw|try|catch|extends|require|module)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /(`[^`]*`|'([^'\\]|\\.)*'|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
{ pattern: /\b(true|false|null|undefined)\b/g, className: 'text-orange-400' },
|
|
],
|
|
java: [
|
|
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(public|private|protected|class|interface|extends|implements|static|final|void|return|if|else|for|while|try|catch|throw|throws|new|import|package|this|super)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /("([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /\b(\d+\.?\d*[LlFfDd]?)\b/g, className: 'text-cyan-400' },
|
|
{ pattern: /\b(true|false|null)\b/g, className: 'text-orange-400' },
|
|
{ pattern: /\b(String|int|long|double|float|boolean|Integer|Long|Double|Map|List|HashMap|ArrayList|HttpClient|HttpRequest|HttpResponse|URI|JSONObject|JSONArray)\b/g, className: 'text-yellow-400' },
|
|
{ pattern: /@\w+/g, className: 'text-pink-400' },
|
|
],
|
|
go: [
|
|
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(package|import|func|return|if|else|for|range|var|const|type|struct|interface|map|chan|go|defer|select|case|default|break|continue|fallthrough|nil)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /(`[^`]*`|"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /\b(\d+\.?\d*)\b/g, className: 'text-cyan-400' },
|
|
{ pattern: /\b(true|false|nil)\b/g, className: 'text-orange-400' },
|
|
{ pattern: /\b(string|int|int64|float64|bool|error|byte|rune|any)\b/g, className: 'text-yellow-400' },
|
|
],
|
|
csharp: [
|
|
{ pattern: /\/\/.*/g, className: 'text-gray-500 italic' },
|
|
{ pattern: /\b(using|namespace|class|public|private|protected|static|async|await|return|if|else|for|foreach|while|try|catch|throw|new|var|void|get|set|this|base)\b/g, className: 'text-purple-400' },
|
|
{ pattern: /("([^"\\]|\\.)*"|\$"([^"\\]|\\.)*")/g, className: 'text-green-400' },
|
|
{ pattern: /\b(\d+\.?\d*[MmFfDdLl]?)\b/g, className: 'text-cyan-400' },
|
|
{ pattern: /\b(true|false|null)\b/g, className: 'text-orange-400' },
|
|
{ pattern: /\b(string|int|long|double|float|bool|object|Task|HttpClient|HttpRequestMessage|HttpResponseMessage|StringContent|JsonSerializer|Dictionary|List)\b/g, className: 'text-yellow-400' },
|
|
{ pattern: /\[\w+\]/g, className: 'text-pink-400' },
|
|
],
|
|
http: [
|
|
{ pattern: /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/gm, className: 'text-purple-400 font-bold' },
|
|
{ pattern: /^[A-Za-z-]+(?=:)/gm, className: 'text-cyan-400' },
|
|
{ pattern: /Bearer\s+\S+/g, className: 'text-green-400' },
|
|
{ pattern: /\b(\d+)\b/g, className: 'text-orange-400' },
|
|
],
|
|
};
|
|
|
|
const langPatterns = patterns[language] || [];
|
|
|
|
if (langPatterns.length === 0) {
|
|
return <span>{code}</span>;
|
|
}
|
|
|
|
const lines = code.split('\n');
|
|
|
|
return (
|
|
<>
|
|
{lines.map((line, lineIndex) => {
|
|
let result: Array<{ text: string; className?: string; start: number }> = [{ text: line, start: 0 }];
|
|
|
|
langPatterns.forEach(({ pattern, className }) => {
|
|
const newResult: typeof result = [];
|
|
|
|
result.forEach(segment => {
|
|
if (segment.className) {
|
|
newResult.push(segment);
|
|
return;
|
|
}
|
|
|
|
const text = segment.text;
|
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
let lastIndex = 0;
|
|
let match;
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
if (match.index > lastIndex) {
|
|
newResult.push({ text: text.slice(lastIndex, match.index), start: segment.start + lastIndex });
|
|
}
|
|
newResult.push({ text: match[0], className, start: segment.start + match.index });
|
|
lastIndex = match.index + match[0].length;
|
|
|
|
if (match[0].length === 0) break;
|
|
}
|
|
|
|
if (lastIndex < text.length) {
|
|
newResult.push({ text: text.slice(lastIndex), start: segment.start + lastIndex });
|
|
}
|
|
});
|
|
|
|
result = newResult.length > 0 ? newResult : result;
|
|
});
|
|
|
|
return (
|
|
<React.Fragment key={lineIndex}>
|
|
{result.map((segment, i) => (
|
|
<span key={i} className={segment.className}>{segment.text}</span>
|
|
))}
|
|
{lineIndex < lines.length - 1 && '\n'}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// CODE BLOCK COMPONENTS
|
|
// =============================================================================
|
|
|
|
const CodeBlock: React.FC<{ code: string; language?: CodeLanguage; title?: string }> = ({
|
|
code,
|
|
language = 'json',
|
|
title
|
|
}) => {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(code);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-lg overflow-hidden bg-[#0a0a0f] mb-4">
|
|
{title && (
|
|
<div className="px-4 py-2 text-xs text-gray-400 border-b border-gray-800 flex items-center justify-between">
|
|
<span>{title}</span>
|
|
<span className="text-gray-500">{LANGUAGES[language]?.label || language}</span>
|
|
</div>
|
|
)}
|
|
<div className="relative">
|
|
<pre className="p-4 overflow-x-auto text-sm font-mono leading-relaxed text-gray-300">
|
|
<code>{highlightSyntax(code, language)}</code>
|
|
</pre>
|
|
<button
|
|
onClick={handleCopy}
|
|
className="absolute top-2 right-2 p-1.5 rounded bg-gray-800 hover:bg-gray-700 text-gray-400 transition-colors"
|
|
title="Copy to clipboard"
|
|
>
|
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TabbedCodeBlock: React.FC<{
|
|
codes: MultiLangCode;
|
|
title?: string;
|
|
}> = ({ codes, title }) => {
|
|
const { activeLanguage, setActiveLanguage } = React.useContext(LanguageContext);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(codes[activeLanguage]);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
const tabs: Array<keyof MultiLangCode> = ['curl', 'javascript', 'python', 'go', 'java', 'csharp', 'php', 'ruby', 'perl'];
|
|
|
|
return (
|
|
<div className="rounded-lg overflow-hidden bg-[#0a0a0f] mb-4">
|
|
{title && (
|
|
<div className="px-4 py-2 text-xs text-gray-400 border-b border-gray-800 bg-gray-900/50">
|
|
{title}
|
|
</div>
|
|
)}
|
|
<div className="flex overflow-x-auto bg-[#12121a] border-b border-gray-800">
|
|
{tabs.map((lang) => (
|
|
<button
|
|
key={lang}
|
|
onClick={() => setActiveLanguage(lang)}
|
|
className={`px-4 py-2.5 text-xs font-medium transition-colors whitespace-nowrap border-b-2 ${
|
|
activeLanguage === lang
|
|
? 'text-white bg-[#1a1a24] border-purple-500'
|
|
: 'text-gray-400 hover:text-gray-200 border-transparent hover:bg-[#1a1a24]/50'
|
|
}`}
|
|
>
|
|
{LANGUAGES[lang].label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="relative">
|
|
<pre className="p-4 overflow-x-auto text-sm font-mono leading-relaxed text-gray-300">
|
|
<code>{highlightSyntax(codes[activeLanguage], activeLanguage)}</code>
|
|
</pre>
|
|
<button
|
|
onClick={handleCopy}
|
|
className="absolute top-2 right-2 p-1.5 rounded bg-gray-800 hover:bg-gray-700 text-gray-400 transition-colors"
|
|
title="Copy to clipboard"
|
|
>
|
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// SIDEBAR NAVIGATION
|
|
// =============================================================================
|
|
|
|
interface NavSection {
|
|
titleKey: string;
|
|
id: string;
|
|
items?: { titleKey: string; id: string; method?: 'GET' | 'POST' | 'PATCH' | 'DELETE' }[];
|
|
}
|
|
|
|
const navSections: NavSection[] = [
|
|
{ titleKey: 'help.api.introduction', id: 'introduction' },
|
|
{ titleKey: 'help.api.authentication', id: 'authentication' },
|
|
{ titleKey: 'help.api.errors', id: 'errors' },
|
|
{ titleKey: 'help.api.rateLimits', id: 'rate-limits' },
|
|
{
|
|
titleKey: 'help.api.business',
|
|
id: 'business',
|
|
items: [
|
|
{ titleKey: 'help.api.businessObject', id: 'business-object' },
|
|
{ titleKey: 'help.api.retrieveBusiness', id: 'retrieve-business', method: 'GET' },
|
|
],
|
|
},
|
|
{
|
|
titleKey: 'help.api.services',
|
|
id: 'services',
|
|
items: [
|
|
{ titleKey: 'help.api.serviceObject', id: 'service-object' },
|
|
{ titleKey: 'help.api.listServices', id: 'list-services', method: 'GET' },
|
|
{ titleKey: 'help.api.retrieveService', id: 'retrieve-service', method: 'GET' },
|
|
],
|
|
},
|
|
{
|
|
titleKey: 'help.api.resources',
|
|
id: 'resources',
|
|
items: [
|
|
{ titleKey: 'help.api.resourceObject', id: 'resource-object' },
|
|
{ titleKey: 'help.api.listResources', id: 'list-resources', method: 'GET' },
|
|
{ titleKey: 'help.api.retrieveResource', id: 'retrieve-resource', method: 'GET' },
|
|
],
|
|
},
|
|
{
|
|
titleKey: 'help.api.availability',
|
|
id: 'availability',
|
|
items: [
|
|
{ titleKey: 'help.api.checkAvailability', id: 'check-availability', method: 'GET' },
|
|
],
|
|
},
|
|
{
|
|
titleKey: 'help.api.appointments',
|
|
id: 'appointments',
|
|
items: [
|
|
{ titleKey: 'help.api.appointmentObject', id: 'appointment-object' },
|
|
{ titleKey: 'help.api.createAppointment', id: 'create-appointment', method: 'POST' },
|
|
{ titleKey: 'help.api.retrieveAppointment', id: 'retrieve-appointment', method: 'GET' },
|
|
{ titleKey: 'help.api.updateAppointment', id: 'update-appointment', method: 'PATCH' },
|
|
{ titleKey: 'help.api.cancelAppointment', id: 'cancel-appointment', method: 'DELETE' },
|
|
{ titleKey: 'help.api.listAppointments', id: 'list-appointments', method: 'GET' },
|
|
],
|
|
},
|
|
{
|
|
titleKey: 'help.api.customers',
|
|
id: 'customers',
|
|
items: [
|
|
{ titleKey: 'help.api.customerObject', id: 'customer-object' },
|
|
{ titleKey: 'help.api.createCustomer', id: 'create-customer', method: 'POST' },
|
|
{ titleKey: 'help.api.retrieveCustomer', id: 'retrieve-customer', method: 'GET' },
|
|
{ titleKey: 'help.api.updateCustomer', id: 'update-customer', method: 'PATCH' },
|
|
{ titleKey: 'help.api.listCustomers', id: 'list-customers', method: 'GET' },
|
|
],
|
|
},
|
|
{
|
|
titleKey: 'help.api.webhooks',
|
|
id: 'webhooks',
|
|
items: [
|
|
{ titleKey: 'help.api.webhookEvents', id: 'webhook-events' },
|
|
{ titleKey: 'help.api.createWebhook', id: 'create-webhook', method: 'POST' },
|
|
{ titleKey: 'help.api.listWebhooks', id: 'list-webhooks', method: 'GET' },
|
|
{ titleKey: 'help.api.deleteWebhook', id: 'delete-webhook', method: 'DELETE' },
|
|
{ titleKey: 'help.api.verifySignatures', id: 'verify-signatures' },
|
|
],
|
|
},
|
|
];
|
|
|
|
const MethodBadge: React.FC<{ method: 'GET' | 'POST' | 'PATCH' | 'DELETE' }> = ({ method }) => {
|
|
const colors = {
|
|
GET: 'text-green-400',
|
|
POST: 'text-blue-400',
|
|
PATCH: 'text-yellow-400',
|
|
DELETE: 'text-red-400',
|
|
};
|
|
|
|
return (
|
|
<span className={`text-[10px] font-mono font-bold ${colors[method]}`}>
|
|
{method}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const Sidebar: React.FC<{
|
|
activeSection: string;
|
|
onSectionClick: (id: string) => void;
|
|
t: (key: string) => string;
|
|
}> = ({
|
|
activeSection,
|
|
onSectionClick,
|
|
t,
|
|
}) => {
|
|
const [expandedSections, setExpandedSections] = useState<string[]>(['business', 'services', 'appointments']);
|
|
|
|
const toggleSection = (id: string) => {
|
|
setExpandedSections(prev =>
|
|
prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]
|
|
);
|
|
};
|
|
|
|
return (
|
|
<nav className="w-64 flex-shrink-0 border-r border-gray-200 dark:border-gray-800 sticky top-[65px] h-[calc(100vh-65px)] overflow-y-auto">
|
|
<div className="p-4 space-y-1">
|
|
{navSections.map(section => (
|
|
<div key={section.id}>
|
|
{section.items ? (
|
|
<>
|
|
<button
|
|
onClick={() => toggleSection(section.id)}
|
|
className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
|
|
>
|
|
{t(section.titleKey)}
|
|
{expandedSections.includes(section.id) ? (
|
|
<ChevronDown size={16} />
|
|
) : (
|
|
<ChevronRight size={16} />
|
|
)}
|
|
</button>
|
|
{expandedSections.includes(section.id) && (
|
|
<div className="ml-3 mt-1 space-y-0.5">
|
|
{section.items.map(item => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => onSectionClick(item.id)}
|
|
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${
|
|
activeSection === item.id
|
|
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300'
|
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
}`}
|
|
>
|
|
{item.method && <MethodBadge method={item.method} />}
|
|
<span className="truncate">{t(item.titleKey)}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={() => onSectionClick(section.id)}
|
|
className={`w-full flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
|
|
activeSection === section.id
|
|
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300'
|
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
}`}
|
|
>
|
|
{t(section.titleKey)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// API SECTION COMPONENT (Stripe-style split pane)
|
|
// =============================================================================
|
|
|
|
interface ApiSectionProps {
|
|
id: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const ApiSection: React.FC<ApiSectionProps> = ({ id, children }) => {
|
|
return (
|
|
<section id={id} className="py-12 border-b border-gray-200 dark:border-gray-800 last:border-b-0 scroll-mt-24">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{children}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const ApiContent: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
<div className="prose prose-gray dark:prose-invert max-w-none">
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
const ApiExample: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
<div className="lg:sticky lg:top-[81px] z-10">
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
// =============================================================================
|
|
// ATTRIBUTE TABLE
|
|
// =============================================================================
|
|
|
|
interface Attribute {
|
|
name: string;
|
|
type: string;
|
|
description: string;
|
|
required?: boolean;
|
|
}
|
|
|
|
const AttributeTable: React.FC<{ attributes: Attribute[] }> = ({ attributes }) => (
|
|
<div className="mt-4 space-y-3">
|
|
{attributes.map(attr => (
|
|
<div key={attr.name} className="border-b border-gray-100 dark:border-gray-800 pb-3 last:border-b-0">
|
|
<div className="flex items-center gap-2">
|
|
<code className="text-sm font-mono text-purple-600 dark:text-purple-400">{attr.name}</code>
|
|
<span className="text-xs text-gray-500">{attr.type}</span>
|
|
{attr.required && (
|
|
<span className="text-xs text-red-500 font-medium">required</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{attr.description}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
const HelpApiDocs: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const [activeSection, setActiveSection] = useState('introduction');
|
|
const [activeLanguage, setActiveLanguage] = useState<keyof MultiLangCode>('curl');
|
|
const [selectedTokenId, setSelectedTokenId] = useState<string | null>(null);
|
|
|
|
// Fetch test tokens
|
|
const { data: testTokens, isLoading: tokensLoading, error: tokensError } = useTestTokensForDocs();
|
|
|
|
// Get the currently selected token or the first one
|
|
const selectedToken = testTokens?.find(t => t.id === selectedTokenId) || testTokens?.[0];
|
|
|
|
// Auto-select first token when tokens load
|
|
useEffect(() => {
|
|
if (testTokens && testTokens.length > 0 && !selectedTokenId) {
|
|
setSelectedTokenId(testTokens[0].id);
|
|
}
|
|
}, [testTokens, selectedTokenId]);
|
|
|
|
// Get the API key to use in examples (either from selected token or default)
|
|
const TEST_API_KEY = selectedToken?.key_prefix || DEFAULT_TEST_API_KEY;
|
|
const TEST_WEBHOOK_SECRET = DEFAULT_TEST_WEBHOOK_SECRET;
|
|
|
|
const handleSectionClick = (id: string) => {
|
|
setActiveSection(id);
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
// Use scrollIntoView with the CSS scroll-mt-24 (96px margin)
|
|
// This respects the scroll-margin-top CSS property we set on sections
|
|
element.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Track active section on scroll
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
const sections = document.querySelectorAll('section[id]');
|
|
let current = 'introduction';
|
|
const headerHeight = 72;
|
|
const threshold = headerHeight + 32; // Header height + some padding
|
|
|
|
sections.forEach(section => {
|
|
const rect = section.getBoundingClientRect();
|
|
if (rect.top <= threshold) {
|
|
current = section.id;
|
|
}
|
|
});
|
|
|
|
setActiveSection(current);
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
// =============================================================================
|
|
// CODE SNIPPETS
|
|
// =============================================================================
|
|
|
|
const getServicesCode: MultiLangCode = {
|
|
curl: `curl ${SANDBOX_URL}/services/ \\
|
|
-H "Authorization: Bearer ${TEST_API_KEY}"`,
|
|
javascript: `const response = await fetch('${SANDBOX_URL}/services/', {
|
|
headers: {
|
|
'Authorization': 'Bearer ${TEST_API_KEY}'
|
|
}
|
|
});
|
|
const services = await response.json();`,
|
|
python: `import requests
|
|
|
|
response = requests.get(
|
|
'${SANDBOX_URL}/services/',
|
|
headers={'Authorization': 'Bearer ${TEST_API_KEY}'}
|
|
)
|
|
services = response.json()`,
|
|
go: `req, _ := http.NewRequest("GET", "${SANDBOX_URL}/services/", nil)
|
|
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
|
|
resp, _ := http.DefaultClient.Do(req)`,
|
|
java: `HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create("${SANDBOX_URL}/services/"))
|
|
.header("Authorization", "Bearer ${TEST_API_KEY}")
|
|
.build();
|
|
HttpResponse<String> response = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString());`,
|
|
csharp: `var client = new HttpClient();
|
|
client.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", "${TEST_API_KEY}");
|
|
var response = await client.GetAsync("${SANDBOX_URL}/services/");`,
|
|
php: `$ch = curl_init('${SANDBOX_URL}/services/');
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ${TEST_API_KEY}'
|
|
]);
|
|
$response = curl_exec($ch);`,
|
|
ruby: `response = Net::HTTP.get_response(
|
|
URI('${SANDBOX_URL}/services/'),
|
|
{ 'Authorization' => 'Bearer ${TEST_API_KEY}' }
|
|
)`,
|
|
perl: `my $ua = LWP::UserAgent->new;
|
|
my $response = $ua->get('${SANDBOX_URL}/services/',
|
|
'Authorization' => 'Bearer ${TEST_API_KEY}'
|
|
);`,
|
|
};
|
|
|
|
const getAvailabilityCode: MultiLangCode = {
|
|
curl: `curl "${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01" \\
|
|
-H "Authorization: Bearer ${TEST_API_KEY}"`,
|
|
javascript: `const response = await fetch(
|
|
'${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01',
|
|
{
|
|
headers: {
|
|
'Authorization': 'Bearer ${TEST_API_KEY}'
|
|
}
|
|
}
|
|
);
|
|
const availability = await response.json();`,
|
|
python: `import requests
|
|
|
|
response = requests.get(
|
|
'${SANDBOX_URL}/availability/',
|
|
params={'service_id': 'svc_123', 'date': '2025-12-01'},
|
|
headers={'Authorization': 'Bearer ${TEST_API_KEY}'}
|
|
)
|
|
availability = response.json()`,
|
|
go: `req, _ := http.NewRequest("GET",
|
|
"${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01", nil)
|
|
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
|
|
resp, _ := http.DefaultClient.Do(req)`,
|
|
java: `HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create("${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01"))
|
|
.header("Authorization", "Bearer ${TEST_API_KEY}")
|
|
.build();`,
|
|
csharp: `var response = await client.GetAsync(
|
|
"${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01");`,
|
|
php: `$ch = curl_init('${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01');
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ${TEST_API_KEY}'
|
|
]);`,
|
|
ruby: `uri = URI('${SANDBOX_URL}/availability/')
|
|
uri.query = URI.encode_www_form(service_id: 'svc_123', date: '2025-12-01')
|
|
response = Net::HTTP.get_response(uri, { 'Authorization' => 'Bearer ${TEST_API_KEY}' })`,
|
|
perl: `my $response = $ua->get(
|
|
'${SANDBOX_URL}/availability/?service_id=svc_123&date=2025-12-01',
|
|
'Authorization' => 'Bearer ${TEST_API_KEY}'
|
|
);`,
|
|
};
|
|
|
|
const createAppointmentCode: MultiLangCode = {
|
|
curl: `curl ${SANDBOX_URL}/appointments/ \\
|
|
-H "Authorization: Bearer ${TEST_API_KEY}" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"service_id": "svc_123",
|
|
"resource_id": "res_456",
|
|
"start_time": "2025-12-01T10:00:00Z",
|
|
"customer_email": "john@example.com",
|
|
"customer_name": "John Doe"
|
|
}'`,
|
|
javascript: `const response = await fetch('${SANDBOX_URL}/appointments/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer ${TEST_API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
service_id: 'svc_123',
|
|
resource_id: 'res_456',
|
|
start_time: '2025-12-01T10:00:00Z',
|
|
customer_email: 'john@example.com',
|
|
customer_name: 'John Doe'
|
|
})
|
|
});`,
|
|
python: `import requests
|
|
|
|
response = requests.post(
|
|
'${SANDBOX_URL}/appointments/',
|
|
headers={'Authorization': 'Bearer ${TEST_API_KEY}'},
|
|
json={
|
|
'service_id': 'svc_123',
|
|
'resource_id': 'res_456',
|
|
'start_time': '2025-12-01T10:00:00Z',
|
|
'customer_email': 'john@example.com',
|
|
'customer_name': 'John Doe'
|
|
}
|
|
)`,
|
|
go: `body := strings.NewReader(\`{
|
|
"service_id": "svc_123",
|
|
"resource_id": "res_456",
|
|
"start_time": "2025-12-01T10:00:00Z",
|
|
"customer_email": "john@example.com",
|
|
"customer_name": "John Doe"
|
|
}\`)
|
|
req, _ := http.NewRequest("POST", "${SANDBOX_URL}/appointments/", body)
|
|
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
|
|
req.Header.Set("Content-Type", "application/json")`,
|
|
java: `String json = """
|
|
{
|
|
"service_id": "svc_123",
|
|
"resource_id": "res_456",
|
|
"start_time": "2025-12-01T10:00:00Z",
|
|
"customer_email": "john@example.com",
|
|
"customer_name": "John Doe"
|
|
}
|
|
""";
|
|
HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create("${SANDBOX_URL}/appointments/"))
|
|
.header("Authorization", "Bearer ${TEST_API_KEY}")
|
|
.header("Content-Type", "application/json")
|
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
|
.build();`,
|
|
csharp: `var content = new StringContent(
|
|
JsonSerializer.Serialize(new {
|
|
service_id = "svc_123",
|
|
resource_id = "res_456",
|
|
start_time = "2025-12-01T10:00:00Z",
|
|
customer_email = "john@example.com",
|
|
customer_name = "John Doe"
|
|
}),
|
|
Encoding.UTF8,
|
|
"application/json"
|
|
);
|
|
var response = await client.PostAsync("${SANDBOX_URL}/appointments/", content);`,
|
|
php: `$data = [
|
|
'service_id' => 'svc_123',
|
|
'resource_id' => 'res_456',
|
|
'start_time' => '2025-12-01T10:00:00Z',
|
|
'customer_email' => 'john@example.com',
|
|
'customer_name' => 'John Doe'
|
|
];
|
|
$ch = curl_init('${SANDBOX_URL}/appointments/');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($data),
|
|
CURLOPT_HTTPHEADER => [
|
|
'Authorization: Bearer ${TEST_API_KEY}',
|
|
'Content-Type: application/json'
|
|
]
|
|
]);`,
|
|
ruby: `uri = URI('${SANDBOX_URL}/appointments/')
|
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
http.use_ssl = true
|
|
request = Net::HTTP::Post.new(uri)
|
|
request['Authorization'] = 'Bearer ${TEST_API_KEY}'
|
|
request['Content-Type'] = 'application/json'
|
|
request.body = {
|
|
service_id: 'svc_123',
|
|
resource_id: 'res_456',
|
|
start_time: '2025-12-01T10:00:00Z',
|
|
customer_email: 'john@example.com',
|
|
customer_name: 'John Doe'
|
|
}.to_json`,
|
|
perl: `my $ua = LWP::UserAgent->new;
|
|
my $req = HTTP::Request->new(POST => '${SANDBOX_URL}/appointments/');
|
|
$req->header('Authorization' => 'Bearer ${TEST_API_KEY}');
|
|
$req->header('Content-Type' => 'application/json');
|
|
$req->content(encode_json({
|
|
service_id => 'svc_123',
|
|
resource_id => 'res_456',
|
|
start_time => '2025-12-01T10:00:00Z',
|
|
customer_email => 'john@example.com',
|
|
customer_name => 'John Doe'
|
|
}));`,
|
|
};
|
|
|
|
const createWebhookCode: MultiLangCode = {
|
|
curl: `curl ${SANDBOX_URL}/webhooks/ \\
|
|
-H "Authorization: Bearer ${TEST_API_KEY}" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"url": "https://example.com/webhooks",
|
|
"events": ["appointment.created", "appointment.cancelled"]
|
|
}'`,
|
|
javascript: `const response = await fetch('${SANDBOX_URL}/webhooks/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer ${TEST_API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
url: 'https://example.com/webhooks',
|
|
events: ['appointment.created', 'appointment.cancelled']
|
|
})
|
|
});
|
|
// Save the secret from the response - it's only shown once!`,
|
|
python: `response = requests.post(
|
|
'${SANDBOX_URL}/webhooks/',
|
|
headers={'Authorization': 'Bearer ${TEST_API_KEY}'},
|
|
json={
|
|
'url': 'https://example.com/webhooks',
|
|
'events': ['appointment.created', 'appointment.cancelled']
|
|
}
|
|
)
|
|
# Save the secret from the response - it's only shown once!`,
|
|
go: `body := strings.NewReader(\`{
|
|
"url": "https://example.com/webhooks",
|
|
"events": ["appointment.created", "appointment.cancelled"]
|
|
}\`)
|
|
req, _ := http.NewRequest("POST", "${SANDBOX_URL}/webhooks/", body)
|
|
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// Save the secret from the response - it's only shown once!`,
|
|
java: `String json = """
|
|
{
|
|
"url": "https://example.com/webhooks",
|
|
"events": ["appointment.created", "appointment.cancelled"]
|
|
}
|
|
""";
|
|
HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create("${SANDBOX_URL}/webhooks/"))
|
|
.header("Authorization", "Bearer ${TEST_API_KEY}")
|
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
|
.build();
|
|
// Save the secret from the response - it's only shown once!`,
|
|
csharp: `var content = new StringContent(
|
|
JsonSerializer.Serialize(new {
|
|
url = "https://example.com/webhooks",
|
|
events = new[] { "appointment.created", "appointment.cancelled" }
|
|
}),
|
|
Encoding.UTF8,
|
|
"application/json"
|
|
);
|
|
var response = await client.PostAsync("${SANDBOX_URL}/webhooks/", content);
|
|
// Save the secret from the response - it's only shown once!`,
|
|
php: `$data = [
|
|
'url' => 'https://example.com/webhooks',
|
|
'events' => ['appointment.created', 'appointment.cancelled']
|
|
];
|
|
$ch = curl_init('${SANDBOX_URL}/webhooks/');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($data),
|
|
CURLOPT_HTTPHEADER => [
|
|
'Authorization: Bearer ${TEST_API_KEY}',
|
|
'Content-Type: application/json'
|
|
]
|
|
]);
|
|
// Save the secret from the response - it's only shown once!`,
|
|
ruby: `request = Net::HTTP::Post.new(URI('${SANDBOX_URL}/webhooks/'))
|
|
request['Authorization'] = 'Bearer ${TEST_API_KEY}'
|
|
request['Content-Type'] = 'application/json'
|
|
request.body = {
|
|
url: 'https://example.com/webhooks',
|
|
events: ['appointment.created', 'appointment.cancelled']
|
|
}.to_json
|
|
# Save the secret from the response - it's only shown once!`,
|
|
perl: `my $req = HTTP::Request->new(POST => '${SANDBOX_URL}/webhooks/');
|
|
$req->header('Authorization' => 'Bearer ${TEST_API_KEY}');
|
|
$req->content(encode_json({
|
|
url => 'https://example.com/webhooks',
|
|
events => ['appointment.created', 'appointment.cancelled']
|
|
}));
|
|
# Save the secret from the response - it's only shown once!`,
|
|
};
|
|
|
|
const verifyWebhookCode: MultiLangCode = {
|
|
curl: `# Verify the signature from the X-Webhook-Signature header
|
|
# Format: <timestamp>.<signature>
|
|
echo -n "<timestamp>.<payload>" | \\
|
|
openssl dgst -sha256 -hmac "${TEST_WEBHOOK_SECRET}"`,
|
|
javascript: `const crypto = require('crypto');
|
|
|
|
function verifyWebhook(payload, header, secret) {
|
|
const [timestamp, signature] = header.split('.');
|
|
const expected = crypto
|
|
.createHmac('sha256', secret)
|
|
.update(\`\${timestamp}.\${payload}\`)
|
|
.digest('hex');
|
|
return crypto.timingSafeEqual(
|
|
Buffer.from(signature),
|
|
Buffer.from(expected)
|
|
);
|
|
}
|
|
|
|
// In your webhook handler:
|
|
const isValid = verifyWebhook(
|
|
req.body,
|
|
req.headers['x-webhook-signature'],
|
|
'${TEST_WEBHOOK_SECRET}'
|
|
);`,
|
|
python: `import hmac
|
|
import hashlib
|
|
|
|
def verify_webhook(payload, header, secret):
|
|
timestamp, signature = header.split('.')
|
|
message = f"{timestamp}.{payload}"
|
|
expected = hmac.new(
|
|
secret.encode(),
|
|
message.encode(),
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
return hmac.compare_digest(expected, signature)
|
|
|
|
# In your webhook handler:
|
|
is_valid = verify_webhook(
|
|
request.body,
|
|
request.headers['X-Webhook-Signature'],
|
|
'${TEST_WEBHOOK_SECRET}'
|
|
)`,
|
|
go: `import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
)
|
|
|
|
func verifyWebhook(payload, header, secret string) bool {
|
|
parts := strings.SplitN(header, ".", 2)
|
|
timestamp, signature := parts[0], parts[1]
|
|
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(timestamp + "." + payload))
|
|
expected := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
return hmac.Equal([]byte(signature), []byte(expected))
|
|
}`,
|
|
java: `import javax.crypto.Mac;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
public boolean verifyWebhook(String payload, String header, String secret) {
|
|
String[] parts = header.split("\\\\.", 2);
|
|
String timestamp = parts[0];
|
|
String signature = parts[1];
|
|
|
|
Mac mac = Mac.getInstance("HmacSHA256");
|
|
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
|
|
String expected = bytesToHex(mac.doFinal((timestamp + "." + payload).getBytes()));
|
|
|
|
return MessageDigest.isEqual(signature.getBytes(), expected.getBytes());
|
|
}`,
|
|
csharp: `using System.Security.Cryptography;
|
|
|
|
bool VerifyWebhook(string payload, string header, string secret) {
|
|
var parts = header.Split('.', 2);
|
|
var timestamp = parts[0];
|
|
var signature = parts[1];
|
|
|
|
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
|
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{payload}"));
|
|
var expected = BitConverter.ToString(hash).Replace("-", "").ToLower();
|
|
|
|
return CryptographicOperations.FixedTimeEquals(
|
|
Encoding.UTF8.GetBytes(signature),
|
|
Encoding.UTF8.GetBytes(expected)
|
|
);
|
|
}`,
|
|
php: `function verifyWebhook($payload, $header, $secret) {
|
|
[$timestamp, $signature] = explode('.', $header, 2);
|
|
$expected = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);
|
|
return hash_equals($expected, $signature);
|
|
}
|
|
|
|
// In your webhook handler:
|
|
$isValid = verifyWebhook(
|
|
file_get_contents('php://input'),
|
|
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE'],
|
|
'${TEST_WEBHOOK_SECRET}'
|
|
);`,
|
|
ruby: `require 'openssl'
|
|
|
|
def verify_webhook(payload, header, secret)
|
|
timestamp, signature = header.split('.', 2)
|
|
expected = OpenSSL::HMAC.hexdigest('sha256', secret, "#{timestamp}.#{payload}")
|
|
Rack::Utils.secure_compare(expected, signature)
|
|
end
|
|
|
|
# In your webhook handler:
|
|
is_valid = verify_webhook(
|
|
request.body.read,
|
|
request.env['HTTP_X_WEBHOOK_SIGNATURE'],
|
|
'${TEST_WEBHOOK_SECRET}'
|
|
)`,
|
|
perl: `use Digest::SHA qw(hmac_sha256_hex);
|
|
|
|
sub verify_webhook {
|
|
my ($payload, $header, $secret) = @_;
|
|
my ($timestamp, $signature) = split /\\./, $header, 2;
|
|
my $expected = hmac_sha256_hex("$timestamp.$payload", $secret);
|
|
return $expected eq $signature;
|
|
}`,
|
|
};
|
|
|
|
const authExampleCode: MultiLangCode = {
|
|
curl: `curl ${SANDBOX_URL}/services/ \\
|
|
-H "Authorization: Bearer ${TEST_API_KEY}"`,
|
|
javascript: `const response = await fetch('${SANDBOX_URL}/services/', {
|
|
headers: {
|
|
'Authorization': 'Bearer ${TEST_API_KEY}'
|
|
}
|
|
});`,
|
|
python: `import requests
|
|
|
|
response = requests.get(
|
|
'${SANDBOX_URL}/services/',
|
|
headers={'Authorization': 'Bearer ${TEST_API_KEY}'}
|
|
)`,
|
|
go: `req, _ := http.NewRequest("GET", "${SANDBOX_URL}/services/", nil)
|
|
req.Header.Set("Authorization", "Bearer ${TEST_API_KEY}")
|
|
resp, _ := http.DefaultClient.Do(req)`,
|
|
java: `HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create("${SANDBOX_URL}/services/"))
|
|
.header("Authorization", "Bearer ${TEST_API_KEY}")
|
|
.build();`,
|
|
csharp: `var client = new HttpClient();
|
|
client.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", "${TEST_API_KEY}");
|
|
var response = await client.GetAsync("${SANDBOX_URL}/services/");`,
|
|
php: `$ch = curl_init('${SANDBOX_URL}/services/');
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ${TEST_API_KEY}'
|
|
]);
|
|
$response = curl_exec($ch);`,
|
|
ruby: `uri = URI('${SANDBOX_URL}/services/')
|
|
request = Net::HTTP::Get.new(uri)
|
|
request['Authorization'] = 'Bearer ${TEST_API_KEY}'
|
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }`,
|
|
perl: `my $ua = LWP::UserAgent->new;
|
|
my $response = $ua->get('${SANDBOX_URL}/services/',
|
|
'Authorization' => 'Bearer ${TEST_API_KEY}'
|
|
);`,
|
|
};
|
|
|
|
return (
|
|
<LanguageContext.Provider value={{ activeLanguage, setActiveLanguage }}>
|
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
|
{/* Header */}
|
|
<header className="sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
|
|
<div className="flex items-center justify-between px-6 py-4">
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
|
|
>
|
|
<ArrowLeft size={20} />
|
|
<span className="text-sm">{t('common.back', 'Back')}</span>
|
|
</button>
|
|
<div className="h-6 w-px bg-gray-200 dark:bg-gray-700" />
|
|
<h1 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.title')}
|
|
</h1>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
{/* Test Token Selector */}
|
|
{testTokens && testTokens.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<Key size={16} className="text-gray-400" />
|
|
<select
|
|
value={selectedTokenId || ''}
|
|
onChange={(e) => setSelectedTokenId(e.target.value)}
|
|
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
{testTokens.map((token) => (
|
|
<option key={token.id} value={token.id}>
|
|
{token.name} ({token.key_prefix}...)
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<a
|
|
href={`${API_BASE_URL}/v1/docs/`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-lg transition-colors"
|
|
>
|
|
<ExternalLink size={16} />
|
|
{t('help.api.interactiveExplorer')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* No Test Tokens Banner */}
|
|
{!tokensLoading && (!testTokens || testTokens.length === 0) && (
|
|
<div className="mx-6 mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
|
<div className="flex items-start gap-3">
|
|
<AlertCircle size={20} className="text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-yellow-800 dark:text-yellow-200">
|
|
{t('help.api.noTestTokensFound')}
|
|
</h3>
|
|
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1" dangerouslySetInnerHTML={{ __html: t('help.api.noTestTokensMessage') }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Token Loading Error */}
|
|
{tokensError && (
|
|
<div className="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<div className="flex items-start gap-3">
|
|
<AlertCircle size={20} className="text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-red-800 dark:text-red-200">
|
|
{t('help.api.errorLoadingTokens')}
|
|
</h3>
|
|
<p className="text-sm text-red-700 dark:text-red-300 mt-1">
|
|
{t('help.api.errorLoadingTokensMessage')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex relative">
|
|
{/* Sidebar */}
|
|
<Sidebar activeSection={activeSection} onSectionClick={handleSectionClick} t={t} />
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 min-w-0 px-8 lg:px-12">
|
|
{/* Introduction */}
|
|
<ApiSection id="introduction">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.introduction')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.introDescription')}
|
|
</p>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.introTestMode')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.baseUrl')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.baseUrlDescription')}
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.baseUrl')}
|
|
language="http"
|
|
code={`https://api.smoothschedule.com/v1/
|
|
|
|
# Sandbox/Test environment
|
|
${SANDBOX_URL}`}
|
|
/>
|
|
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
<strong>{t('help.api.sandboxMode')}</strong> {t('help.api.sandboxModeDescription')}
|
|
</p>
|
|
</div>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Authentication */}
|
|
<ApiSection id="authentication">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.authentication')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.authDescription')}
|
|
</p>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.authBearer')}
|
|
</p>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.authWarning')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.apiKeyFormat')}
|
|
</h3>
|
|
<ul className="text-gray-600 dark:text-gray-300">
|
|
<li><code>ss_test_*</code> — {t('help.api.testKey')}</li>
|
|
<li><code>ss_live_*</code> — {t('help.api.liveKey')}</li>
|
|
</ul>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<TabbedCodeBlock
|
|
title={t('help.api.authenticatedRequest')}
|
|
codes={authExampleCode}
|
|
/>
|
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
|
<strong>{t('help.api.keepKeysSecret')}</strong> {t('help.api.keepKeysSecretDescription')}
|
|
</p>
|
|
</div>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Errors */}
|
|
<ApiSection id="errors">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.errors')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.errorsDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.httpStatusCodes')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: '200 - OK', type: '', description: t('help.api.statusOk') },
|
|
{ name: '201 - Created', type: '', description: t('help.api.statusCreated') },
|
|
{ name: '400 - Bad Request', type: '', description: t('help.api.statusBadRequest') },
|
|
{ name: '401 - Unauthorized', type: '', description: t('help.api.statusUnauthorized') },
|
|
{ name: '403 - Forbidden', type: '', description: t('help.api.statusForbidden') },
|
|
{ name: '404 - Not Found', type: '', description: t('help.api.statusNotFound') },
|
|
{ name: '409 - Conflict', type: '', description: t('help.api.statusConflict') },
|
|
{ name: '429 - Too Many Requests', type: '', description: t('help.api.statusTooManyRequests') },
|
|
{ name: '500 - Server Error', type: '', description: t('help.api.statusServerError') },
|
|
]}
|
|
/>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.errorResponse')}
|
|
language="json"
|
|
code={`{
|
|
"error": {
|
|
"code": "validation_error",
|
|
"message": "Invalid request parameters",
|
|
"details": {
|
|
"start_time": ["This field is required."],
|
|
"service_id": ["Invalid service ID."]
|
|
}
|
|
}
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Rate Limits */}
|
|
<ApiSection id="rate-limits">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.rateLimits')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.rateLimitsDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.limits')}
|
|
</h3>
|
|
<ul className="text-gray-600 dark:text-gray-300">
|
|
<li><strong>1,000</strong> {t('help.api.requestsPerHour')}</li>
|
|
<li><strong>100</strong> {t('help.api.requestsPerMinute')}</li>
|
|
</ul>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.rateLimitHeaders')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.rateLimitHeadersDescription')}
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.rateLimitHeaders')}
|
|
language="http"
|
|
code={`X-RateLimit-Limit: 1000
|
|
X-RateLimit-Remaining: 847
|
|
X-RateLimit-Reset: 1732795200
|
|
X-RateLimit-Burst-Limit: 100
|
|
X-RateLimit-Burst-Remaining: 95`}
|
|
/>
|
|
<CodeBlock
|
|
title="429 RESPONSE"
|
|
language="json"
|
|
code={`{
|
|
"error": {
|
|
"code": "rate_limit_exceeded",
|
|
"message": "Rate limit exceeded",
|
|
"retry_after": 120
|
|
}
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Business Object */}
|
|
<ApiSection id="business-object">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.businessObject')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.businessObjectDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.attributes')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: 'id', type: 'string', description: 'Unique identifier for the business.' },
|
|
{ name: 'name', type: 'string', description: 'The business name.' },
|
|
{ name: 'subdomain', type: 'string', description: 'The subdomain for this business.' },
|
|
{ name: 'logo_url', type: 'string', description: 'URL of the business logo.' },
|
|
{ name: 'primary_color', type: 'string', description: 'Primary brand color (hex).' },
|
|
{ name: 'timezone', type: 'string', description: 'Business timezone (IANA format).' },
|
|
{ name: 'cancellation_window_hours', type: 'integer', description: 'Hours before appointment when cancellation is allowed.' },
|
|
]}
|
|
/>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title="THE BUSINESS OBJECT"
|
|
language="json"
|
|
code={`{
|
|
"id": "biz_a1b2c3d4e5f6",
|
|
"name": "Acme Salon",
|
|
"subdomain": "acme",
|
|
"logo_url": "https://cdn.smoothschedule.com/logos/acme.png",
|
|
"primary_color": "#3B82F6",
|
|
"secondary_color": "#1E40AF",
|
|
"timezone": "America/New_York",
|
|
"cancellation_window_hours": 24
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Retrieve Business */}
|
|
<ApiSection id="retrieve-business">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-mono">
|
|
GET
|
|
</span>
|
|
{t('help.api.retrieveBusiness')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.retrieveBusinessDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.requiredScope')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
<code>business:read</code>
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.endpoint')}
|
|
language="http"
|
|
code="GET /v1/business/"
|
|
/>
|
|
<TabbedCodeBlock title={t('help.api.request')} codes={getServicesCode} />
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Service Object */}
|
|
<ApiSection id="service-object">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.serviceObject')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.serviceObjectDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.attributes')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: 'id', type: 'string', description: 'Unique identifier for the service.' },
|
|
{ name: 'name', type: 'string', description: 'The service name.' },
|
|
{ name: 'description', type: 'string', description: 'Service description.' },
|
|
{ name: 'duration', type: 'integer', description: 'Duration in minutes.' },
|
|
{ name: 'price', type: 'string', description: 'Price as a decimal string.' },
|
|
{ name: 'photos', type: 'array', description: 'URLs of service photos.' },
|
|
{ name: 'is_active', type: 'boolean', description: 'Whether the service is active.' },
|
|
]}
|
|
/>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title="THE SERVICE OBJECT"
|
|
language="json"
|
|
code={`{
|
|
"id": "svc_a1b2c3d4e5f6",
|
|
"name": "Haircut",
|
|
"description": "Professional haircut with wash and style",
|
|
"duration": 45,
|
|
"price": "35.00",
|
|
"photos": [
|
|
"https://cdn.smoothschedule.com/photos/haircut1.jpg"
|
|
],
|
|
"is_active": true
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* List Services */}
|
|
<ApiSection id="list-services">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-mono">
|
|
GET
|
|
</span>
|
|
{t('help.api.listServices')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.listServicesDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.requiredScope')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
<code>services:read</code>
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.endpoint')}
|
|
language="http"
|
|
code="GET /v1/services/"
|
|
/>
|
|
<TabbedCodeBlock title={t('help.api.request')} codes={getServicesCode} />
|
|
<CodeBlock
|
|
title={t('help.api.response')}
|
|
language="json"
|
|
code={`[
|
|
{
|
|
"id": "svc_a1b2c3d4e5f6",
|
|
"name": "Haircut",
|
|
"description": "Professional haircut with wash and style",
|
|
"duration": 45,
|
|
"price": "35.00",
|
|
"is_active": true
|
|
},
|
|
{
|
|
"id": "svc_b2c3d4e5f6g7",
|
|
"name": "Color Treatment",
|
|
"description": "Full color treatment",
|
|
"duration": 120,
|
|
"price": "150.00",
|
|
"is_active": true
|
|
}
|
|
]`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Check Availability */}
|
|
<ApiSection id="check-availability">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-mono">
|
|
GET
|
|
</span>
|
|
{t('help.api.checkAvailability')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.checkAvailabilityDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.parameters')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: 'service_id', type: 'string', description: 'The service to check availability for.', required: true },
|
|
{ name: 'date', type: 'string', description: 'Start date in YYYY-MM-DD format.', required: true },
|
|
{ name: 'days', type: 'integer', description: 'Number of days to check (1-30). Default: 7' },
|
|
{ name: 'resource_id', type: 'string', description: 'Filter by specific resource.' },
|
|
]}
|
|
/>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.requiredScope')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
<code>availability:read</code>
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.endpoint')}
|
|
language="http"
|
|
code="GET /v1/availability/"
|
|
/>
|
|
<TabbedCodeBlock title={t('help.api.request')} codes={getAvailabilityCode} />
|
|
<CodeBlock
|
|
title={t('help.api.response')}
|
|
language="json"
|
|
code={`{
|
|
"service": {
|
|
"id": "svc_a1b2c3d4e5f6",
|
|
"name": "Haircut",
|
|
"duration": 45
|
|
},
|
|
"date_range": {
|
|
"start": "2025-12-01",
|
|
"end": "2025-12-08"
|
|
},
|
|
"slots": [
|
|
{
|
|
"start_time": "2025-12-01T09:00:00Z",
|
|
"end_time": "2025-12-01T09:45:00Z",
|
|
"resource_id": "res_123",
|
|
"resource_name": "Jane Smith"
|
|
},
|
|
{
|
|
"start_time": "2025-12-01T10:00:00Z",
|
|
"end_time": "2025-12-01T10:45:00Z",
|
|
"resource_id": "res_123",
|
|
"resource_name": "Jane Smith"
|
|
}
|
|
]
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Create Appointment */}
|
|
<ApiSection id="create-appointment">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-mono">
|
|
POST
|
|
</span>
|
|
{t('help.api.createAppointment')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.createAppointmentDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.parameters')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: 'service_id', type: 'string', description: 'ID of the service being booked.', required: true },
|
|
{ name: 'resource_id', type: 'string', description: 'ID of the resource (staff/room).', required: true },
|
|
{ name: 'start_time', type: 'string', description: 'ISO 8601 datetime for the appointment start.', required: true },
|
|
{ name: 'customer_email', type: 'string', description: 'Customer email address.', required: true },
|
|
{ name: 'customer_name', type: 'string', description: 'Customer full name.', required: true },
|
|
{ name: 'customer_phone', type: 'string', description: 'Customer phone number.' },
|
|
{ name: 'notes', type: 'string', description: 'Additional notes for the appointment.' },
|
|
]}
|
|
/>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.requiredScope')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
<code>bookings:write</code>
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.endpoint')}
|
|
language="http"
|
|
code="POST /v1/appointments/"
|
|
/>
|
|
<TabbedCodeBlock title={t('help.api.request')} codes={createAppointmentCode} />
|
|
<CodeBlock
|
|
title={t('help.api.response')}
|
|
language="json"
|
|
code={`{
|
|
"id": "apt_a1b2c3d4e5f6",
|
|
"service": {
|
|
"id": "svc_123",
|
|
"name": "Haircut"
|
|
},
|
|
"resource": {
|
|
"id": "res_456",
|
|
"name": "Jane Smith"
|
|
},
|
|
"customer": {
|
|
"id": "cust_789",
|
|
"name": "John Doe",
|
|
"email": "john@example.com"
|
|
},
|
|
"start_time": "2025-12-01T10:00:00Z",
|
|
"end_time": "2025-12-01T10:45:00Z",
|
|
"status": "scheduled",
|
|
"created_at": "2025-11-28T15:30:00Z"
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Webhook Events */}
|
|
<ApiSection id="webhook-events">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.webhookEvents')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.webhookEventsDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.eventTypes')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: 'appointment.created', type: 'event', description: 'Fired when a new appointment is booked.' },
|
|
{ name: 'appointment.updated', type: 'event', description: 'Fired when an appointment is modified.' },
|
|
{ name: 'appointment.cancelled', type: 'event', description: 'Fired when an appointment is cancelled.' },
|
|
{ name: 'appointment.completed', type: 'event', description: 'Fired when an appointment is marked complete.' },
|
|
{ name: 'customer.created', type: 'event', description: 'Fired when a new customer is created.' },
|
|
{ name: 'customer.updated', type: 'event', description: 'Fired when customer info is updated.' },
|
|
]}
|
|
/>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.webhookPayload')}
|
|
language="json"
|
|
code={`{
|
|
"id": "evt_a1b2c3d4e5f6",
|
|
"type": "appointment.created",
|
|
"created_at": "2025-11-28T15:30:00Z",
|
|
"data": {
|
|
"id": "apt_a1b2c3d4e5f6",
|
|
"service": {
|
|
"id": "svc_123",
|
|
"name": "Haircut"
|
|
},
|
|
"customer": {
|
|
"id": "cust_789",
|
|
"name": "John Doe"
|
|
},
|
|
"start_time": "2025-12-01T10:00:00Z",
|
|
"status": "scheduled"
|
|
}
|
|
}`}
|
|
/>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Create Webhook */}
|
|
<ApiSection id="create-webhook">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0 flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-mono">
|
|
POST
|
|
</span>
|
|
{t('help.api.createWebhook')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.createWebhookDescription')}
|
|
<strong>{t('help.api.secretOnlyOnce')}</strong>
|
|
{t('help.api.secretOnlyOnceDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.parameters')}
|
|
</h3>
|
|
<AttributeTable
|
|
attributes={[
|
|
{ name: 'url', type: 'string', description: 'The HTTPS URL to receive webhook payloads.', required: true },
|
|
{ name: 'events', type: 'array', description: 'List of event types to subscribe to.', required: true },
|
|
{ name: 'description', type: 'string', description: 'Optional description for the webhook.' },
|
|
]}
|
|
/>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.requiredScope')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
<code>webhooks:manage</code>
|
|
</p>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title={t('help.api.endpoint')}
|
|
language="http"
|
|
code="POST /v1/webhooks/"
|
|
/>
|
|
<TabbedCodeBlock title={t('help.api.request')} codes={createWebhookCode} />
|
|
<CodeBlock
|
|
title={t('help.api.response')}
|
|
language="json"
|
|
code={`{
|
|
"id": "whk_a1b2c3d4e5f6",
|
|
"url": "https://example.com/webhooks",
|
|
"events": [
|
|
"appointment.created",
|
|
"appointment.cancelled"
|
|
],
|
|
"secret": "${TEST_WEBHOOK_SECRET}",
|
|
"is_active": true,
|
|
"created_at": "2025-11-28T15:30:00Z"
|
|
}`}
|
|
/>
|
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
|
<strong>{t('help.api.saveYourSecret')}</strong> {t('help.api.saveYourSecretDescription')}
|
|
</p>
|
|
</div>
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Verify Signatures */}
|
|
<ApiSection id="verify-signatures">
|
|
<ApiContent>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mt-0">
|
|
{t('help.api.verifySignatures')}
|
|
</h2>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.verifySignaturesDescription')}
|
|
</p>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.signatureFormat')}
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
{t('help.api.signatureFormatDescription')}
|
|
</p>
|
|
<pre className="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg text-sm overflow-x-auto">
|
|
<code><timestamp>.<signature></code>
|
|
</pre>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t('help.api.verificationSteps')}
|
|
</h3>
|
|
<ol className="text-gray-600 dark:text-gray-300 list-decimal list-inside space-y-2">
|
|
<li>{t('help.api.verificationStep1')}</li>
|
|
<li>{t('help.api.verificationStep2')}</li>
|
|
<li>{t('help.api.verificationStep3')}</li>
|
|
<li>{t('help.api.verificationStep4')}</li>
|
|
</ol>
|
|
</ApiContent>
|
|
<ApiExample>
|
|
<CodeBlock
|
|
title="SIGNATURE HEADER"
|
|
language="http"
|
|
code={`X-Webhook-Signature: 1732795200.a1b2c3d4e5f6...`}
|
|
/>
|
|
<TabbedCodeBlock title="VERIFICATION" codes={verifyWebhookCode} />
|
|
</ApiExample>
|
|
</ApiSection>
|
|
|
|
{/* Footer padding */}
|
|
<div className="h-24" />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</LanguageContext.Provider>
|
|
);
|
|
};
|
|
|
|
export default HelpApiDocs;
|