initial commit after project creation

This commit is contained in:
Gerhard Scheikl
2026-04-01 09:38:50 +02:00
commit b02af637d4
292 changed files with 61408 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
const AccordionItemFields = [
{
name: "heading",
label: "Heading",
type: "string",
description:
"The heading text that will be displayed in the collapsed state",
},
{
name: "docText",
label: "Body Text",
isBody: true,
type: "rich-text",
},
{
name: "image",
label: "image",
type: "image",
},
];
export const AccordionTemplate = {
name: "accordion",
label: "Accordion",
ui: {
defaultItem: {
heading: "Click to expand",
//TODO: Need to configure this to be a rich text field
docText: {
type: "root",
children: [
{
type: "p",
children: [
{
type: "text",
text: "Default Text. Edit me!",
},
],
},
],
},
image: "/img/rico-replacement.jpg",
fullWidth: false,
},
},
fields: [
...AccordionItemFields,
{
name: "fullWidth",
label: "Full Width",
type: "boolean",
},
],
};
export default AccordionTemplate;
export const AccordionBlockTemplate = {
name: "accordionBlock",
label: "Accordion Block",
fields: [
{
name: "fullWidth",
label: "Full Width",
type: "boolean",
},
{
name: "accordionItems",
label: "Accordion Items",
type: "object",
list: true,
fields: AccordionItemFields,
ui: {
itemProps: (item) => {
return {
label: item.heading ?? "Accordion Item",
};
},
defaultItem: {
heading: "Click to expand",
docText: {
type: "root",
children: [
{
type: "p",
children: [
{
type: "text",
text: "Default Text. Edit me!",
},
],
},
],
},
image: "/img/rico-replacement.jpg",
},
},
},
],
};

View File

@@ -0,0 +1,343 @@
"use client";
import { CustomDropdown } from "@/src/components/ui/custom-dropdown";
import type { DropdownOption } from "@/src/components/ui/custom-dropdown";
import React, { useState, useEffect } from "react";
// Define schema type to match the actual structure from the API
interface SchemaFile {
id: string;
relativePath: string;
apiSchema?: string | null;
_sys: {
filename: string;
};
}
// Interface for parsed Swagger/OpenAPI details
interface SchemaDetails {
title?: string;
version?: string;
endpointCount: number;
endpoints: Endpoint[];
}
// Interface for endpoint details
interface Endpoint {
path: string;
method: string;
summary: string;
operationId?: string;
}
// Parse Swagger/OpenAPI JSON to extract details
const parseSwaggerJson = (jsonContent: string): SchemaDetails => {
try {
const parsed = JSON.parse(jsonContent);
// Extract endpoints
const endpoints: Endpoint[] = [];
if (parsed.paths) {
for (const path of Object.keys(parsed.paths)) {
const pathObj = parsed.paths[path];
for (const method of Object.keys(pathObj)) {
const operation = pathObj[method];
endpoints.push({
path,
method: method.toUpperCase(),
summary: operation.summary || `${method.toUpperCase()} ${path}`,
operationId: operation.operationId,
});
}
}
}
return {
title: parsed.info?.title || "Unknown API",
version: parsed.info?.version || "Unknown Version",
endpointCount: endpoints.length,
endpoints,
};
} catch (error) {
return {
title: "Error Parsing Schema",
version: "Unknown",
endpointCount: 0,
endpoints: [],
};
}
};
const getSchemas = async () => {
try {
const { schemas } = await fetchSchemas();
if (schemas) {
// Convert API response into our simpler SchemaFile interface
const schemaFiles: SchemaFile[] = schemas.map((schema) => ({
id: schema.id,
relativePath: schema.filename,
apiSchema: schema.apiSchema,
_sys: {
filename: schema.displayName,
},
}));
return schemaFiles;
}
return [];
} catch (error) {
return [];
}
};
const fetchSchemas = async () => {
const response = await fetch("/api/list-api-schemas");
return await response.json();
};
const getSchemaDetails = async (schemaPath: string, schemas: SchemaFile[]) => {
try {
// Find the selected schema
const selectedSchema = schemas.find((s) => s.relativePath === schemaPath);
if (selectedSchema?.apiSchema) {
const details = parseSwaggerJson(selectedSchema.apiSchema);
return details;
}
// If the schema content isn't in the current data, fetch it
const { schemas: data } = await fetchSchemas();
if (data?.apiSchema) {
const details = parseSwaggerJson(data.apiSchema);
return details;
}
} catch (error) {
return null;
}
};
// Custom field for selecting an API schema file
const SchemaSelector = (props: any) => {
const { input, field } = props;
const [schemas, setSchemas] = useState<SchemaFile[]>([]);
const [loading, setLoading] = useState(true);
const [schemaDetails, setSchemaDetails] = useState<SchemaDetails | null>(
null
);
const [loadingDetails, setLoadingDetails] = useState(false);
const [selectedEndpoint, setSelectedEndpoint] = useState<string>("");
// Fetch schema details when a schema is selected
useEffect(() => {
if (!input.value) {
setSchemaDetails(null);
return;
}
const parts = input.value.split("|");
if (parts.length > 1) {
setSelectedEndpoint(parts[1]);
} else {
setSelectedEndpoint("");
}
const fetchSchemaDetails = async () => {
setLoadingDetails(true);
const details = await getSchemaDetails(
input.value.split("|")[0],
schemas
);
setSchemaDetails(details);
setLoadingDetails(false);
};
fetchSchemaDetails();
}, [input.value, schemas]);
// Fetch available schema files when component mounts
useEffect(() => {
const fetchSchemas = async () => {
setLoading(true);
const schemas = await getSchemas();
setSchemas(schemas);
setLoading(false);
};
fetchSchemas();
}, []);
const handleSchemaChange = async (schemaPath: string) => {
// Reset endpoint selection when schema changes
if (!schemaDetails) {
setLoadingDetails(true);
const details = await getSchemaDetails(
input.value.split("|")[0],
schemas
);
setSchemaDetails(details);
setLoadingDetails(false);
}
setSelectedEndpoint("");
input.onChange(schemaPath);
};
const handleEndpointChange = (endpoint: string) => {
setSelectedEndpoint(endpoint);
// Extract just the schema path
const schemaPath = input.value.split("|")[0];
// Combine schema path and endpoint
input.onChange(endpoint ? `${schemaPath}|${endpoint}` : schemaPath);
};
// Helper function to create a unique endpoint identifier
const createEndpointId = (endpoint: Endpoint) => {
return `${endpoint.method}:${endpoint.path}`;
};
return (
<div className="w-full max-w-full overflow-x-hidden">
<label className="block font-medium text-gray-700 mb-1">
{field.label || "Select API Schema"}
</label>
{loading ? (
<div className="py-2 px-3 bg-gray-100 rounded text-gray-500">
Loading schemas...
</div>
) : schemas.length === 0 ? (
<div className="max-w-full w-full py-2 px-3 bg-red-50 text-red-500 rounded whitespace-normal">
No API schema files found. Please upload one in the Content Manager.
</div>
) : (
<div className="max-w-full w-full overflow-x-hidden">
{/* Schema selector dropdown */}
<CustomDropdown
value={input.value?.split("|")[0]}
onChange={handleSchemaChange}
options={[
{ value: "", label: "Select a schema" },
...schemas.map<DropdownOption>((schema) => ({
value: schema.relativePath,
label: schema._sys.filename,
})),
]}
placeholder="Select a schema"
/>
{input.value && (
<div className="mt-3 p-3 bg-blue-50 text-blue-600 rounded text-sm w-full max-w-full overflow-x-hidden">
<div className="font-medium mb-1 truncate break-words whitespace-normal max-w-full">
Selected schema:{" "}
<span className="truncate break-words whitespace-normal max-w-full">
{
schemas.find(
(s) => s.relativePath === input.value.split("|")[0]
)?._sys.filename
}
</span>
</div>
{loadingDetails ? (
<div className="text-blue-500">Loading schema details...</div>
) : schemaDetails ? (
<>
<div className="grid grid-cols-3 gap-2 mt-2 w-full max-w-full">
<div className="bg-blue-100 p-2 rounded w-full max-w-full break-words whitespace-normal">
<div className="text-xs text-blue-500 truncate break-words whitespace-normal max-w-full">
API Name
</div>
<div className="font-medium truncate break-words whitespace-normal max-w-full">
{schemaDetails.title}
</div>
</div>
<div className="bg-blue-100 p-2 rounded w-full max-w-full break-words whitespace-normal">
<div className="text-xs text-blue-500 truncate break-words whitespace-normal max-w-full">
Version
</div>
<div className="font-medium truncate break-words whitespace-normal max-w-full">
{schemaDetails.version}
</div>
</div>
<div className="bg-blue-100 p-2 rounded w-full max-w-full break-words whitespace-normal">
<div className="text-xs text-blue-500 truncate break-words whitespace-normal max-w-full">
Endpoints
</div>
<div className="font-medium truncate break-words whitespace-normal max-w-full">
{schemaDetails.endpointCount}
</div>
</div>
</div>
{/* Endpoint selector */}
{schemaDetails.endpoints.length > 0 && (
<div className="mt-4 w-full max-w-full overflow-x-hidden">
<label className="block text-blue-700 font-medium mb-1">
Select Endpoint (Optional)
</label>
{/* Endpoint selector dropdown */}
<CustomDropdown
value={selectedEndpoint}
onChange={handleEndpointChange}
options={[
{ value: "", label: "All Endpoints" },
...schemaDetails.endpoints
.sort((a, b) => a.path.localeCompare(b.path))
.map<DropdownOption>((endpoint) => ({
value: createEndpointId(endpoint),
label: `${endpoint.method} ${endpoint.path} ${
endpoint.summary ? `- ${endpoint.summary}` : ""
}`,
})),
]}
placeholder="All Endpoints"
contentClassName="bg-white border border-blue-300"
/>
</div>
)}
</>
) : (
<div className="text-blue-500">
Unable to load schema details
</div>
)}
</div>
)}
</div>
)}
{field.description && (
<p className="mt-1 text-sm text-gray-500 break-words whitespace-normal max-w-full">
{field.description}
</p>
)}
<div className="mt-4 p-3 bg-gray-50 text-gray-600 rounded-md text-sm break-words whitespace-normal max-w-full">
<p>
<strong>Note:</strong> To add more schema files, go to the Content
Manager and add files to the API Schema collection.
</p>
</div>
</div>
);
};
export const ApiReferenceTemplate = {
name: "apiReference",
label: "API Reference",
ui: {
defaultItem: {
schemaFile: "test-doc.json",
},
},
fields: [
{
type: "string",
name: "schemaFile",
label: "API Schema",
description:
"Select a Swagger/OpenAPI schema file to display in this component. Optionally select a specific endpoint to display.",
ui: {
component: SchemaSelector,
},
},
],
};

View File

@@ -0,0 +1,27 @@
export const CalloutTemplate = {
name: "Callout",
label: "Callout",
ui: {
defaultItem: {
body: "This is a callout",
variant: "warning",
},
},
fields: [
{
name: "body",
label: "Body",
type: "rich-text",
isBody: true,
},
{
name: "variant",
label: "Variant",
type: "string",
options: ["warning", "info", "success", "error", "idea", "lock", "api"],
defaultValue: "warning",
},
],
};
export default CalloutTemplate;

View File

@@ -0,0 +1,66 @@
export const CardGridTemplate = {
name: "cardGrid",
label: "Card Grid",
ui: {
defaultItem: {
cards: [
{
title: "Card Title",
description: "Card Description",
link: "https://www.google.com",
linkText: "Search now",
},
],
},
},
fields: [
{
name: "cards",
label: "Cards",
type: "object",
list: true,
ui: {
defaultItem: () => {
return {
title: "Card Title",
description: "Card Description",
link: "https://www.google.com",
linkText: "Search now",
};
},
itemProps: (item) => {
return {
label: item.title || "Untitled",
};
},
},
fields: [
{
name: "title",
label: "Title",
type: "string",
},
{
name: "description",
label: "Description",
type: "string",
ui: {
component: "textarea",
},
},
{
name: "link",
label: "Link",
type: "string",
},
{
name: "linkText",
label: "Button Text",
type: "string",
},
],
},
],
};
export default CardGridTemplate;

View File

@@ -0,0 +1,156 @@
import MonacoCodeEditor from "@/tina/customFields/monaco-code-editor";
export const CodeTabsTemplate = {
name: "codeTabs",
label: "Code Tabs",
ui: {
defaultItem: {
tabs: [
{
name: "Query",
content: "const CONTENT_MANAGEMENT = 'Optimized';",
},
{
name: "Response",
content: "const LLAMAS = '100';",
},
],
initialSelectedIndex: 0,
},
},
fields: [
{
type: "object",
name: "tabs",
label: "Tabs",
list: true,
ui: {
itemProps: (item) => ({
label: `🗂️ ${item?.name ?? "Tab"}`,
}),
defaultItem: {
name: "Tab",
content: "const CONTENT_MANAGEMENT = 'Optimized';",
language: "text",
},
},
fields: [
{
type: "string",
name: "name",
label: "Name",
},
{
type: "string",
name: "language",
label: "Code Highlighting Language",
options: [
{
value: "text",
label: "Plain Text",
},
{
value: "javascript",
label: "JavaScript",
},
{
value: "typescript",
label: "TypeScript",
},
{
value: "python",
label: "Python",
},
{
value: "json",
label: "JSON",
},
{
value: "html",
label: "HTML",
},
{
value: "css",
label: "CSS",
},
{
value: "jsx",
label: "JSX",
},
{
value: "tsx",
label: "TSX",
},
{
value: "markdown",
label: "Markdown",
},
{
value: "shell",
label: "Shell",
},
{
value: "sql",
label: "SQL",
},
{
value: "graphql",
label: "GraphQL",
},
{
value: "java",
label: "Java",
},
{
value: "php",
label: "PHP",
},
{
value: "cpp",
label: "C++",
},
{
value: "yaml",
label: "YAML",
},
{
value: "xml",
label: "XML",
},
{
value: "scss",
label: "SCSS",
},
{
value: "vue",
label: "Vue",
},
{
value: "svelte",
label: "Svelte",
},
],
},
{
type: "string",
name: "content",
label: "Content",
ui: {
component: MonacoCodeEditor,
format: (val?: string) => val?.replaceAll("<22>", " "),
parse: (val?: string) => val?.replaceAll(" ", "<22>"),
},
},
],
},
{
type: "number",
name: "initialSelectedIndex",
label: "Initial Selected Index",
description:
"The index of the tab to select by default, starting from 0.",
},
],
};
export default CodeTabsTemplate;

View File

@@ -0,0 +1,45 @@
import { FileStructureField } from "@/tina/customFields/file-structure";
export const FileStructureTemplate = {
name: "fileStructure",
label: "File Structure",
fields: [
{
type: "object",
name: "fileStructure",
label: "File Structure",
ui: {
component: FileStructureField,
},
list: true,
fields: [
{
type: "string",
name: "id",
label: "ID",
},
{
type: "string",
name: "name",
label: "Name",
},
{
type: "string",
name: "type",
label: "Type",
},
{
type: "string",
name: "parentId",
label: "Parent ID",
},
],
},
{
type: "string",
name: "caption",
label: "Caption",
description: "Optional caption that appears under the component",
},
],
};

View File

@@ -0,0 +1,67 @@
import MonacoCodeEditor from "@/tina/customFields/monaco-code-editor";
export const RecipeTemplate = {
name: "recipe",
label: "Code Accordion (Recipe)",
fields: [
{
name: "title",
label: "Heading Title",
type: "string",
},
{
name: "description",
label: "Description",
type: "string",
},
{
type: "string",
name: "code",
label: "Code",
ui: {
component: MonacoCodeEditor,
format: (val?: string) => val?.replaceAll("<22>", " "),
parse: (val?: string) => val?.replaceAll(" ", "<22>"),
},
},
{
name: "instruction",
label: "Instruction",
type: "object",
list: true,
ui: {
itemProps: (item) => {
return { label: item?.header };
},
},
fields: [
{
name: "header",
label: "Header",
type: "string",
},
{
name: "itemDescription",
label: "Item Description",
type: "string",
},
{
name: "codeLineStart",
label: "Code Line Start",
type: "number",
description:
"Enter negative values to highlight from 0 to your end number",
},
{
name: "codeLineEnd",
label: "Code Line End",
type: "number",
description:
"Highlighting will not work if end number is greater than start number",
},
],
},
],
};
export default RecipeTemplate;

View File

@@ -0,0 +1,62 @@
export const ScrollShowcaseTemplate = {
label: "Scroll Showcase",
name: "scrollShowcase",
fields: [
{
type: "object",
label: "Showcase Items",
name: "showcaseItems",
list: true,
ui: {
defaultItem: {
title: "Title",
image: "/img/rico-replacement.jpg",
content: {
type: "root",
children: [
{
type: "p",
children: [
{
type: "text",
text: "Default Text. Edit me!",
},
],
},
],
},
useAsSubsection: false,
},
itemProps: (item) => {
return {
label: item.title,
};
},
},
fields: [
{
type: "image",
label: "Image",
name: "image",
},
{
type: "string",
label: "Title",
name: "title",
},
{
type: "boolean",
label: "Use as Subsection",
name: "useAsSubsection",
},
{
type: "rich-text",
label: "Content",
name: "content",
},
],
},
],
};
export default ScrollShowcaseTemplate;

View File

@@ -0,0 +1,53 @@
export const TypeDefinitionTemplate = {
name: "typeDefinition",
label: "Type Definition",
fields: [
{
type: "object",
name: "property",
label: "Property",
list: true,
ui: {
itemProps: (item) => {
return {
label: item.name,
};
},
},
fields: [
{
type: "string",
name: "name",
label: "Name",
},
{
type: "rich-text",
name: "description",
label: "Description",
},
{
type: "string",
name: "type",
label: "Type",
},
{
type: "string",
name: "typeUrl",
label: "Type URL",
description:
"Turns the type into a link for further context. Useful for deeply nested types.",
},
{
type: "boolean",
name: "required",
label: "Required",
},
{
type: "boolean",
name: "experimental",
label: "Experimental",
},
],
},
],
};

View File

@@ -0,0 +1,34 @@
export const YoutubeTemplate = {
name: "youtube",
label: "Youtube Video",
ui: {
defaultItem: {
embedSrc: "https://www.youtube.com/embed/CsCQS7HIBv0?si=os9ona92O2VMOl-V",
caption: "Seth goes over the basics of using TinaCMS",
minutes: "2",
},
},
fields: [
{
type: "string",
name: "embedSrc",
label: "Embed URL",
description:
"⚠︎ Only YouTube embed URLs work - they look like this https://www.youtube.com/embed/Yoh2c5RUTiY",
},
{
type: "string",
name: "caption",
label: "Caption",
description: "The caption of the video",
},
{
type: "string",
name: "minutes",
label: "Minutes",
description: "The duration of the video in minutes",
},
],
};
export default YoutubeTemplate;

View File

@@ -0,0 +1,35 @@
import type { Template } from "tinacms";
import { titleCase } from "title-case";
export const itemTemplate: Template = {
label: "Item",
name: "item",
ui: {
itemProps: (item) => {
return {
label: `🔗 ${titleCase(
item?.slug?.split("/").at(-1).split(".").at(0).replaceAll("-", " ") ??
"Unnamed Menu Item"
)}`,
};
},
},
fields: [
{
name: "slug",
label: "Page",
type: "reference",
collections: ["docs"],
},
],
};
export const submenusLabel: Pick<Template, "label" | "name" | "ui"> = {
label: "Submenu",
name: "items",
ui: {
itemProps: (item) => ({
label: `🗂️ ${item?.title ?? "Unnamed Menu Group"}`,
}),
},
};

View File

@@ -0,0 +1,37 @@
import type { Template } from "tinacms";
import { itemTemplate } from "./navbar-ui.template";
const createMenuTemplate = (
templates: Template[],
level: number
): Template => ({
label: "Submenu",
name: "items",
ui: {
itemProps: (item) => {
return { label: `🗂️ ${level} | ${item?.title ?? "Unnamed Menu Group"}` };
},
},
fields: [
{ name: "title", label: "Name", type: "string" },
{
name: "items",
label: "Submenu Items",
type: "object",
list: true,
templates,
},
],
});
const thirdLevelSubmenu: Template = createMenuTemplate([itemTemplate], 3);
const secondLevelSubmenu: Template = createMenuTemplate(
[thirdLevelSubmenu, itemTemplate],
2
);
export const submenuTemplate: Template = createMenuTemplate(
[secondLevelSubmenu, itemTemplate],
1
);
export default submenuTemplate;