Skip to main content

Infrastructure

Api Documentation code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/api-documentation.blade.php

@props([
    'baseUrl' => 'https://api.harbour.test',
    'version' => '2026-06-14',
    'endpoints' => [],
    'webhooks' => [],
])

@php
    $endpoints = $endpoints ?: [
        [
            'method' => 'GET',
            'path' => '/v1/posts',
            'model' => 'Posts',
            'title' => 'List posts',
            'description' => 'Returns published and draft blog posts for the authenticated workspace.',
            'scope' => 'posts:read',
            'response' => '200 OK',
            'responseExample' => <<<'JSON'
{
  "data": [
    {
      "id": "pst_01HX9",
      "title": "Designing a calmer changelog",
      "slug": "designing-a-calmer-changelog",
      "status": "published",
      "published_at": "2026-06-14T03:12:00Z"
    }
  ]
}
JSON,
            'schema' => [
                ['key' => 'data', 'source' => 'Response', 'type' => 'array<object>'],
                ['key' => 'data[].id', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data[].title', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data[].slug', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data[].status', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data[].published_at', 'source' => 'Response', 'type' => 'string<date-time>'],
            ],
        ],
        [
            'method' => 'POST',
            'path' => '/v1/posts',
            'model' => 'Posts',
            'title' => 'Create post',
            'description' => 'Creates a draft blog post and returns the editable resource.',
            'scope' => 'posts:write',
            'response' => '201 Created',
            'requestExample' => <<<'JSON'
{
  "title": "Shipping better release notes",
  "slug": "shipping-better-release-notes",
  "excerpt": "A short guide to writing product updates readers can scan.",
  "body": "Start with the user impact, then include the operational detail.",
  "status": "draft",
  "author_id": "usr_01H8R",
  "tags": ["release-notes", "product"]
}
JSON,
            'responseExample' => <<<'JSON'
{
  "data": {
    "id": "pst_01HYA",
    "title": "Shipping better release notes",
    "slug": "shipping-better-release-notes",
    "status": "draft",
    "edit_url": "https://harbour.test/posts/shipping-better-release-notes/edit"
  }
}
JSON,
            'schema' => [
                ['key' => 'title', 'source' => 'Request', 'type' => 'string', 'required' => true],
                ['key' => 'slug', 'source' => 'Request', 'type' => 'string', 'required' => true],
                ['key' => 'excerpt', 'source' => 'Request', 'type' => 'string', 'required' => false],
                ['key' => 'body', 'source' => 'Request', 'type' => 'string', 'required' => true],
                ['key' => 'status', 'source' => 'Request', 'type' => 'string', 'required' => false],
                ['key' => 'author_id', 'source' => 'Request', 'type' => 'string', 'required' => true],
                ['key' => 'tags', 'source' => 'Request', 'type' => 'array<string>', 'required' => false],
                ['key' => 'data', 'source' => 'Response', 'type' => 'object'],
                ['key' => 'data.id', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.title', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.slug', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.status', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.edit_url', 'source' => 'Response', 'type' => 'url'],
            ],
        ],
        [
            'method' => 'PATCH',
            'path' => '/v1/posts/{post}',
            'model' => 'Posts',
            'title' => 'Update post',
            'description' => 'Updates blog post metadata, editorial status, or scheduled publish time.',
            'scope' => 'posts:write',
            'response' => '200 OK',
            'requestExample' => <<<'JSON'
{
  "title": "Shipping better release notes",
  "status": "scheduled",
  "published_at": "2026-06-20T09:00:00Z"
}
JSON,
            'responseExample' => <<<'JSON'
{
  "data": {
    "id": "pst_01HYA",
    "title": "Shipping better release notes",
    "status": "scheduled",
    "published_at": "2026-06-20T09:00:00Z"
  }
}
JSON,
            'schema' => [
                ['key' => 'title', 'source' => 'Request', 'type' => 'string', 'required' => false],
                ['key' => 'status', 'source' => 'Request', 'type' => 'string', 'required' => false],
                ['key' => 'published_at', 'source' => 'Request', 'type' => 'string<date-time>', 'required' => false],
                ['key' => 'data', 'source' => 'Response', 'type' => 'object'],
                ['key' => 'data.id', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.title', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.status', 'source' => 'Response', 'type' => 'string'],
                ['key' => 'data.published_at', 'source' => 'Response', 'type' => 'string<date-time>'],
            ],
        ],
        [
            'method' => 'DELETE',
            'path' => '/v1/posts/{post}',
            'model' => 'Posts',
            'title' => 'Delete post',
            'description' => 'Deletes a draft blog post or archives a published post for audit history.',
            'scope' => 'posts:write',
            'response' => '204 No Content',
            'responseExample' => <<<'TEXT'
HTTP/1.1 204 No Content

No response body is returned for a successful post deletion.
TEXT,
        ],
    ];

    $webhooks = $webhooks ?: [
        ['event' => 'post.published', 'description' => 'A blog post moved from draft or scheduled into the published feed.'],
        ['event' => 'post.updated', 'description' => 'A title, body, tag, or editorial status changed.'],
        ['event' => 'post.deleted', 'description' => 'A draft post was deleted or a published post was archived.'],
    ];

    $endpoints = collect($endpoints)
        ->map(function (array $endpoint, int $index): array {
            $endpoint['key'] ??= strtolower($endpoint['method']).'-'.trim((string) preg_replace('/[^a-z0-9]+/i', '-', $endpoint['path']), '-').'-'.$index;

            return $endpoint;
        })
        ->values()
        ->all();

    $methodVariants = [
        'GET' => 'green',
        'POST' => 'blue',
        'PATCH' => 'amber',
        'DELETE' => 'red',
    ];

    $navigation = [
        ['href' => '#authentication', 'label' => 'Authentication', 'description' => 'Bearer token headers'],
        ['href' => '#endpoints', 'label' => 'Endpoints', 'description' => count($endpoints).' REST operations'],
        ['href' => '#webhooks', 'label' => 'Webhooks', 'description' => count($webhooks).' event types'],
    ];

    $codeExampleClasses = 'overflow-x-auto px-4 py-4 font-mono text-xs leading-6 text-gray-700 dark:text-gray-300 '
        .'[&_.code-line]:block [&_.code-line]:whitespace-pre '
        .'[&_.tok-key]:text-brand-700 dark:[&_.tok-key]:text-brand-300 '
        .'[&_.tok-str]:text-green-700 dark:[&_.tok-str]:text-green-300 '
        .'[&_.tok-com]:text-gray-400 dark:[&_.tok-com]:text-gray-500 [&_.tok-com]:italic '
        .'[&_.tok-num]:text-amber-600 dark:[&_.tok-num]:text-amber-200 '
        .'[&_.tok-bool]:text-accent-700 dark:[&_.tok-bool]:text-accent-300 '
        .'[&_.tok-punc]:text-gray-400 dark:[&_.tok-punc]:text-gray-500';

    $highlightCodeLine = function (string $line): string {
        $escaped = e($line);

        if ($escaped === '') {
            return '&nbsp;';
        }

        if (str_starts_with($escaped, 'HTTP/') || str_starts_with($escaped, 'No response body')) {
            return '<span class="tok-com">'.$escaped.'</span>';
        }

        $escaped = preg_replace('/(:\s*)(&quot;[^&]+&quot;)/', '$1<span class="tok-str">$2</span>', $escaped);
        $escaped = preg_replace('/(:\s*)(\d+(?:\.\d+)?)/', '$1<span class="tok-num">$2</span>', $escaped);
        $escaped = preg_replace('/(:\s*)(true|false|null)\b/', '$1<span class="tok-bool">$2</span>', $escaped);
        $escaped = preg_replace('/(&quot;[^&]+&quot;)(\s*:)/', '<span class="tok-key">$1</span><span class="tok-punc">$2</span>', $escaped);

        return $escaped;
    };
@endphp

<div {{ $attributes->merge(['class' => 'grid gap-8 lg:grid-cols-[16rem_minmax(0,1fr)]']) }}>
    <aside class="lg:sticky lg:top-24 lg:self-start">
        <nav aria-label="API documentation sections" class="overflow-hidden rounded-2xl border border-gray-200 bg-white text-sm shadow-sm ring-1 ring-gray-900/5 dark:border-gray-800 dark:bg-gray-900 dark:ring-white/5">
            <div class="border-b border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-950/40">
                <div class="flex items-start justify-between gap-3">
                    <div>
                        <p class="text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">API reference</p>
                        <h2 class="mt-2 text-base font-semibold tracking-tight text-gray-900 dark:text-white">Harbour API</h2>
                    </div>
                </div>

                <div class="mt-4 rounded-xl border border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900">
                    <p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Base URL</p>
                    <p class="mt-1 break-all font-mono text-xs font-semibold text-gray-800 dark:text-gray-100">{{ $baseUrl }}</p>
                </div>
            </div>

            <div class="p-3">
                <p class="px-2 pb-2 text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">Sections</p>

                <div class="space-y-1">
                    @foreach ($navigation as $item)
                        <a href="{{ $item['href'] }}" class="group flex items-start gap-3 rounded-xl px-3 py-2.5 transition hover:bg-brand-50 dark:hover:bg-brand-950/30">
                            <span class="min-w-0">
                                <span class="block font-semibold text-gray-800 transition group-hover:text-brand-800 dark:text-gray-200 dark:group-hover:text-brand-200">{{ $item['label'] }}</span>
                                <span class="mt-0.5 block text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $item['description'] }}</span>
                            </span>
                        </a>
                    @endforeach
                </div>
            </div>

            <div class="border-t border-gray-200 p-4 dark:border-gray-800">
                <div class="grid grid-cols-2 gap-3">
                    <div>
                        <p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Version</p>
                        <p class="mt-1 font-mono text-xs font-semibold text-gray-900 dark:text-white">{{ $version }}</p>
                    </div>
                    <div>
                        <p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Limit</p>
                        <p class="mt-1 font-mono text-xs font-semibold text-gray-900 dark:text-white">600/min</p>
                    </div>
                    <div class="col-span-2">
                        <p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Format</p>
                        <p class="mt-1 font-mono text-xs font-semibold text-gray-900 dark:text-white">application/json</p>
                    </div>
                </div>

                <div class="mt-4 rounded-xl bg-gray-50 p-3 dark:bg-gray-950/50">
                    <p class="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-300">
                        <x-ui.icon name="shield-check" size="sm" weight="fill" class="text-brand-500 dark:text-brand-400" />
                        Token scopes required
                    </p>
                    <p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">Read and write scopes are shown beside every endpoint.</p>
                </div>
            </div>
        </nav>

        <div class="mt-4 rounded-2xl border border-gray-200 bg-white p-4 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900">
            <p class="text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">Quick links</p>
            <div class="mt-3 space-y-2">
                @foreach ([
                    ['href' => '#authentication', 'label' => 'Copy auth example', 'icon' => 'copy'],
                    ['href' => '#webhooks', 'label' => 'Review signatures', 'icon' => 'signature'],
                ] as $link)
                    <a href="{{ $link['href'] }}" class="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-gray-600 transition hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white">
                        <x-ui.icon :name="$link['icon']" size="sm" />
                        {{ $link['label'] }}
                    </a>
                @endforeach
            </div>
        </div>
    </aside>

    <div class="min-w-0 space-y-10">
        <section id="authentication" class="scroll-mt-24 space-y-5">
            <div>
                <h2 class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">Authenticate every request with a workspace token</h2>
                <p class="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
                    Tokens are scoped by workspace and permission. Send the token in the Authorization header and rotate it from the workspace security screen.
                </p>
            </div>

            <x-ui.code-window :tabs="['curl' => 'Request', 'json' => 'Response']" default="curl" lang="http">
                <x-slot:curl>
<div class="code-line"><span class="tok-fn">curl</span> <span class="tok-punc">{{ $baseUrl }}</span><span class="tok-str">/v1/posts</span> \</div>
<div class="code-line">  <span class="tok-key">-H</span> <span class="tok-str">"Authorization: Bearer hbr_live_****"</span> \</div>
<div class="code-line">  <span class="tok-key">-H</span> <span class="tok-str">"Accept: application/json"</span></div>
                </x-slot:curl>
                <x-slot:json>
<div class="code-line"><span class="tok-punc">{</span></div>
<div class="code-line">  <span class="tok-key">"data"</span><span class="tok-punc">:</span> <span class="tok-punc">[</span></div>
<div class="code-line">    <span class="tok-punc">{</span><span class="tok-key">"id"</span><span class="tok-punc">:</span> <span class="tok-str">"pst_01HX9"</span><span class="tok-punc">,</span> <span class="tok-key">"title"</span><span class="tok-punc">:</span> <span class="tok-str">"Designing a calmer changelog"</span><span class="tok-punc">,</span> <span class="tok-key">"status"</span><span class="tok-punc">:</span> <span class="tok-str">"published"</span><span class="tok-punc">}</span></div>
<div class="code-line">  <span class="tok-punc">]</span></div>
<div class="code-line"><span class="tok-punc">}</span></div>
                </x-slot:json>
            </x-ui.code-window>
        </section>

        <section id="endpoints" class="scroll-mt-24 space-y-5">
            <div>
                <h2 class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">Core endpoints</h2>
                <p class="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
                    The mock API is organized around blog post publishing, editorial updates, and post lifecycle events.
                </p>
            </div>

            <div
                x-data="{ activeEndpoint: @js($endpoints[0]['key'] ?? '') }"
                data-api-endpoint-tabs
                class="space-y-5"
            >
                <div class="overflow-x-auto rounded-2xl border border-gray-200 bg-white p-2 shadow-sm dark:border-gray-800 dark:bg-gray-900" role="tablist" aria-label="API endpoint examples">
                    <div class="grid min-w-full grid-cols-2 gap-2 md:grid-cols-4">
                        @foreach ($endpoints as $endpoint)
                            <button
                                type="button"
                                role="tab"
                                data-endpoint-tab="{{ $endpoint['key'] }}"
                                x-on:click="activeEndpoint = @js($endpoint['key'])"
                                x-on:keydown.enter.prevent="activeEndpoint = @js($endpoint['key'])"
                                x-on:keydown.space.prevent="activeEndpoint = @js($endpoint['key'])"
                                :aria-selected="activeEndpoint === @js($endpoint['key'])"
                                :class="activeEndpoint === @js($endpoint['key']) ? 'border-brand-300 bg-brand-50 text-gray-900 shadow-sm dark:border-brand-800 dark:bg-brand-950/40 dark:text-white' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'"
                                class="flex min-h-14 items-center gap-2 rounded-xl border px-3 py-3 text-left transition cursor-pointer"
                            >
                                <x-ui.badge :variant="$methodVariants[$endpoint['method']] ?? 'gray'" size="sm" :pill="false">{{ $endpoint['method'] }}</x-ui.badge>
                                <span class="text-sm font-semibold">{{ $endpoint['model'] ?? 'Posts' }}</span>
                            </button>
                        @endforeach
                    </div>
                </div>

                @foreach ($endpoints as $endpoint)
                    <article
                        x-show="activeEndpoint === @js($endpoint['key'])"
                        x-cloak
                        role="tabpanel"
                        data-endpoint-panel="{{ $endpoint['key'] }}"
                        class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
                    >
                        <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
                            <div class="min-w-0">
                                <div class="flex flex-wrap items-center gap-2">
                                    <x-ui.badge :variant="$methodVariants[$endpoint['method']] ?? 'gray'" :pill="false">{{ $endpoint['method'] }}</x-ui.badge>
                                    <code class="break-all rounded-md bg-gray-100 px-2 py-1 font-mono text-sm text-gray-800 dark:bg-gray-800 dark:text-gray-100">{{ $endpoint['path'] }}</code>
                                </div>
                                <h3 class="mt-4 text-base font-semibold text-gray-900 dark:text-white">{{ $endpoint['title'] }}</h3>
                                <p class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">{{ $endpoint['description'] }}</p>
                            </div>
                            <div class="flex shrink-0 flex-wrap gap-2 sm:justify-end">
                                <x-ui.badge variant="gray">{{ $endpoint['scope'] }}</x-ui.badge>
                                <x-ui.badge variant="outline">{{ $endpoint['response'] }}</x-ui.badge>
                            </div>
                        </div>

                        @if (! empty($endpoint['requestExample']))
                            <div class="mt-5 overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950/30">
                                <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-2.5 dark:border-gray-800">
                                    <div class="flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
                                        <x-ui.icon name="upload-simple" size="sm" />
                                        Request body
                                    </div>
                                    <span class="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">application/json</span>
                                </div>
                                <pre class="{{ $codeExampleClasses }}"><code>@foreach (explode("\n", $endpoint['requestExample']) as $line)<span class="code-line">{!! $highlightCodeLine($line) !!}</span>@endforeach</code></pre>
                            </div>
                        @endif

                        @if (! empty($endpoint['responseExample']))
                            <div class="{{ ! empty($endpoint['requestExample']) ? 'mt-3' : 'mt-5' }} overflow-hidden rounded-xl border border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950/50">
                                <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-2.5 dark:border-gray-800">
                                    <div class="flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
                                        <x-ui.icon name="file-code" size="sm" />
                                        Response example
                                    </div>
                                    <span class="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">{{ $endpoint['response'] }}</span>
                                </div>
                                <pre class="{{ $codeExampleClasses }}"><code>@foreach (explode("\n", $endpoint['responseExample']) as $line)<span class="code-line">{!! $highlightCodeLine($line) !!}</span>@endforeach</code></pre>
                            </div>
                        @endif

                        @if (! empty($endpoint['schema']))
                            <div class="{{ (! empty($endpoint['requestExample']) || ! empty($endpoint['responseExample'])) ? 'mt-3' : 'mt-5' }} overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950/30">
                                <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-2.5 dark:border-gray-800">
                                    <div class="flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
                                        <x-ui.icon name="list-checks" size="sm" />
                                        Field reference
                                    </div>
                                    <span class="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">schema</span>
                                </div>

                                <x-ui.table flush>
                                    <x-slot:head>
                                        <x-ui.table.heading>Key</x-ui.table.heading>
                                        <x-ui.table.heading>Location</x-ui.table.heading>
                                        <x-ui.table.heading>Data type</x-ui.table.heading>
                                        <x-ui.table.heading>Required</x-ui.table.heading>
                                    </x-slot:head>

                                    @foreach ($endpoint['schema'] as $field)
                                        <x-ui.table.row>
                                            <x-ui.table.cell>
                                                <code class="font-mono text-xs text-gray-900 dark:text-white">{{ $field['key'] }}</code>
                                            </x-ui.table.cell>
                                            <x-ui.table.cell>
                                                <x-ui.badge :variant="($field['source'] ?? null) === 'Request' ? 'brand' : 'outline'" size="sm" :pill="false">{{ $field['source'] ?? 'Response' }}</x-ui.badge>
                                            </x-ui.table.cell>
                                            <x-ui.table.cell>
                                                <code class="font-mono text-xs text-gray-600 dark:text-gray-300">{{ $field['type'] }}</code>
                                            </x-ui.table.cell>
                                            <x-ui.table.cell>
                                                @if (($field['source'] ?? null) === 'Request')
                                                    <x-ui.badge :variant="($field['required'] ?? false) ? 'green' : 'gray'" size="sm" :pill="false">
                                                        {{ ($field['required'] ?? false) ? 'Required' : 'Optional' }}
                                                    </x-ui.badge>
                                                @else
                                                    <span class="text-gray-400 dark:text-gray-500">-</span>
                                                @endif
                                            </x-ui.table.cell>
                                        </x-ui.table.row>
                                    @endforeach
                                </x-ui.table>
                            </div>
                        @endif
                    </article>
                @endforeach
            </div>
        </section>

        <section id="webhooks" class="scroll-mt-24 space-y-5">
            <div>
                <h2 class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">Subscribe to workspace events</h2>
                <p class="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
                    Webhooks are delivered with an HMAC signature in the Harbour-Signature header and retried for 24 hours.
                </p>
            </div>

            <x-ui.table>
                <x-slot:head>
                    <x-ui.table.heading>Event</x-ui.table.heading>
                    <x-ui.table.heading>Description</x-ui.table.heading>
                </x-slot:head>

                @foreach ($webhooks as $webhook)
                    <x-ui.table.row>
                        <x-ui.table.cell>
                            <code class="font-mono text-sm text-gray-900 dark:text-white">{{ $webhook['event'] }}</code>
                        </x-ui.table.cell>
                        <x-ui.table.cell muted>{{ $webhook['description'] }}</x-ui.table.cell>
                    </x-ui.table.row>
                @endforeach
            </x-ui.table>
        </section>
    </div>
</div>