feat: internal api docs page

This commit is contained in:
5rahim
2024-10-31 15:30:20 +00:00
parent 3600816742
commit 5d8aff4021
13 changed files with 620 additions and 140 deletions

View File

@@ -1,11 +1,17 @@
# Contribution Guide
All contributions are welcome. If you're not sure about something, feel free to ask.
All contributions are welcome _if_ they are in the scope of the project. If you're not sure about something, feel free to ask.
## Guidelines
Note that you should try and make your changes against the **most active branch**, which is usually the `main` branch but
may be different when a new minor version is being developed.
- Make sure you are familiar with Go and React.
- Your contributions must be small and focused. If you want to add a new feature that requires substantial changes or additions to the codebase, please contact the dev first.
- Make sure your changes are in line with the project's goals (Create a feature request if you're unsure).
- Make sure your changes are well tested and do not introduce any new issues or regressions.
- You should try and make your changes against the **most active branch**, which is usually the `main` branch but
may be different when a new version is being developed.
## How to contribute
1. Create an issue before starting work on a feature or a bug fix.
2. Fork the repository, clone it, and create a new branch.

View File

@@ -47,13 +47,13 @@ Note that the web interface should be built first before building the server.
# Development
To get started, you will need to be familiar with Go and React.
1. To get started, you **must be familiar with Go and React**.
I recommend creating a dummy AniList account for testing purposes.
2. I recommend creating a dummy AniList account to use for testing. This will prevent the tests from affecting your actual account.
## Server
1. Choose a data directory for testing.
1. Create/choose a dummy data directory for testing.
This will prevent the server from writing to your actual data directory.
2. Create a dummy `web` folder if you want to develop the web interface too or build the web interface first and move the contents to a `web` directory at the root of the project before running the server.
@@ -85,20 +85,24 @@ Either way, a `web` directory should be present at the root of the project.
During development, the web interface is served by the Next.js development server instead of the Go server,
leading to different ports.
## Codegen
## Handlers & Codegen
1. All routes are declared in `internal/handlers/routes.go` where a `handler` method is passed to each route.
2. Route handlers are defined in `internal/handlers`.
> The following points are very important for understanding the codebase.
- All routes are declared in `internal/handlers/routes.go` where a `handler` method is passed to each route.
- Route handlers are defined in `internal/handlers`.
- The comments above each route handler declaration is a form of documentation similar to OpenAPI
- These comments allow the internal code generator (`codegen/main.go`) to generate endpoint objects & types for the client.
- Types for the frontend are auto-generated in `seanime-web/api/generated/types.ts` based on the `@returns` directive and request body variables of each route handler. The code generator will analyze the entire Go codebase and convert _returned_ public structs into Typescript types.
- Types for the frontend are auto-generated in `seanime-web/api/generated/types.ts`, `seanime-web/api/generated/endpoint.types.ts`, `seanime-web/api/generated/hooks_template.ts` based on the comments above route handlers and all public Go structs.
Run the `go generate` command at the top of `codegen/main.go` to generate the necessary types for the frontend.
This should be done after making changes to the route handlers or structs returned by the route handlers.
This should be done **each time you make changes** to the **route handlers** or **structs** that are used in the frontend.
## AniList GraphQL API
> The following points are for understanding the AniList API integration.
Anilist queries are defined in `internal/anilist/queries/*.graphql` and generated using `gqlgenc`.
Run this when you make changes to the GraphQL schema.
@@ -119,11 +123,11 @@ cd ../../..
go mod tidy
```
`gqlgenc` will generate the different queries, mutations and structs associated with the AniList API and the queries we defined.
These are located in `internal/api/anilist/clieng_gen.go`.
- `gqlgenc` will generate the different queries, mutations and structs associated with the AniList API and the queries we defined.
These are located in `internal/api/anilist/client_gen.go`.
In `internal/api/anilist/client.go`, we manually reimplements the different queries and mutations into a `ClientWrapper` struct and a `MockClientWrapper` for testing.
- In `internal/api/anilist/client.go`, we manually reimplements the different queries and mutations into a `ClientWrapper` struct and a `MockClientWrapper` for testing.
This is done to avoid using the generated code directly in the business logic. It also allows us to mock the AniList API for testing.
## Tests

View File

@@ -2327,7 +2327,7 @@
"",
"\t@summary returns the API documentation",
"\t@route /api/v1/internal/docs [GET]",
"\t@returns docs.Docs",
"\t@returns []handlers.ApiDocsGroup",
""
],
"filepath": "internal/handlers/docs.go",
@@ -2341,9 +2341,9 @@
],
"params": [],
"bodyFields": [],
"returns": "docs.Docs",
"returnGoType": "docs.Docs",
"returnTypescriptType": "INTERNAL_Docs"
"returns": "[]handlers.ApiDocsGroup",
"returnGoType": "handlers.ApiDocsGroup",
"returnTypescriptType": "Array\u003cApiDocsGroup\u003e"
}
},
{
@@ -2956,7 +2956,7 @@
"\t@summary returns the total size of cache files.",
"\t@desc The total size of the cache files is returned in human-readable format.",
"\t@route /api/v1/filecache/total-size [GET]",
"\t@returns bool",
"\t@returns string",
""
],
"filepath": "internal/handlers/filecache.go",
@@ -2972,9 +2972,9 @@
],
"params": [],
"bodyFields": [],
"returns": "bool",
"returnGoType": "bool",
"returnTypescriptType": "boolean"
"returns": "string",
"returnGoType": "string",
"returnTypescriptType": "string"
}
},
{
@@ -3028,7 +3028,7 @@
"\t@summary returns the total size of cached video file data.",
"\t@desc The total size of the cache video file data is returned in human-readable format.",
"\t@route /api/v1/filecache/mediastream/videofiles/total-size [GET]",
"\t@returns bool",
"\t@returns string",
""
],
"filepath": "internal/handlers/filecache.go",
@@ -3044,9 +3044,9 @@
],
"params": [],
"bodyFields": [],
"returns": "bool",
"returnGoType": "bool",
"returnTypescriptType": "boolean"
"returns": "string",
"returnGoType": "string",
"returnTypescriptType": "string"
}
},
{

View File

@@ -28290,6 +28290,16 @@
"public": false,
"comments": []
},
{
"name": "torrentRepository",
"jsonName": "torrentRepository",
"goType": "torrent.Repository",
"typescriptType": "Torrent_Repository",
"usedStructName": "torrent.Repository",
"required": false,
"public": false,
"comments": []
},
{
"name": "playbackManager",
"jsonName": "playbackManager",
@@ -28380,6 +28390,16 @@
"public": true,
"comments": []
},
{
"name": "TorrentRepository",
"jsonName": "TorrentRepository",
"goType": "torrent.Repository",
"typescriptType": "Torrent_Repository",
"usedStructName": "torrent.Repository",
"required": false,
"public": true,
"comments": []
},
{
"name": "PlaybackManager",
"jsonName": "PlaybackManager",
@@ -28614,6 +28634,15 @@
"required": true,
"public": true,
"comments": []
},
{
"name": "AutoSelect",
"jsonName": "AutoSelect",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
}
],
"comments": []
@@ -29014,6 +29043,17 @@
"formattedName": "Debrid_TorrentInfo",
"package": "debrid",
"fields": [
{
"name": "ID",
"jsonName": "id",
"goType": "string",
"typescriptType": "string",
"required": false,
"public": true,
"comments": [
" ID of the torrent if added to the debrid service"
]
},
{
"name": "Name",
"jsonName": "name",
@@ -33271,6 +33311,281 @@
],
"comments": []
},
{
"filepath": "../internal/handlers/docs.go",
"filename": "docs.go",
"name": "ApiDocsGroup",
"formattedName": "ApiDocsGroup",
"package": "handlers",
"fields": [
{
"name": "Filename",
"jsonName": "filename",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Name",
"jsonName": "name",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Handlers",
"jsonName": "handlers",
"goType": "[]RouteHandler",
"typescriptType": "Array\u003cRouteHandler\u003e",
"usedStructName": "handlers.RouteHandler",
"required": false,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/handlers/docs.go",
"filename": "docs.go",
"name": "RouteHandler",
"formattedName": "RouteHandler",
"package": "handlers",
"fields": [
{
"name": "Name",
"jsonName": "name",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "TrimmedName",
"jsonName": "trimmedName",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Comments",
"jsonName": "comments",
"goType": "[]string",
"typescriptType": "Array\u003cstring\u003e",
"required": false,
"public": true,
"comments": []
},
{
"name": "Filepath",
"jsonName": "filepath",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Filename",
"jsonName": "filename",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Api",
"jsonName": "api",
"goType": "RouteHandlerApi",
"typescriptType": "RouteHandlerApi",
"usedStructName": "handlers.RouteHandlerApi",
"required": false,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/handlers/docs.go",
"filename": "docs.go",
"name": "RouteHandlerApi",
"formattedName": "RouteHandlerApi",
"package": "handlers",
"fields": [
{
"name": "Summary",
"jsonName": "summary",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Descriptions",
"jsonName": "descriptions",
"goType": "[]string",
"typescriptType": "Array\u003cstring\u003e",
"required": false,
"public": true,
"comments": []
},
{
"name": "Endpoint",
"jsonName": "endpoint",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "Methods",
"jsonName": "methods",
"goType": "[]string",
"typescriptType": "Array\u003cstring\u003e",
"required": false,
"public": true,
"comments": []
},
{
"name": "Params",
"jsonName": "params",
"goType": "[]RouteHandlerParam",
"typescriptType": "Array\u003cRouteHandlerParam\u003e",
"usedStructName": "handlers.RouteHandlerParam",
"required": false,
"public": true,
"comments": []
},
{
"name": "BodyFields",
"jsonName": "bodyFields",
"goType": "[]RouteHandlerParam",
"typescriptType": "Array\u003cRouteHandlerParam\u003e",
"usedStructName": "handlers.RouteHandlerParam",
"required": false,
"public": true,
"comments": []
},
{
"name": "Returns",
"jsonName": "returns",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "ReturnGoType",
"jsonName": "returnGoType",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "ReturnTypescriptType",
"jsonName": "returnTypescriptType",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/handlers/docs.go",
"filename": "docs.go",
"name": "RouteHandlerParam",
"formattedName": "RouteHandlerParam",
"package": "handlers",
"fields": [
{
"name": "Name",
"jsonName": "name",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "JsonName",
"jsonName": "jsonName",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": []
},
{
"name": "GoType",
"jsonName": "goType",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": [
" e.g., []models.User"
]
},
{
"name": "UsedStructType",
"jsonName": "usedStructType",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": [
" e.g., models.User"
]
},
{
"name": "TypescriptType",
"jsonName": "typescriptType",
"goType": "string",
"typescriptType": "string",
"required": true,
"public": true,
"comments": [
" e.g., Array\u003cUser\u003e"
]
},
{
"name": "Required",
"jsonName": "required",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
},
{
"name": "Descriptions",
"jsonName": "descriptions",
"goType": "[]string",
"typescriptType": "Array\u003cstring\u003e",
"required": false,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/handlers/download.go",
"filename": "download.go",

View File

@@ -21,9 +21,9 @@ var additionalStructNamesForEndpoints = []string{
"vendor_hibike_torrent.AnimeTorrent",
}
func GenerateTypescriptEndpointsFile(docsPath string, structsPath string, outDir string) []string {
handlers := LoadHandlers(docsPath)
structs := LoadPublicStructs(structsPath)
func GenerateTypescriptEndpointsFile(handlersJsonPath string, structsJsonPath string, outDir string) []string {
handlers := LoadHandlers(handlersJsonPath)
structs := LoadPublicStructs(structsJsonPath)
_ = os.MkdirAll(outDir, os.ModePerm)
f, err := os.Create(filepath.Join(outDir, typescriptEndpointsFileName))

View File

@@ -1,27 +1,96 @@
package handlers
import (
"github.com/goccy/go-json"
"os"
"path/filepath"
"strings"
)
type (
ApiDocsGroup struct {
Filename string `json:"filename"`
Name string `json:"name"`
Handlers []*RouteHandler `json:"handlers"`
}
RouteHandler struct {
Name string `json:"name"`
TrimmedName string `json:"trimmedName"`
Comments []string `json:"comments"`
Filepath string `json:"filepath"`
Filename string `json:"filename"`
Api *RouteHandlerApi `json:"api"`
}
RouteHandlerApi struct {
Summary string `json:"summary"`
Descriptions []string `json:"descriptions"`
Endpoint string `json:"endpoint"`
Methods []string `json:"methods"`
Params []*RouteHandlerParam `json:"params"`
BodyFields []*RouteHandlerParam `json:"bodyFields"`
Returns string `json:"returns"`
ReturnGoType string `json:"returnGoType"`
ReturnTypescriptType string `json:"returnTypescriptType"`
}
RouteHandlerParam struct {
Name string `json:"name"`
JsonName string `json:"jsonName"`
GoType string `json:"goType"` // e.g., []models.User
UsedStructType string `json:"usedStructType"` // e.g., models.User
TypescriptType string `json:"typescriptType"` // e.g., Array<User>
Required bool `json:"required"`
Descriptions []string `json:"descriptions"`
}
)
var cachedDocs []*ApiDocsGroup
// HandleGetDocs
//
// @summary returns the API documentation
// @route /api/v1/internal/docs [GET]
// @returns docs.Docs
// @returns []handlers.ApiDocsGroup
func HandleGetDocs(c *RouteCtx) error {
//// Read the file
//wd, _ := os.Getwd()
//buf, err := os.ReadFile(filepath.Join(wd, "seanime-docs/routes.json"))
//if err != nil {
// return c.RespondWithError(err)
//}
//
//var data *docs.Docs
//// Unmarshal the data
//err = json.Unmarshal(buf, &data)
//if err != nil {
// return c.RespondWithError(err)
//}
//
//return c.RespondWithData(data)
return c.RespondWithData("Hello, World!")
if len(cachedDocs) > 0 {
return c.RespondWithData(cachedDocs)
}
// Read the file
wd, _ := os.Getwd()
buf, err := os.ReadFile(filepath.Join(wd, "codegen/generated/handlers.json"))
if err != nil {
return c.RespondWithError(err)
}
var data []*RouteHandler
// Unmarshal the data
err = json.Unmarshal(buf, &data)
if err != nil {
return c.RespondWithError(err)
}
// Group the data
groups := make(map[string]*ApiDocsGroup)
for _, handler := range data {
group, ok := groups[handler.Filename]
if !ok {
group = &ApiDocsGroup{
Filename: handler.Filename,
Name: strings.TrimPrefix(handler.Filename, ".go"),
}
groups[handler.Filename] = group
}
group.Handlers = append(group.Handlers, handler)
}
cachedDocs = make([]*ApiDocsGroup, 0, len(groups))
for _, group := range groups {
cachedDocs = append(cachedDocs, group)
}
return c.RespondWithData(groups)
}

View File

@@ -10,7 +10,7 @@ import (
// @summary returns the total size of cache files.
// @desc The total size of the cache files is returned in human-readable format.
// @route /api/v1/filecache/total-size [GET]
// @returns bool
// @returns string
func HandleGetFileCacheTotalSize(c *RouteCtx) error {
// Get the cache size
size, err := c.App.FileCacher.GetTotalSize()
@@ -59,7 +59,7 @@ func HandleRemoveFileCacheBucket(c *RouteCtx) error {
// @summary returns the total size of cached video file data.
// @desc The total size of the cache video file data is returned in human-readable format.
// @route /api/v1/filecache/mediastream/videofiles/total-size [GET]
// @returns bool
// @returns string
func HandleGetFileCacheMediastreamVideoFilesTotalSize(c *RouteCtx) error {
// Get the cache size
size, err := c.App.FileCacher.GetMediastreamVideoFilesTotalSize()

View File

@@ -564,7 +564,7 @@
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// export function useGetDocs() {
// return useServerQuery<INTERNAL_Docs>({
// return useServerQuery<Array<ApiDocsGroup>>({
// endpoint: API_ENDPOINTS.DOCS.GetDocs.endpoint,
// method: API_ENDPOINTS.DOCS.GetDocs.methods[0],
// queryKey: [API_ENDPOINTS.DOCS.GetDocs.key],
@@ -755,7 +755,7 @@
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// export function useGetFileCacheTotalSize() {
// return useServerQuery<boolean>({
// return useServerQuery<string>({
// endpoint: API_ENDPOINTS.FILECACHE.GetFileCacheTotalSize.endpoint,
// method: API_ENDPOINTS.FILECACHE.GetFileCacheTotalSize.methods[0],
// queryKey: [API_ENDPOINTS.FILECACHE.GetFileCacheTotalSize.key],
@@ -775,7 +775,7 @@
// }
// export function useGetFileCacheMediastreamVideoFilesTotalSize() {
// return useServerQuery<boolean>({
// return useServerQuery<string>({
// endpoint: API_ENDPOINTS.FILECACHE.GetFileCacheMediastreamVideoFilesTotalSize.endpoint,
// method: API_ENDPOINTS.FILECACHE.GetFileCacheMediastreamVideoFilesTotalSize.methods[0],
// queryKey: [API_ENDPOINTS.FILECACHE.GetFileCacheMediastreamVideoFilesTotalSize.key],

View File

@@ -1783,6 +1783,10 @@ export type Debrid_CachedFile = {
* - Package: debrid
*/
export type Debrid_TorrentInfo = {
/**
* ID of the torrent if added to the debrid service
*/
id?: string
name: string
hash: string
size: number
@@ -2186,6 +2190,17 @@ export type ExtensionRepo_UpdateData = {
// Handlers
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* - Filepath: internal/handlers/docs.go
* - Filename: docs.go
* - Package: handlers
*/
export type ApiDocsGroup = {
filename: string
name: string
handlers?: Array<RouteHandler>
}
/**
* - Filepath: internal/handlers/directory_selector.go
* - Filename: directory_selector.go
@@ -2231,6 +2246,61 @@ export type MalAuthResponse = {
token_type: string
}
/**
* - Filepath: internal/handlers/docs.go
* - Filename: docs.go
* - Package: handlers
*/
export type RouteHandler = {
name: string
trimmedName: string
comments?: Array<string>
filepath: string
filename: string
api?: RouteHandlerApi
}
/**
* - Filepath: internal/handlers/docs.go
* - Filename: docs.go
* - Package: handlers
*/
export type RouteHandlerApi = {
summary: string
descriptions?: Array<string>
endpoint: string
methods?: Array<string>
params?: Array<RouteHandlerParam>
bodyFields?: Array<RouteHandlerParam>
returns: string
returnGoType: string
returnTypescriptType: string
}
/**
* - Filepath: internal/handlers/docs.go
* - Filename: docs.go
* - Package: handlers
*/
export type RouteHandlerParam = {
name: string
jsonName: string
/**
* e.g., []models.User
*/
goType: string
/**
* e.g., models.User
*/
usedStructType: string
/**
* e.g., Array<User>
*/
typescriptType: string
required: boolean
descriptions?: Array<string>
}
/**
* - Filepath: internal/handlers/status.go
* - Filename: status.go

View File

@@ -1,8 +1,9 @@
import { useServerQuery } from "@/api/client/requests"
import { API_ENDPOINTS } from "@/api/generated/endpoints"
import { ApiDocsGroup } from "@/api/generated/types"
export function useGetDocs() {
return useServerQuery<any>({
return useServerQuery<ApiDocsGroup[]>({
endpoint: API_ENDPOINTS.DOCS.GetDocs.endpoint,
method: API_ENDPOINTS.DOCS.GetDocs.methods[0],
queryKey: [API_ENDPOINTS.DOCS.GetDocs.key],

View File

@@ -97,7 +97,7 @@ export function MonthCalendar(props: WeekCalendarProps) {
})
daysArray.push({
date: format(day, "yyyy-MM-dd"),
date: "Aired",
isCurrentMonth: isSameMonth(day, currentDate),
isToday: isToday(day),
isSelected: false,

View File

@@ -131,6 +131,16 @@ export function DebridSettings(props: DebridSettingsProps) {
help="Let Seanime find the best torrent automatically, based on cache and resolution."
/>
{f.watch("streamAutoSelect") && f.watch("provider") === "torbox" && (
<Alert
intent="warning-basic"
title="Auto-select with TorBox"
description={<p>
Avoid using auto-select if you have a limited amount of downloads on your Debrid service.
</p>}
/>
)}
<Field.Select
name="streamPreferredResolution"
label="Preferred resolution"

View File

@@ -1,104 +1,109 @@
"use client"
import { DatePicker } from "@/components/ui/date-picker"
import { normalizeDate } from "@/lib/helpers/date"
import React from "react"
import { useGetDocs } from "@/api/hooks/docs.hooks"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Badge } from "@/components/ui/badge"
import { LoadingSpinner } from "@/components/ui/loading-spinner"
import { Separator } from "@/components/ui/separator"
import React, { useEffect } from "react"
export default function Page() {
const [value, setValue] = React.useState<Date | undefined>(undefined)
const { data, isLoading } = useGetDocs()
React.useEffect(() => {
setValue(normalizeDate("2024-04-15T00:00:00Z"))
console.log(normalizeDate("2024-04-15T00:00:00Z"))
console.log(new Date("2024-04-15T00:00:00Z"))
}, [])
React.useEffect(() => {
console.log(value)
}, [value])
useEffect(() => {
console.log(data)
})
if (isLoading) return <LoadingSpinner />
return (
<div className="space-y-4 container py-10">
<DatePicker
value={value}
onValueChange={setValue}
/>
{/*{data?.routeGroups?.map((group, i) => (*/}
{/* <div key={group.filename + i} className="space-y-4">*/}
{/* <h4 className="">{group.filename}</h4>*/}
{data?.toSorted((a, b) => a.filename?.localeCompare(b.filename)).map((group, i) => (
<div key={group.filename + i} className="space-y-4">
<h4 className=""><span>{group.filename}</span> <span className="text-gray-300">/</span>
<span className="text-[--muted]"> {group.filename.replace(".go", "")}.hooks.ts</span></h4>
<Accordion type="multiple" defaultValue={[]}>
{group.handlers?.toSorted((a, b) => a.filename?.localeCompare(b.filename)).map((route, i) => (
<AccordionItem value={route.name} key={route.name + i} className="space-y-2">
<AccordionTrigger className="rounded flex-none w-full">
<p className="flex gap-2 items-center">
<Badge
className="w-24 py-4"
intent={(route.api!.methods?.includes("GET") && route.api!.methods?.length === 1) ? "success"
: route.api!.methods?.includes("GET") ? "warning"
: route.api!.methods?.includes("DELETE") ? "alert"
: route.api!.methods?.includes("PATCH") ? "warning" : "primary"}
>
{route.api!.methods?.join(", ")}
</Badge>
<span className="font-semibold flex-none whitespace-nowrap">{route.api!.endpoint}</span>
<span className="font-normal text-sm text-[--muted] flex-none whitespace-nowrap">{route.name}</span>
{/*<span className="font-medium text-[--muted] text-sm truncate flex-shrink">({route.name.replace("Handle", "")})</span>*/}
<span className="text-[--muted] text-[.97rem] whitespace-nowrap truncate text-ellipsis"> - {route.api!.summary}</span>
</p>
</AccordionTrigger>
{/* <Accordion type="multiple" defaultValue={[]}>*/}
{/* {group.routes.toSorted((a, b) => a.endpoint.length - b.endpoint.length).map((route, i) => (*/}
{/* <AccordionItem value={route.name} key={route.name + i} className="space-y-2">*/}
{/* <AccordionTrigger className="rounded flex-none w-full">*/}
{/* <p className="flex gap-2 items-center">*/}
{/* <Badge*/}
{/* className="w-24 py-4"*/}
{/* intent={(route.methods.includes("GET") && route.methods.length === 1) ? "success"*/}
{/* : route.methods.includes("GET") ? "warning"*/}
{/* : route.methods.includes("DELETE") ? "alert"*/}
{/* : route.methods.includes("PATCH") ? "warning" : "primary"}*/}
{/* >*/}
{/* {route.methods.join(", ")}*/}
{/* </Badge>*/}
{/* <span className="font-semibold flex-none whitespace-nowrap">{route.endpoint}</span>*/}
{/* /!*<span className="font-medium text-[--muted] text-sm truncate flex-shrink">({route.name.replace("Handle", "")})</span>*!/*/}
{/* <span className="text-[--muted] text-[.97rem] whitespace-nowrap truncate text-ellipsis"> - {route.summary}</span>*/}
{/* </p>*/}
{/* </AccordionTrigger>*/}
{/* <AccordionContent className="space-y-4 bg-gray-50 border rounded mb-4">*/}
<AccordionContent className="space-y-4 border rounded mb-4">
{/*<p className="font-bold">*/}
{/* {route.name.replace("Handle", "")}*/}
{/* {route.name}*/}
{/*</p>*/}
{/* {!!route.description && <p className="">{route.description}</p>}*/}
{/* <div className="space-y-2">*/}
{/* <h5>Params</h5>*/}
{/* <ul className="list-disc pl-4">*/}
{/* {route.params.map((param, i) => (*/}
{/* <li key={param.name + i} className="flex gap-2 items-center">*/}
{/* <p className="font-medium">*/}
{/* {param.name}*/}
{/* {param.required && <span className="text-red-500">*</span>}*/}
{/*<p className="">*/}
{/* Used in: <span className="font-bold">{route.filename.replace(".go", "")}.hooks.ts</span>*/}
{/*</p>*/}
{/* <p className="text-[--muted]">{param.type}</p>*/}
{/* <p>{param.description}</p>*/}
{/* </li>*/}
{/* ))}*/}
{/* </ul>*/}
{/* </div>*/}
{!!route.api!.descriptions?.length && <div>
{route.api!.descriptions?.map((desc, i) => (
<p key={desc + i}>{desc}</p>
))}
</div>}
{/* <div className="space-y-2">*/}
{/* <h5>Request Body Fields</h5>*/}
{/* <ul className="list-disc pl-4">*/}
{/* {route.requestBodyFields.map((field, i) => (*/}
{/* <li key={field.name + i} className="flex gap-2 items-center">*/}
{/* <p className="font-medium">{field.name}</p>*/}
{/* <p className="text-[--muted]">{field.type}</p>*/}
{/* <p>{field.description}</p>*/}
{/* </li>*/}
{/* ))}*/}
{/* </ul>*/}
{/* </div>*/}
{!!route.api!.params?.length && <div className="space-y-2">
<h5>URL Params</h5>
<ul className="list-disc pl-4">
{route.api!.params?.map((param, i) => (
<li key={param.name + i} className="flex gap-2 items-center">
<p className="font-medium">
{param.name}
{param.required && <span className="text-red-500">*</span>}
</p>
<p className="text-[--muted]">{param.typescriptType}</p>
{param.descriptions?.map((desc, i) => (
<p key={desc + i}>{desc}</p>
))}
</li>
))}
</ul>
</div>}
{/* <div className="flex gap-2 items-center">*/}
{/* <p className="font-medium text-[--muted]">Returns</p>*/}
{/* <p className="font-bold text-brand-900">{route.returns}</p>*/}
{/* </div>*/}
{/* </AccordionContent>*/}
{/* </AccordionItem>*/}
{/* ))}*/}
{/* </Accordion>*/}
{!!route.api?.bodyFields?.length && <div className="space-y-2">
<h5>Body</h5>
<ul className="list-disc pl-4">
{route.api?.bodyFields?.map((field, i) => (
<li key={field.name + i} className="flex gap-2 items-center">
<p className="font-medium">{field.jsonName} {field.required &&
<span className="text-[--red]">*</span>}</p>
<p className="text-[--muted]">{field.typescriptType}</p>
{field.descriptions?.map((desc, i) => (
<p key={desc + i}>{desc}</p>
))}
</li>
))}
</ul>
</div>}
{/* <Separator />*/}
{/* </div>*/}
{/*))}*/}
<div className="flex gap-2 items-center">
<p className="font-medium text-[--muted]">Returns</p>
<p className="font-bold text-brand-900">{route.api!.returnTypescriptType}</p>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
<Separator />
</div>
))}
</div>
)
}