-
+
{title}
diff --git a/src/components/ScopeBadges.module.css b/src/components/ScopeBadges.module.css
index c727927..c762eef 100644
--- a/src/components/ScopeBadges.module.css
+++ b/src/components/ScopeBadges.module.css
@@ -12,5 +12,5 @@
border-radius: 4px;
background-color: transparent !important;
color: var(--ifm-color-primary);
- border: 2px solid var(--ifm-toc-border-color)
+ border: 2px solid var(--ifm-toc-border-color);
}
diff --git a/src/components/ScopeBadges.jsx b/src/components/ScopeBadges.tsx
similarity index 79%
rename from src/components/ScopeBadges.jsx
rename to src/components/ScopeBadges.tsx
index ebdbca2..e4ed840 100644
--- a/src/components/ScopeBadges.jsx
+++ b/src/components/ScopeBadges.tsx
@@ -10,9 +10,5 @@ export function ScopeBadges({ scope, read, write }) {
}
function Badge({ scope }) {
- return (
-
- {scope}
-
- )
+ return
{scope}
}
diff --git a/src/components/documentation/Sample.jsx b/src/components/documentation/Sample.tsx
similarity index 69%
rename from src/components/documentation/Sample.jsx
rename to src/components/documentation/Sample.tsx
index 9dd3243..ca8b4fc 100644
--- a/src/components/documentation/Sample.jsx
+++ b/src/components/documentation/Sample.tsx
@@ -1,8 +1,9 @@
-import { useState } from 'react'
import CodeBlock from '@theme/CodeBlock'
+import { useState } from 'react'
import styles from './Sample.module.css'
+import type { Sample as SampleType } from './useServiceDocs'
-export default function Sample({ sample }) {
+export function Sample({ sample }: { sample: SampleType }) {
const [shown, setShown] = useState(false)
const stringified = JSON.stringify(sample.sample, null, 2)
@@ -12,18 +13,12 @@ export default function Sample({ sample }) {
<>
{sample.title}
- setShown(!shown)}
- >
+ setShown(!shown)}>
{shown ? 'Hide' : 'Show'}
-
+
{shown ? content : '{ ... }'}
>
diff --git a/src/components/documentation/ServiceDocumentation.jsx b/src/components/documentation/ServiceDocumentation.jsx
deleted file mode 100644
index dbd7a16..0000000
--- a/src/components/documentation/ServiceDocumentation.jsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React from 'react'
-import useServiceDocs from './useServiceDocs'
-import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
-import styles from './ServiceDocumentation.module.css'
-import clsx from 'clsx'
-import Sample from './Sample'
-import Head from '@docusaurus/Head'
-import Heading from '@theme/Heading'
-import { ScopeBadges } from '../ScopeBadges'
-
-function slugify(text) {
- if (typeof text !== 'string' || text.length === 0) {
- return '';
- }
-
- // 1. Remove characters that are not a-z, 0-9, space, or dash
- let slug = text.toLowerCase().replace(/[^a-z0-9\s-]/g, '');
-
- // 2. Replace all remaining spaces and multiple dashes with a single dash
- slug = slug.replace(/[\s-]+/g, '-');
-
- // 3. Trim leading/trailing dashes
- slug = slug.replace(/^-+|-+$/g, '');
-
- return slug;
-}
-
-export default function ServiceDocumentation({ service, metaDescription }) {
- const { siteConfig } = useDocusaurusContext()
- const serviceData = useServiceDocs(service)
-
- if (!serviceData) {
- return Service "{service}" not found
- }
-
- const { routes } = serviceData
-
- const getParamRequiredText = (requiredType) => {
- if (requiredType === 'YES') return ✅ Yes
- if (requiredType === 'NO') return ❌ No
- if (requiredType === 'SOMETIMES') return ⚠️ Sometimes
- }
-
- const getParamDescriptionText = (description) => {
- let transformed = description
-
- const parts = description.split(' ')
- for (const part of parts) {
- if (part.startsWith('@type')) {
- const innerMatch = part.match(/[^\(]*\:([^\)]*)/)[0]
- const fullMatch = part.match(/@type\((.*)\:(.*)\)/)[0]
- const matchParts = innerMatch.split(':')
-
- transformed = transformed.replace(fullMatch, `${matchParts[0]} `)
- }
- }
-
- return transformed
- }
-
- const getSortOrder = (method) => {
- return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method)
- }
-
- const getRouteDescription = (route) => {
- return route.description?.split('\n').map((part, idx) => {
- if (idx === 0) return null
- return {part}
- }) ?? null
- }
-
- const getRouteTitle = (route) => {
- return route.description?.split('\n')[0] ?? 'No title'
- }
-
- return (
- <>
-
-
-
-
-
-
-
- {routes.sort((a, b) => getSortOrder(a.method) - getSortOrder(b.method)).map((route, idx) => {
- const headers = route.params.filter(({ type }) => type === 'headers')
- const routeParams = route.params.filter(({ type }) => type === 'route')
- const queryParams = route.params.filter(({ type }) => type === 'query')
- const bodyParams = route.params.filter(({ type }) => type === 'body')
-
- const sections = [
- { title: 'Headers', params: headers },
- { title: 'Route params', params: routeParams },
- { title: 'Query keys', params: queryParams },
- { title: 'Body keys', params: bodyParams }
- ]
-
- const routeTitle = getRouteTitle(route)
-
- const scopes = route.scopes || []
- const scopeMap = scopes.reduce((acc, scopeString) => {
- const [action, scope] = scopeString.split(':')
- if (!acc[scope]) {
- acc[scope] = { read: false, write: false }
- }
- if (action === 'read') acc[scope].read = true
- if (action === 'write') acc[scope].write = true
- return acc
- }, {})
-
- return (
-
- {routeTitle}
-
- {Object.entries(scopeMap).map(([scope, { read, write }]) => (
-
- ))}
-
-
- {route.method} {siteConfig.customFields.docs.baseUrl}{route.path}
-
-
- {getRouteDescription(route)}
-
- {sections.filter((section) => {
- if (route.method === 'GET') {
- return section.title !== 'Body keys'
- }
- return true
- }).map((section) => {
- return (
-
-
- {section.title}
-
-
-
- {section.params.length === 0 &&
None available
}
-
- {section.params.length > 0 &&
-
-
-
- Key
- Required
- Description
-
-
-
-
- {section.params.sort((a, b) => a.name.localeCompare(b.name)).map((param, idx) => (
-
- {param.name}
- {getParamRequiredText(param.required)}
-
-
- ))}
-
-
- }
-
-
- )
- })}
-
- {route.samples.map((sample) => )}
-
-
-
- )
- })}
- >
- )
-}
diff --git a/src/components/documentation/ServiceDocumentation.module.css b/src/components/documentation/ServiceDocumentation.module.css
index a7d98a7..a7b1bcc 100644
--- a/src/components/documentation/ServiceDocumentation.module.css
+++ b/src/components/documentation/ServiceDocumentation.module.css
@@ -1,7 +1,7 @@
.url {
display: inline-block;
padding: 8px;
- margin-bottom: calc(var(--ifm-heading-vertical-rhythm-top) * var(--ifm-leading))
+ margin-bottom: calc(var(--ifm-heading-vertical-rhythm-top) * var(--ifm-leading));
}
.descriptionText {
@@ -41,7 +41,8 @@
background-color: #b91c1c;
}
-.nameCell, .requiredCell {
+.nameCell,
+.requiredCell {
min-width: 140px;
}
diff --git a/src/components/documentation/ServiceDocumentation.tsx b/src/components/documentation/ServiceDocumentation.tsx
new file mode 100644
index 0000000..ee41999
--- /dev/null
+++ b/src/components/documentation/ServiceDocumentation.tsx
@@ -0,0 +1,251 @@
+import Head from '@docusaurus/Head'
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
+import Heading from '@theme/Heading'
+import clsx from 'clsx'
+import React from 'react'
+import { ScopeBadges } from '../ScopeBadges'
+import { Sample } from './Sample'
+import styles from './ServiceDocumentation.module.css'
+import { Docs, ServiceRoute, useServiceDocs } from './useServiceDocs'
+
+type ScopeMap = {
+ [scope: string]: {
+ read: boolean
+ write: boolean
+ }
+}
+
+function slugify(text?: string) {
+ if (typeof text !== 'string' || text.length === 0) {
+ return ''
+ }
+
+ // 1. Remove characters that are not a-z, 0-9, space, or dash
+ let slug = text.toLowerCase().replace(/[^a-z0-9\s-]/g, '')
+
+ // 2. Replace all remaining spaces and multiple dashes with a single dash
+ slug = slug.replace(/[\s-]+/g, '-')
+
+ // 3. Trim leading/trailing dashes
+ slug = slug.replace(/^-+|-+$/g, '')
+
+ return slug
+}
+
+function normaliseMethod(method: string) {
+ return method.toUpperCase()
+}
+
+export function ServiceDocumentation({
+ service,
+ metaDescription,
+}: {
+ service: string
+ metaDescription: string
+}) {
+ const { siteConfig } = useDocusaurusContext()
+ const serviceData = useServiceDocs(service)
+
+ if (!serviceData) {
+ return "{service}" not found
+ }
+
+ const { routes } = serviceData
+
+ const getParamRequiredText = (required: boolean) => {
+ if (required) {
+ return (
+
+ ✅ Yes
+
+ )
+ }
+
+ return (
+
+ ❌ No
+
+ )
+ }
+
+ const getParamDescriptionText = (description?: string) => {
+ if (!description) {
+ return ''
+ }
+
+ let transformed = description
+
+ const parts = description.split(' ')
+ for (const part of parts) {
+ if (part.startsWith('@type')) {
+ const innerMatch = part.match(/[^(]*:([^)]*)/)?.[0]
+ const fullMatch = part.match(/@type\((.*):(.*)\)/)?.[0]
+ if (innerMatch && fullMatch) {
+ const matchParts = innerMatch.split(':')
+ transformed = transformed.replace(
+ fullMatch,
+ `${matchParts[0]} `,
+ )
+ }
+ }
+ }
+
+ return transformed
+ }
+
+ const getSortOrder = (method) => {
+ return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(normaliseMethod(method))
+ }
+
+ const getRouteDescription = (route: ServiceRoute) => {
+ return (
+ route.description?.split('\n').map((part, idx) => {
+ if (idx === 0) {
+ return null
+ }
+ return (
+
+ {part}
+
+ )
+ }) ?? null
+ )
+ }
+
+ const getRouteTitle = (route: ServiceRoute) => {
+ return route.description?.split('\n')[0] ?? 'No title'
+ }
+
+ const extractParamTypes = (params: ServiceRoute['params'], paramType: string) => {
+ return params.filter((param) => param.location === paramType)
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {routes
+ .sort((a, b) => getSortOrder(a.method) - getSortOrder(b.method))
+ .map((route, idx) => {
+ const sections = [
+ { title: 'Headers', params: extractParamTypes(route.params, 'headers') },
+ { title: 'Route params', params: extractParamTypes(route.params, 'route') },
+ { title: 'Query keys', params: extractParamTypes(route.params, 'query') },
+ { title: 'Body keys', params: extractParamTypes(route.params, 'body') },
+ ]
+
+ const routeTitle = getRouteTitle(route)
+
+ const scopes = route.scopes || []
+ const scopeMap = scopes.reduce((acc, scopeString) => {
+ const [action, scope] = scopeString.split(':')
+ if (!acc[scope]) {
+ acc[scope] = { read: false, write: false }
+ }
+ if (action === 'read') {
+ acc[scope].read = true
+ }
+ if (action === 'write') {
+ acc[scope].write = true
+ }
+ return acc
+ }, {} as ScopeMap)
+
+ return (
+
+
+ {routeTitle}
+
+
+ {Object.entries(scopeMap).map(([scope, { read, write }]) => (
+
+ ))}
+
+
+
+ {normaliseMethod(route.method)}
+ {' '}
+
+ {(siteConfig.customFields.docs as Docs).baseUrl}
+ {route.path}
+
+
+
+ {getRouteDescription(route)}
+
+ {sections
+ .filter((section) => {
+ if (normaliseMethod(route.method) === 'GET') {
+ return section.title !== 'Body keys'
+ }
+ return true
+ })
+ .map((section) => {
+ return (
+
+
+ {section.title}
+
+
+
+ {section.params.length === 0 && (
+
None available
+ )}
+
+ {section.params.length > 0 && (
+
+
+
+ Key
+ Required
+ Description
+
+
+
+
+ {section.params
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((param, idx) => (
+
+
+ {param.name}
+
+
+ {getParamRequiredText(param.required)}
+
+
+
+ ))}
+
+
+ )}
+
+
+ )
+ })}
+
+ {route.samples?.map((sample) => (
+
+ ))}
+
+
+
+ )
+ })}
+ >
+ )
+}
diff --git a/src/components/documentation/ServiceDocumentationWithTOC.jsx b/src/components/documentation/ServiceDocumentationWithTOC.jsx
deleted file mode 100644
index 1252eef..0000000
--- a/src/components/documentation/ServiceDocumentationWithTOC.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import ServiceDocumentation, { generateTOCForService } from './ServiceDocumentation'
-import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
-import TOCInline from '@theme/TOCInline'
-
-/**
- * Wrapper component that renders ServiceDocumentation with an inline TOC
- * This allows the dynamically generated headings to appear in the right sidebar
- */
-export default function ServiceDocumentationWithTOC({ service, metaDescription }) {
- const { siteConfig } = useDocusaurusContext()
- const toc = generateTOCForService(service, siteConfig)
-
- return (
- <>
-
-
- >
- )
-}
diff --git a/src/components/documentation/generateServiceTOC.jsx b/src/components/documentation/generateServiceTOC.jsx
deleted file mode 100644
index 6eada78..0000000
--- a/src/components/documentation/generateServiceTOC.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Server-side function to generate TOC entries for a service.
- *
- * @param {string} service - The service name (e.g., 'GameStatAPIService')
- * @param {object} siteConfig - The Docusaurus site config
- * @returns {Array} Array of TOC entries
- */
-export function generateServiceTOC(service, siteConfig) {
- const services = siteConfig?.customFields?.docs?.services || []
- const serviceData = services.find(s => s.name === service)
-
- if (!serviceData || !serviceData.routes) {
- return []
- }
-
- const slugify = (text) => {
- if (typeof text !== 'string' || text.length === 0) {
- return ''
- }
- let slug = text.toLowerCase().replace(/[^a-z0-9\s-]/g, '')
- slug = slug.replace(/[\s-]+/g, '-')
- slug = slug.replace(/^-+|-+$/g, '')
- return slug
- }
-
- const getSortOrder = (method) => {
- return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method)
- }
-
- const getRouteTitle = (route) => {
- return route.description?.split('\n')[0] ?? 'No title'
- }
-
- return serviceData.routes
- .sort((a, b) => getSortOrder(a.method) - getSortOrder(b.method))
- .map(route => ({
- value: getRouteTitle(route),
- id: slugify(getRouteTitle(route)),
- level: 3
- }))
-}
diff --git a/src/components/documentation/generateServiceTOC.tsx b/src/components/documentation/generateServiceTOC.tsx
new file mode 100644
index 0000000..2b53c1c
--- /dev/null
+++ b/src/components/documentation/generateServiceTOC.tsx
@@ -0,0 +1,42 @@
+import config from '@generated/docusaurus.config'
+import { Docs, ServiceRoute } from './useServiceDocs'
+
+export function generateServiceTOC(service: string) {
+ const siteConfig = config
+ const services = (siteConfig.customFields.docs as Docs).services
+ const serviceData = services.find((s) => s.name === service)
+
+ if (!serviceData?.routes) {
+ return []
+ }
+
+ const slugify = (text: string) => {
+ if (typeof text !== 'string' || text.length === 0) {
+ return ''
+ }
+
+ const slug = text
+ .toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, '')
+ .replace(/[\s-]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+
+ return slug
+ }
+
+ const getSortOrder = (method: string) => {
+ return ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method.toUpperCase())
+ }
+
+ const getRouteTitle = (route: ServiceRoute) => {
+ return route.description?.split('\n')[0] ?? 'Missing title'
+ }
+
+ return serviceData.routes
+ .sort((a, b) => getSortOrder(a.method) - getSortOrder(b.method))
+ .map((route) => ({
+ value: getRouteTitle(route),
+ id: slugify(getRouteTitle(route)),
+ level: 3,
+ }))
+}
diff --git a/src/components/documentation/useServiceDocs.js b/src/components/documentation/useServiceDocs.js
deleted file mode 100644
index 20424c9..0000000
--- a/src/components/documentation/useServiceDocs.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
-
-export default function useServiceDocs(serviceName) {
- const { siteConfig } = useDocusaurusContext()
- const { services } = siteConfig.customFields.docs
-
- return services.find((service) => service.name === serviceName)
-}
diff --git a/src/components/documentation/useServiceDocs.ts b/src/components/documentation/useServiceDocs.ts
new file mode 100644
index 0000000..3762914
--- /dev/null
+++ b/src/components/documentation/useServiceDocs.ts
@@ -0,0 +1,39 @@
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
+
+export type Sample = {
+ title: string
+ sample: object
+}
+
+export type ServiceRoute = {
+ method: 'get' | 'post' | 'put' | 'patch' | 'delete'
+ path: string
+ description: string
+ params: {
+ location: 'query' | 'body' | 'route' | 'headers'
+ name: string
+ required: boolean
+ description?: string
+ type: string
+ }[]
+ samples: Sample[] | null
+ scopes?: string[]
+}
+
+export type TaloService = {
+ name: string
+ path: string
+ routes: ServiceRoute[]
+}
+
+export type Docs = {
+ baseUrl: string
+ services: TaloService[]
+}
+
+export function useServiceDocs(serviceName: string) {
+ const { siteConfig } = useDocusaurusContext()
+ const { services } = siteConfig.customFields.docs as Docs
+
+ return services.find((service) => service.name === serviceName)
+}
diff --git a/src/pages/index.jsx b/src/pages/index.tsx
similarity index 75%
rename from src/pages/index.jsx
rename to src/pages/index.tsx
index 880b06f..b75affb 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.tsx
@@ -1,6 +1,6 @@
import Layout from '@theme/Layout'
-import styles from './index.module.css'
import HomepageFeatures from '../components/HomepageFeatures'
+import styles from './index.module.css'
function HomepageHeader() {
return (
@@ -8,7 +8,10 @@ function HomepageHeader() {
Talo Game Services
An open-source backend for your games.
-
Using Talo, you can integrate leaderboards, stats, event tracking, Steamworks and more with our Godot plugin, Unity package or API.
+
+ Using Talo, you can integrate leaderboards, stats, event tracking, Steamworks and more
+ with our Godot plugin, Unity package or API.
+
)
@@ -18,7 +21,8 @@ export default function Home() {
return (
+ description='Talo is an open-source backend for your game. You can integrate leaderboards, stats, event tracking, Steamworks and more with our Godot plugin or Unity package.'
+ >
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..920d7a6
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ // This file is not used in compilation. It is here just for a nice editor experience.
+ "extends": "@docusaurus/tsconfig",
+ "compilerOptions": {
+ "baseUrl": "."
+ },
+ "exclude": [".docusaurus", "build"]
+}