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,20 @@
import { wrapFieldsWithMeta } from "tinacms";
import { JsonFileUploadComponent } from "../customFields/file-upload";
export const API_Schema_Collection = {
name: "apiSchema",
label: "API Schema",
path: "content/apiSchema",
format: "json",
fields: [
{
name: "apiSchema",
label: "API Schema",
type: "string",
ui: { component: wrapFieldsWithMeta(JsonFileUploadComponent) },
},
],
};
export default API_Schema_Collection;

105
tina/collections/docs.tsx Normal file
View File

@@ -0,0 +1,105 @@
import AccordionTemplate, {
AccordionBlockTemplate,
} from "@/tina/templates/markdown-embeds/accordion.template";
import { ApiReferenceTemplate } from "@/tina/templates/markdown-embeds/api-reference.template";
import CalloutTemplate from "@/tina/templates/markdown-embeds/callout.template";
import CardGridTemplate from "@/tina/templates/markdown-embeds/card-grid.template";
import CodeTabsTemplate from "@/tina/templates/markdown-embeds/code-tabs.template";
import { FileStructureTemplate } from "@/tina/templates/markdown-embeds/file-structure.template";
import RecipeTemplate from "@/tina/templates/markdown-embeds/recipe.template";
import ScrollShowcaseTemplate from "@/tina/templates/markdown-embeds/scroll-showcase.template";
import { TypeDefinitionTemplate } from "@/tina/templates/markdown-embeds/type-definition.template";
import YoutubeTemplate from "@/tina/templates/markdown-embeds/youtube.template";
import type { Template } from "tinacms";
import SeoInformation from "./seo-information";
export const docsCollection = {
name: "docs",
label: "Docs",
path: "content/docs",
format: "mdx",
ui: {
beforeSubmit: async ({ values }) => {
return {
...values,
last_edited: new Date().toISOString(),
auto_generated: false,
};
},
router: ({ document }) => {
if (document._sys.filename === "index") {
return "/";
}
const slug = document._sys.breadcrumbs.join("/");
return `/docs/${slug}`;
},
filename: {
slugify: (values) => {
return (
values?.title
?.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "") // Remove special characters except spaces and dashes
.replace(/\s+/g, "-") // Replace spaces with dashes
.replace(/-+/g, "-") // Replace multiple dashes with single dash
.replace(/^-|-$/g, "") || // Remove leading/trailing dashes
""
);
},
},
},
fields: [
SeoInformation,
{
name: "title",
label: "Title",
type: "string",
isTitle: true,
required: true,
},
{
type: "string",
name: "last_edited",
label: "Last Edited",
ui: {
component: "hidden",
},
},
{
type: "boolean",
name: "auto_generated",
label: "Auto Generated",
description: "Indicates if this document was automatically generated",
ui: {
component: "hidden",
},
},
{
type: "boolean",
name: "tocIsHidden",
label: "Hide Table of Contents",
description:
"Hide the Table of Contents on this page and expand the content window.",
},
{
type: "rich-text",
name: "body",
label: "Body",
isBody: true,
templates: [
ScrollShowcaseTemplate as Template,
CardGridTemplate as Template,
RecipeTemplate as Template,
AccordionTemplate as Template,
AccordionBlockTemplate as Template,
ApiReferenceTemplate as Template,
YoutubeTemplate as Template,
CodeTabsTemplate as Template,
CalloutTemplate as Template,
TypeDefinitionTemplate as Template,
FileStructureTemplate as unknown as Template,
],
},
],
};
export default docsCollection;

View File

@@ -0,0 +1,206 @@
import { getBearerAuthHeader } from "@/src/utils/tina/get-bearer-auth-header";
import { ApiReferencesSelector } from "../customFields/api-reference-selector";
import { itemTemplate } from "../templates/navbar-ui.template";
import submenuTemplate from "../templates/submenu.template";
const docsNavigationBarFields = [
{
name: "title",
label: "Title Label",
type: "string",
},
{
name: "supermenuGroup",
label: "Supermenu Group",
type: "object",
list: true,
ui: {
itemProps: (item) => ({
label: `🗂️ ${item?.title ?? "Unnamed Menu Group"}`,
}),
},
fields: [
{ name: "title", label: "Name", type: "string" },
{
name: "items",
label: "Page or Submenu",
type: "object",
list: true,
templates: [submenuTemplate, itemTemplate],
},
],
},
];
const documentSubMenuTemplate = {
name: "documentSubMenu",
label: "Document Submenu",
fields: [
{ name: "title", label: "Name", type: "string" },
{
name: "items",
label: "Items",
type: "object",
list: true,
templates: [itemTemplate],
},
],
};
const groupOfApiReferencesTemplate = {
name: "groupOfApiReferences",
label: "Group of API References",
fields: [
{
type: "string",
name: "apiGroup",
label: "API Group",
ui: {
component: ApiReferencesSelector,
},
},
],
};
const apiNavigationBarFields = [
{
name: "title",
label: "title",
type: "string",
},
{
name: "supermenuGroup",
label: "Supermenu Group",
type: "object",
list: true,
templates: [documentSubMenuTemplate, groupOfApiReferencesTemplate],
},
];
const docsTabTemplate = {
name: "docsTab",
label: "Docs Tab",
fields: docsNavigationBarFields,
};
const apiTabTemplate = {
name: "apiTab",
label: "API Tab",
fields: apiNavigationBarFields,
};
export const docsNavigationBarCollection = {
name: "navigationBar",
label: "Navigation Bar",
path: "content/navigation-bar",
format: "json",
ui: {
allowedActions: {
create: false,
delete: false,
},
beforeSubmit: async ({ values }: { values: Record<string, any> }) => {
try {
// Generate .mdx files for API endpoints when navigation is saved
const response = await fetch("/api/process-api-docs", {
method: "POST",
headers: getBearerAuthHeader(),
body: JSON.stringify({
data: values,
}),
});
if (response.ok) {
const result = await response.json();
} else {
const error = await response.json();
// Log error but don't block the save operation
}
// Always return the values, don't block the save operation if file generation fails
return {
...values,
};
} catch (error) {
// Don't block the save operation if file generation fails
return {
...values,
};
}
},
},
fields: [
{
name: "lightModeLogo",
label: "Light Mode Logo",
type: "image",
},
{
name: "darkModeLogo",
label: "Dark Mode Logo",
type: "image",
description: "If your light mode logo fits dark-mode, leave this blank.",
},
{
name: "tabs",
label: "Tabs",
type: "object",
list: true,
ui: {
itemProps: (item) => ({
label: `🗂️ ${item?.title ?? "Unnamed Tab"}`,
}),
},
templates: [docsTabTemplate, apiTabTemplate],
},
{
name: "ctaButtons",
label: "CTA Buttons",
type: "object",
fields: [
{
name: "button1",
label: "Button 1",
type: "object",
fields: [
{ label: "Label", name: "label", type: "string" },
{ label: "Link", name: "link", type: "string" },
{
label: "variant",
name: "variant",
type: "string",
options: [
"primary-background",
"secondary-background",
"primary-outline",
"secondary-outline",
],
},
],
},
{
name: "button2",
label: "Button 2",
type: "object",
fields: [
{ label: "Label", name: "label", type: "string" },
{ label: "Link", name: "link", type: "string" },
{
label: "variant",
name: "variant",
type: "string",
options: [
"primary-background",
"secondary-background",
"primary-outline",
"secondary-outline",
],
},
],
},
],
},
],
};
export default docsNavigationBarCollection;

View File

@@ -0,0 +1,48 @@
import { TextInputWithCount } from "../customFields/text-input-with-count";
export const SeoInformation = {
type: "object",
label: "SEO Values",
name: "seo",
fields: [
{
type: "string",
label: "Meta - Title",
description: "Recommended limit of 70 characters",
name: "title",
ui: {
validate: (value) => {
if (value && value.length > 70) {
return "Title should be 70 characters or less";
}
},
component: TextInputWithCount(70),
},
},
{
type: "string",
label: "Meta - Description",
description: "Recommended limit of 150 characters",
name: "description",
component: "textarea",
ui: {
component: TextInputWithCount(150, true),
},
},
{
type: "string",
label: "Canonical URL",
name: "canonicalUrl",
description: "Default URL if no URL is provided",
},
{
type: "image",
label: "Open Graph Image",
name: "ogImage",
uploadDir: () => "og",
description: "Default image if no image is provided",
},
],
};
export default SeoInformation;

View File

@@ -0,0 +1,179 @@
import { BROWSER_TAB_THEME_KEY } from "@/src/components/ui/theme-selector";
import React from "react";
import { RedirectItem } from "../customFields/redirect-item";
import { ThemeSelector } from "../customFields/theme-selector";
export const Settings = {
name: "settings",
label: "Settings",
path: "content/settings",
format: "json",
ui: {
global: true,
allowedActions: {
create: false,
delete: false,
},
defaultItem: {
autoCapitalizeNavigation: true,
},
beforeSubmit: async ({ values }: { values: Record<string, any> }) => {
sessionStorage.setItem(BROWSER_TAB_THEME_KEY, values.selectedTheme);
},
},
fields: [
{
name: "selectedTheme",
label: "Selected Theme",
description: "Choose your website's visual theme with color previews",
type: "string",
ui: {
component: ThemeSelector,
},
},
{
name: "redirects",
label: "Redirects",
type: "object",
list: true,
ui: {
itemProps: (item) => {
return {
label:
item.source && item.destination ? (
<RedirectItem
source={item.source}
destination={item.destination}
permanent={item.permanent}
/>
) : (
"Add Redirect"
),
};
},
},
fields: [
{
name: "source",
label: "Source",
type: "string",
ui: {
validate: (value) => {
if (!value?.startsWith("/")) {
return "Source path must start with /";
}
},
},
},
{
name: "destination",
label: "Destination",
type: "string",
ui: {
validate: (value) => {
if (!value?.startsWith("/")) {
return "Destination path must start with /";
}
},
},
},
{
name: "permanent",
label: "Permanent",
type: "boolean",
},
],
},
{
name: "title",
label: "Title",
type: "string",
},
{
name: "description",
label: "Description",
type: "string",
},
{
name: "seoDefaultTitle",
label: "SEO Default Title",
type: "string",
},
{
name: "publisher",
label: "Publisher",
type: "string",
},
{
name: "applicationName",
label: "Application Name",
type: "string",
},
{
name: "siteUrl",
label: "Site URL",
type: "string",
},
{
name: "roadmapUrl",
label: "Roadmap URL",
type: "string",
},
{
name: "licenseUrl",
label: "License URL",
type: "string",
},
{
name: "keywords",
label: "Keywords",
type: "string",
},
{
name: "docsHomepage",
label: "Docs Homepage",
type: "string",
},
{
name: "autoApiTitles",
label: "Auto-Capitalize Titles",
description:
"Auto-capitalize titles in the navigation bar and generated API pages",
type: "boolean",
defaultValue: true,
},
{
name: "defaultOGImage",
label: "Default OG Image",
type: "image",
uploadDir: () => "og",
},
{
name: "social",
label: "Social",
type: "object",
fields: [
{
name: "twitterHandle",
label: "Twitter Handle",
type: "string",
},
{
name: "twitter",
label: "Twitter",
type: "string",
},
{
name: "github",
label: "GitHub",
type: "string",
},
{
name: "forum",
label: "Forum",
type: "string",
},
],
},
],
};

View File

@@ -0,0 +1,166 @@
import { CustomColorToggle } from "@/components/ui/custom-color-toggle";
export const GlobalSiteConfiguration = {
name: "globalSiteConfiguration",
label: "Global Site Configuration",
ui: {
global: true,
allowedActions: {
create: false,
delete: false,
},
},
path: "content/site-config",
format: "json",
fields: [
{
name: "docsConfig",
label: "Docs Config",
type: "object",
fields: [
{
name: "documentationSiteTitle",
label: "Documentation Site Title",
type: "string",
},
],
},
{
name: "colorScheme",
label: "Color Scheme",
type: "object",
fields: [
{
name: "siteColors",
label: "Site Colors",
type: "object",
defaultItem: () => {
return {
primaryStart: "#f97316",
primaryEnd: "#f97316",
primaryVia: "#f97316",
};
},
fields: [
{
name: "primaryStart",
label: "Primary Color | Gradient Start",
type: "string",
description:
"This is the start of the primary color gradient ⚠️ If you want a solid color leave the end and via empty ⚠️",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
{
name: "primaryEnd",
label: "Primary Color | Gradient End",
type: "string",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
{
name: "primaryVia",
label: "Primary Color | Gradient Via",
type: "string",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
{
name: "secondaryStart",
label: "Secondary Color | Gradient Start",
type: "string",
description:
"This is the start of the secondary color gradient ⚠️ If you want a solid color leave the end and via empty ⚠️",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
{
name: "secondaryEnd",
label: "Secondary Color | Gradient End",
type: "string",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
{
name: "secondaryVia",
label: "Secondary Color | Gradient Via",
type: "string",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
{
name: "rightHandSideActiveColor",
label: "Right Hand Side ToC Active Color",
type: "string",
ui: {
component: "color",
colorFormat: "hex",
},
},
{
name: "rightHandSideInactiveColor",
label: "Right Hand Side ToC Inactive Color",
type: "string",
ui: {
component: "color",
colorFormat: "hex",
},
},
],
},
{
name: "customColorToggle",
label: "Custom Color Toggle",
type: "object",
fields: [
{
name: "disableColor",
label: "Tick to use Default Background Color",
type: "boolean",
},
{
name: "colorValue",
label: "Color Value",
type: "string",
},
],
ui: {
component: CustomColorToggle,
},
},
{
name: "leftSidebarBackground",
label: "Left Sidebar Background",
type: "string",
description: "This is the background color of the left sidebar",
ui: {
component: "color",
colorFormat: "hex",
widget: "sketch",
},
},
],
},
],
};
export default GlobalSiteConfiguration;