api-client/packages/hoppscotch-common/src/components/collections/documentation/sections/Response.vue
Nivedin e63bfe3723
feat: API Documentation (#5499)
Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
2025-11-25 11:26:57 +05:30

247 lines
8.2 KiB
Vue

<template>
<div v-if="hasResponseExamples" class="max-w-2xl space-y-2">
<h2
class="text-sm font-semibold text-secondaryDark flex items-end px-4 p-2 border-b border-divider"
>
{{ t("documentation.response.title") }}
</h2>
<div
v-if="responseExamples && responseExamples.length > 0"
class="border border-divider"
>
<HoppSmartTabs
v-model="selectedResponseTab"
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary "
>
<HoppSmartTab
v-for="(example, index) in responseExamples"
:id="`response-${index}`"
:key="index"
:label="String(example.statusCode)"
class="flex h-full w-full flex-1 flex-col"
>
<div class="rounded-md overflow-hidden my-4">
<div class="px-4 py-2 border-b border-divider">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-sm text-secondary">
{{ example.name || "Untitled" }}
</span>
</div>
<HoppSmartItem
:icon="IconCopy"
:title="t('documentation.copy_response')"
@click="copyResponseExample(example)"
/>
</div>
</div>
<HoppSmartTabs
v-model="selectedContentTabs[index]"
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary"
>
<HoppSmartTab
v-if="example.body"
id="body"
:label="t('documentation.response_body')"
class="flex h-full w-full flex-1 flex-col"
>
<div class="p-4">
<div v-if="isJsonResponse(example)">
<pre
class="bg-primaryLight p-3 rounded my-2 overflow-auto max-h-64 text-sm font-mono text-secondaryLight"
>{{ formatJSON(example.body) }}</pre
>
</div>
<div v-else>
<pre
class="bg-primaryLight p-3 rounded my-2 overflow-auto max-h-64 text-sm font-mono text-secondaryLight"
>{{ example.body }}</pre
>
</div>
</div>
</HoppSmartTab>
<HoppSmartTab
v-if="example.headers && example.headers.length > 0"
id="headers"
:label="`${t('documentation.response_headers')} (${example.headers.length})`"
class="flex h-full w-full flex-1 flex-col"
>
<div class="p-4">
<table class="w-full border-collapse text-sm">
<thead class="bg-divider/20">
<tr>
<th
class="text-left py-2 px-3 font-semibold text-secondaryDark text-xs"
>
{{ t("documentation.key") }}
</th>
<th
class="text-left py-2 px-3 font-semibold text-secondaryDark text-xs"
>
{{ t("documentation.value") }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(header, headerIndex) in example.headers"
:key="headerIndex"
class="border-t border-divider"
>
<td class="py-2 px-3 text-accent text-xs">
{{ header.key }}
</td>
<td class="py-2 px-3 text-secondaryLight text-xs">
{{ header.value }}
</td>
</tr>
</tbody>
</table>
</div>
</HoppSmartTab>
<template #actions>
<div
v-if="example.statusCode"
class="flex items-center gap-2 px-4"
>
<span
class="px-1 py-.5 text-tiny rounded"
:class="getStatusCodeClass(example.statusCode)"
>
{{ example.statusCode }} -
{{ getStatusCodeReasonPhrase(example.statusCode) }}
</span>
</div>
</template>
</HoppSmartTabs>
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div>
<div v-else class="text-center py-8 text-secondaryLight">
<icon-lucide-file-text class="mx-auto mb-2" size="24" />
<p class="text-sm">{{ t("documentation.response.no_examples") }}</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue"
import IconCopy from "~icons/lucide/copy"
import { useToast } from "~/composables/toast"
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
interface ResponseExample {
name?: string
statusCode?: number
headers?: Array<{ key: string; value: string }>
body?: string
contentType?: string
}
const props = defineProps<{
responseExamples?: ResponseExample[] | null
}>()
const toast = useToast()
const selectedResponseTab = ref<string>("response-0")
const selectedContentTabs = ref<Record<number, string>>({})
// Initialize tabs when responseExamples change
watch(
() => props.responseExamples,
(newExamples) => {
if (newExamples && newExamples.length > 0) {
// Set default response tab to the first example
selectedResponseTab.value = "response-0"
// Initialize content tabs for each response example
const newSelectedTabs: Record<number, string> = {}
newExamples.forEach((example, index) => {
// Default to "body" tab if body exists, otherwise "headers"
newSelectedTabs[index] = example.body ? "body" : "headers"
})
selectedContentTabs.value = newSelectedTabs
}
},
{ immediate: true }
)
const hasResponseExamples = computed(() => {
return props.responseExamples && props.responseExamples.length > 0
})
/**
* Returns the appropriate CSS class for styling the status code badge
* @param statusCode The HTTP status code
* @returns CSS class string for the status code badge
*/
function getStatusCodeClass(statusCode: number): string {
if (statusCode >= 200 && statusCode < 300) {
return "bg-green-500/10 text-green-500 "
} else if (statusCode >= 300 && statusCode < 400) {
return "bg-yellow-500/10 text-yellow-500"
} else if (statusCode >= 400 && statusCode < 500) {
return "bg-orange-500/10 text-orange-500"
} else if (statusCode >= 500) {
return "bg-red-500/10 text-red-500"
}
return "bg-secondaryLight/20 text-secondaryLight"
}
/**
* Check if the response is JSON based on content type or body structure
* @param example Response example
* @returns Boolean indicating if response is JSON
*/
function isJsonResponse(example: ResponseExample): boolean {
if (example.contentType?.includes("application/json")) {
return true
}
// Try to parse as JSON to determine if it's valid JSON
try {
JSON.parse(example.body || "")
return true
} catch (e) {
return false
}
}
/**
* Format JSON string for display
* @param jsonString String to format
* @returns Formatted JSON string
*/
function formatJSON(jsonString: string): string {
try {
const parsed = JSON.parse(jsonString || "{}")
return JSON.stringify(parsed, null, 2)
} catch (e) {
return jsonString || ""
}
}
/**
* Copy response example to clipboard
* @param example Response example to copy
*/
async function copyResponseExample(example: ResponseExample): Promise<void> {
try {
const responseText = example.body || ""
await navigator.clipboard.writeText(responseText)
toast.success(t("documentation.response.example_copied"))
} catch (err) {
console.error("Failed to copy response example: ", err)
toast.error(t("documentation.response.example_copy_failed"))
}
}
</script>