Compare commits

...

2 Commits

Author SHA1 Message Date
johannes.gasser 2a0f7d5bcf add missing files
Build and Deploy / build-and-deploy (push) Failing after 2m42s
2026-05-07 09:46:44 +02:00
johannes.gasser bf04f8679d switch from tinacms to sanity 2026-05-07 09:36:34 +02:00
43 changed files with 10954 additions and 23201 deletions
+9
View File
@@ -0,0 +1,9 @@
# Sanity CMS Configuration
# Get your project ID from https://sanity.io/manage
PUBLIC_SANITY_PROJECT_ID=p49jotqu
PUBLIC_SANITY_DATASET=production
# Only needed when running the migration script (scripts/migrate-to-sanity.mjs)
# Create a write token at https://sanity.io/manage → API → Tokens
# Revoke after migration is complete.
SANITY_API_TOKEN=skQ3tzSF8tp5ETeDq26zlgYAoxv50L4D37uITONtQ0efe4wHxjH64NrauljoyucMGy0F58fOXmUbQXh7C0glLFcD0PUd0tVRwzifuuLYvlZmX0MKOo6aZB2U2P5ISydRNzKd0sPGyMjxRo47psGb3wCtzT0Xcv8ByMVOlqnn35bc6uDj7Afl
-7
View File
@@ -1,7 +0,0 @@
# TinaCMS Configuration
# For local development with Git-based workflow, these can be left empty
# For TinaCMS Cloud, get these values from https://app.tina.io
TINA_CLIENT_ID=
TINA_TOKEN=
TINA_BRANCH=main
+1 -14
View File
@@ -39,16 +39,6 @@ jobs:
${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:${{ steps.tag.outputs.sha }}
${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:latest
- name: Build and push backend image
uses: docker/build-push-action@v5
with:
context: .
target: backend
push: true
tags: |
${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:${{ steps.tag.outputs.sha }}
${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:latest
- name: Update image tags in gitops repo
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
@@ -63,10 +53,7 @@ jobs:
sed -i \
"s|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:.*|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:${SHA}|" \
"apps/${{ env.APP }}/deployment.yaml"
sed -i \
"s|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:.*|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:${SHA}|" \
"apps/${{ env.APP }}/tina-backend-deployment.yaml"
git add "apps/${{ env.APP }}/deployment.yaml" "apps/${{ env.APP }}/tina-backend-deployment.yaml"
git add "apps/${{ env.APP }}/deployment.yaml"
git diff --cached --quiet && echo "No changes, skipping commit." && exit 0
git commit -m "ci: update ${{ env.APP }} to ${SHA}"
git push origin main
-1
View File
@@ -16,7 +16,6 @@ pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
+4 -16
View File
@@ -1,26 +1,14 @@
# Shared build: generates /dist (Astro site + admin SPA) and tina/__generated__
# Build: generates /dist (Astro static site + Sanity Studio SPA)
FROM node:24 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --legacy-peer-deps
RUN npm ci
COPY . .
ENV TINA_PUBLIC_IS_LOCAL=true
RUN npm run build:docker
RUN npm run build
# Frontend: Nginx serving the static site + TinaCMS admin SPA
# Frontend: Nginx serving the static site + Sanity Studio SPA
FROM nginx:alpine AS frontend
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
# Backend: TinaCMS self-hosted API server
FROM node:24 AS backend
WORKDIR /app
COPY package*.json ./
RUN npm ci --legacy-peer-deps --omit=dev
COPY backend/ ./backend/
COPY --from=builder /app/tina/__generated__ ./tina/__generated__
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "backend/index.js"]
+14 -3
View File
@@ -1,12 +1,23 @@
// @ts-check
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import { loadEnv } from 'vite';
import sanity from '@sanity/astro';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
const env = loadEnv(process.env.NODE_ENV ?? 'development', process.cwd(), '');
// https://astro.build/config
export default defineConfig({
integrations: [mdx()],
integrations: [
sanity({
projectId: env.PUBLIC_SANITY_PROJECT_ID,
dataset: env.PUBLIC_SANITY_DATASET ?? 'production',
studioBasePath: '/studio',
useCdn: false,
}),
react(),
],
vite: {
plugins: [tailwindcss()],
-50
View File
@@ -1,50 +0,0 @@
import express from 'express'
import { TinaNodeBackend, LocalBackendAuthProvider, createDatabase, createLocalDatabase } from '@tinacms/datalayer'
import { AuthJsBackendAuthProvider, TinaAuthJSOptions } from 'tinacms-authjs'
import { MongodbLevel } from 'mongodb-level'
import { GitHubProvider } from 'tinacms-gitprovider-github'
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === 'true'
const branch = process.env.GITEA_BRANCH || 'main'
const databaseClient = isLocal
? createLocalDatabase()
: createDatabase({
gitProvider: new GitHubProvider({
branch,
owner: process.env.GITEA_OWNER,
repo: process.env.GITEA_REPO,
token: process.env.GITEA_TOKEN,
octokitOptions: {
baseUrl: 'https://git.idealconnected.com/api/v1',
},
}),
databaseAdapter: new MongodbLevel({
collectionName: 'tinacms',
dbName: 'tinacms',
mongoUri: process.env.MONGODB_URI,
}),
})
const authProvider = isLocal
? LocalBackendAuthProvider()
: AuthJsBackendAuthProvider({
authOptions: TinaAuthJSOptions({
databaseClient,
secret: process.env.NEXTAUTH_SECRET,
}),
})
const tinaHandler = TinaNodeBackend({
authProvider,
databaseClient,
})
const app = express()
app.use(express.json())
// Mount at root — TinaNodeBackend routes /api/tina/* internally
app.all('*', (req, res) => tinaHandler(req, res))
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Tina backend running on port ${port}`))
-26
View File
@@ -1,26 +0,0 @@
---
title: Welcome to Theater Ziefen
date: 2026-01-28T00:00:00.000Z
excerpt: We're excited to announce our upcoming theatre production in 2029. Stay tuned for more updates!
coverImage: /images/theater-placeholder.jpg
---
# Welcome to Theater Ziefen
We are thrilled to welcome you to the official website of Theater Ziefen. Our theatre production is scheduled to take place in approximately three years, and we can't wait to share this journey with you.
## What to Expect
This blog will keep you updated on:
- Production developments and announcements
- Behind-the-scenes insights
- Cast and crew introductions
- Ticket information (coming soon)
- Event schedules
## Stay Connected
Make sure to check back regularly for updates, or subscribe to our newsletter to receive the latest news directly in your inbox.
We look forward to bringing you an unforgettable theatrical experience!
-27
View File
@@ -1,27 +0,0 @@
---
title: About Theater Ziefen
description: Learn more about our upcoming theatre production and the team behind it
---
# About Theater Ziefen
Theater Ziefen is an exciting theatrical production planned for 2029. Our mission is to bring exceptional live theatre to our community and create memorable experiences for audiences of all ages.
## Our Vision
We believe in the power of theatre to inspire, entertain, and bring people together. Our upcoming production will showcase local talent and creativity while delivering a professional, high-quality performance.
## The Production
Details about our production will be announced in the coming months. Stay tuned to our blog for updates on:
- Show dates and times
- Venue information
- Cast announcements
- Ticket sales
## Get Involved
Interested in being part of Theater Ziefen? We're always looking for passionate individuals to join our team. Whether you're interested in performing, helping backstage, or volunteering, we'd love to hear from you.
Contact us for more information about opportunities to get involved.
-92
View File
@@ -1,92 +0,0 @@
---
title: Freilichttheater Ziefen 2028
hero:
title_de: Freilichttheater in Ziefen
title_en: Open-Air Theater in Ziefen
subtitle_de: '2028 grosses Freilichtspiel geplant. Der Blick auf die Gesellschaft der damaligen Zeit aus der Sicht des in Ziefen aufgewachsenen späteren Pfarrers, Waisenvaters und Mundartautors… Jonas Breitenstein'
subtitle_en: 'A grand open-air play planned for 2028. A view of society of that time from the perspective of Jonas Breitenstein, who grew up in Ziefen and later became a pastor, orphanage father, and dialect author.'
projekt:
title_de: Das Projekt
title_en: The Project
content_de: ''
content_en: ''
team:
title_de: Wer steckt dahinter
title_en: Who's Behind It
content_de: ''
content_en: ''
organizations:
- name: Verein J.B.
role_de: Hauptverantwortlich für das Theaterprojekt
role_en: Main responsibility for the theater project
logo: /images/Frontispitz_SW_clean_web.png
url: 'https://jonas-breitenstein.ch/verein.html'
- name: Verein 4417
role_de: Mitorganisator
role_en: Co-organizer
logo: /images/img_medium.jpg
url: 'https://www.ziefen.ch/freizeit-kultur/vereine.html/87/association/23'
- name: Bürgergemeinde
role_de: Unterstützer
role_en: Supporter
logo: /images/Wappen Ziefen.png
url: 'https://bg-ziefen.ch/'
- name: Einwohnergemeinde
role_de: Unterstützer
role_en: Supporter
logo: /images/Wappen Ziefen.png
url: 'https://www.ziefen.ch/'
stueck:
title_de: Das Stück
title_en: The Play
content_de: ''
content_en: ''
mitwirkung:
title_de: Mitwirkung im Schauspiel
title_en: Participation in the Play
content_de: ''
content_en: ''
director_name: Danny Wehrmüller
director_email: director@theater-ziefen.ch
director_phone: +41 XX XXX XX XX
termine:
title_de: Termine
title_en: Schedule
events:
- title_de: Casting
title_en: Casting
date: Frühjahr 2027
description_de: Gruppencasting zum gegenseitigen Kennenlernen
description_en: Group casting to get to know each other
- title_de: Proben
title_en: Rehearsals
date: Ab Oktober 2027
description_de: Beginn der regelmässigen Proben
description_en: Start of regular rehearsals
- title_de: Intensive Proben
title_en: Intensive Rehearsals
date: Ab Mai 2028
description_de: 'WICHTIG: Keine Ferien oder längere Abwesenheit planen! Mit zwei Probewochenenden.'
description_en: 'IMPORTANT: Do not plan vacations or longer absences! Includes two rehearsal weekends.'
- title_de: Vorstellungen
title_en: Performances
date: Juni 2028
description_de: Acht bis neun Vorstellungen
description_en: Eight to nine performances
presse:
title_de: Presse
title_en: Press
content_de: ''
content_en: ''
gallery:
title_de: Foto Galerie
title_en: Photo Gallery
images: []
tickets:
title_de: Tickets
title_en: Tickets
content_de: ''
content_en: ''
ticket_url: ''
---
-10
View File
@@ -1,10 +0,0 @@
{
"users": [
{
"name": "Admin",
"email": "admin@theater-jb.ch",
"username": "admin",
"password": "changeme"
}
]
}
+10249 -19493
View File
File diff suppressed because it is too large Load Diff
+17 -20
View File
@@ -3,35 +3,32 @@
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "tinacms dev -c \"astro dev\"",
"dev": "astro dev",
"build": "astro check && astro build",
"build:tina": "tinacms build && astro check && astro build",
"build:docker": "tinacms build && astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"migrate": "node --env-file=.env scripts/migrate-to-sanity.mjs"
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/mdx": "^5.0.4",
"@astrojs/react": "^5.0.4",
"@portabletext/to-html": "^5.0.2",
"@sanity/astro": "^3.4.0",
"@sanity/client": "^7.22.0",
"@sanity/image-url": "^2.1.1",
"@sanity/vision": "^3.0.0",
"@tailwindcss/vite": "^4.2.4",
"@tinacms/cli": "^2.2.5",
"@tinacms/datalayer": "^2.0.18",
"fs-extra": "^9.1.0",
"astro": "^6.2.1",
"express": "^5.2.1",
"mongodb-level": "^0.0.4",
"next": "^15.5.15",
"next-auth": "^4.24.14",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"slate": "^0.124.1",
"slate-dom": "^0.124.1",
"slate-hyperscript": "^0.100.0",
"slate-react": "^0.124.0",
"react-is": "^19.0.0",
"sanity": "^5.24.0",
"styled-components": "^6.1.19",
"tailwindcss": "^4.2.4",
"tinacms": "^3.7.5",
"tinacms-authjs": "^21.0.5",
"tinacms-gitprovider-github": "^4.1.5",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"vite": "^7"
},
"devDependencies": {
"@portabletext/markdown": "^1.2.0"
}
}
+30
View File
@@ -0,0 +1,30 @@
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './src/sanity/schemas'
export default defineConfig({
projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
dataset: import.meta.env.PUBLIC_SANITY_DATASET ?? 'production',
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Theater Homepage')
.child(
S.document()
.schemaType('theaterHomepage')
.documentId('theaterHomepage')
),
S.divider(),
S.documentTypeListItem('post').title('Blog Posts'),
S.documentTypeListItem('page').title('Pages'),
]),
}),
visionTool(),
],
schema: { types: schemaTypes },
})
+257
View File
@@ -0,0 +1,257 @@
/**
* One-shot migration script: TinaCMS MDX files → Sanity documents
*
* Prerequisites:
* npm install --save-dev gray-matter @portabletext/markdown
*
* Usage:
* npm run migrate
*
* Revoke SANITY_API_TOKEN after migration is complete.
*/
import { createClient } from '@sanity/client'
import { createReadStream, readdirSync, readFileSync, existsSync } from 'fs'
import { join, basename } from 'path'
import matter from 'gray-matter'
import { markdownToPortableText } from '@portabletext/markdown'
const PROJECT_ID = process.env.PUBLIC_SANITY_PROJECT_ID
const TOKEN = process.env.SANITY_API_TOKEN
const DATASET = process.env.PUBLIC_SANITY_DATASET ?? 'production'
if (!PROJECT_ID || !TOKEN) {
console.error('Missing PUBLIC_SANITY_PROJECT_ID or SANITY_API_TOKEN environment variables.')
process.exit(1)
}
const client = createClient({
projectId: PROJECT_ID,
dataset: DATASET,
apiVersion: '2024-01-01',
token: TOKEN,
useCdn: false,
})
function randomKey() {
return Math.random().toString(36).slice(2, 10)
}
function emptyPortableText() {
return []
}
function stringToPortableText(text) {
if (!text || text === '') return emptyPortableText()
return [
{
_type: 'block',
_key: randomKey(),
style: 'normal',
markDefs: [],
children: [{ _type: 'span', _key: randomKey(), text, marks: [] }],
},
]
}
// Upload a local image file to Sanity and return a Sanity image reference
async function uploadImageFile(localPath, label) {
if (!existsSync(localPath)) {
console.warn(` Skipping missing image: ${localPath}`)
return null
}
console.log(` Uploading image: ${label}`)
const asset = await client.assets.upload('image', createReadStream(localPath), {
filename: basename(localPath),
})
return { _type: 'image', asset: { _type: 'reference', _ref: asset._id } }
}
// Map /images/filename.ext → public/images/filename.ext (relative to project root)
function localPathForImageUrl(imageUrl) {
if (!imageUrl || !imageUrl.startsWith('/images/')) return null
return join('public', imageUrl)
}
// ---------------------------------------------------------------------------
// Migrate theaterHomepage singleton
// ---------------------------------------------------------------------------
async function migrateTheaterHomepage() {
console.log('\n--- Migrating theaterHomepage ---')
const raw = readFileSync('content/theater/homepage.mdx', 'utf-8')
const { data: fm } = matter(raw)
// Pre-upload all organization logos
const orgs = (fm.team?.organizations ?? [])
const uploadedLogos = {}
for (const org of orgs) {
if (org.logo && !uploadedLogos[org.logo]) {
const localPath = localPathForImageUrl(org.logo)
if (localPath) {
uploadedLogos[org.logo] = await uploadImageFile(localPath, org.logo)
}
}
}
const doc = {
_type: 'theaterHomepage',
_id: 'theaterHomepage',
title: fm.title,
hero: fm.hero ?? {},
projekt: {
title_de: fm.projekt?.title_de ?? '',
title_en: fm.projekt?.title_en ?? '',
content_de: stringToPortableText(fm.projekt?.content_de),
content_en: stringToPortableText(fm.projekt?.content_en),
},
team: {
title_de: fm.team?.title_de ?? '',
title_en: fm.team?.title_en ?? '',
content_de: stringToPortableText(fm.team?.content_de),
content_en: stringToPortableText(fm.team?.content_en),
organizations: orgs.map((org) => ({
_key: randomKey(),
name: org.name,
role_de: org.role_de,
role_en: org.role_en,
url: org.url,
...(org.logo && uploadedLogos[org.logo] ? { logo: uploadedLogos[org.logo] } : {}),
})),
},
stueck: {
title_de: fm.stueck?.title_de ?? '',
title_en: fm.stueck?.title_en ?? '',
content_de: stringToPortableText(fm.stueck?.content_de),
content_en: stringToPortableText(fm.stueck?.content_en),
},
mitwirkung: {
title_de: fm.mitwirkung?.title_de ?? '',
title_en: fm.mitwirkung?.title_en ?? '',
content_de: stringToPortableText(fm.mitwirkung?.content_de),
content_en: stringToPortableText(fm.mitwirkung?.content_en),
director_name: fm.mitwirkung?.director_name,
director_email: fm.mitwirkung?.director_email,
director_phone: fm.mitwirkung?.director_phone,
},
termine: {
title_de: fm.termine?.title_de ?? '',
title_en: fm.termine?.title_en ?? '',
events: (fm.termine?.events ?? []).map((e) => ({ ...e, _key: randomKey() })),
},
presse: {
title_de: fm.presse?.title_de ?? '',
title_en: fm.presse?.title_en ?? '',
content_de: stringToPortableText(fm.presse?.content_de),
content_en: stringToPortableText(fm.presse?.content_en),
},
gallery: {
title_de: fm.gallery?.title_de ?? '',
title_en: fm.gallery?.title_en ?? '',
images: [],
},
tickets: {
title_de: fm.tickets?.title_de ?? '',
title_en: fm.tickets?.title_en ?? '',
content_de: stringToPortableText(fm.tickets?.content_de),
content_en: stringToPortableText(fm.tickets?.content_en),
...(fm.tickets?.ticket_url ? { ticket_url: fm.tickets.ticket_url } : {}),
},
}
await client.createOrReplace(doc)
console.log(' ✓ theaterHomepage created/replaced')
}
// ---------------------------------------------------------------------------
// Migrate blog posts
// ---------------------------------------------------------------------------
async function migratePosts() {
console.log('\n--- Migrating blog posts ---')
const blogDir = 'content/blog'
if (!existsSync(blogDir)) {
console.log(' No blog directory found, skipping.')
return
}
const files = readdirSync(blogDir).filter((f) => f.endsWith('.mdx'))
for (const file of files) {
const slug = file.replace('.mdx', '')
const raw = readFileSync(join(blogDir, file), 'utf-8')
const { data: fm, content: body } = matter(raw)
const portableTextBody = body.trim()
? markdownToPortableText(body)
: []
// Upload cover image if present and exists locally
let coverImage
if (fm.coverImage) {
const localPath = localPathForImageUrl(fm.coverImage)
if (localPath) {
coverImage = await uploadImageFile(localPath, fm.coverImage)
}
}
const doc = {
_type: 'post',
_id: `post-${slug}`,
title: fm.title,
slug: { _type: 'slug', current: slug },
date: fm.date,
excerpt: fm.excerpt,
body: portableTextBody,
...(coverImage ? { coverImage } : {}),
}
await client.createOrReplace(doc)
console.log(` ✓ Post migrated: ${slug}`)
}
}
// ---------------------------------------------------------------------------
// Migrate static pages
// ---------------------------------------------------------------------------
async function migratePages() {
console.log('\n--- Migrating pages ---')
const pagesDir = 'content/pages'
if (!existsSync(pagesDir)) {
console.log(' No pages directory found, skipping.')
return
}
const files = readdirSync(pagesDir).filter((f) => f.endsWith('.mdx'))
for (const file of files) {
const slug = file.replace('.mdx', '')
const raw = readFileSync(join(pagesDir, file), 'utf-8')
const { data: fm, content: body } = matter(raw)
const portableTextBody = body.trim()
? markdownToPortableText(body)
: []
const doc = {
_type: 'page',
_id: `page-${slug}`,
title: fm.title,
slug: { _type: 'slug', current: slug },
description: fm.description,
body: portableTextBody,
}
await client.createOrReplace(doc)
console.log(` ✓ Page migrated: ${slug}`)
}
}
// ---------------------------------------------------------------------------
// Run
// ---------------------------------------------------------------------------
try {
await migrateTheaterHomepage()
await migratePosts()
await migratePages()
console.log('\n✓ Migration complete.')
} catch (err) {
console.error('\n✗ Migration failed:', err)
process.exit(1)
}
+4 -2
View File
@@ -1,6 +1,8 @@
---
import { urlFor } from '../sanity/imageUrl';
interface Props {
images: string[];
images: any[];
}
const { images } = Astro.props;
@@ -10,7 +12,7 @@ const { images } = Astro.props;
<div class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] md:grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-[--spacing-md] md:gap-[--spacing-sm] mt-[--spacing-lg]">
{images.map((image) => (
<div class="relative overflow-hidden rounded-lg shadow-[0_4px_10px_rgba(0,0,0,0.2)] transition-all duration-300 aspect-[4/3] hover:-translate-y-1 hover:shadow-[0_8px_20px_rgba(0,0,0,0.3)] group">
<img src={image} alt="Theater photo" loading="lazy" class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" />
<img src={urlFor(image)} alt="Theater photo" loading="lazy" class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" />
</div>
))}
</div>
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference types="@sanity/astro/module" />
+8 -6
View File
@@ -1,21 +1,23 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { sanityClient } from '../sanity/client';
import { renderPortableText } from '../sanity/portableText';
import { pageBySlugQuery } from '../sanity/queries';
const { Content, frontmatter } = await import('../../content/pages/about.mdx');
const page = await sanityClient.fetch(pageBySlugQuery, { slug: 'about' });
const bodyHtml = renderPortableText(page?.body);
---
<BaseLayout title={frontmatter.title} description={frontmatter.description}>
<BaseLayout title={page?.title} description={page?.description}>
<div class="page-header">
<div class="container">
<h1>{frontmatter.title}</h1>
<h1>{page?.title}</h1>
</div>
</div>
<div class="page-content">
<div class="container">
<div class="prose">
<Content />
</div>
<div class="prose" set:html={bodyHtml} />
</div>
</div>
</BaseLayout>
+17 -19
View File
@@ -1,30 +1,30 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { sanityClient } from '../../sanity/client';
import { renderPortableText } from '../../sanity/portableText';
import { urlFor } from '../../sanity/imageUrl';
import { allPostsQuery } from '../../sanity/queries';
export async function getStaticPaths() {
const allPosts = import.meta.glob<{ Content: any; frontmatter: Record<string, any> }>('../../../content/blog/*.mdx', { eager: true });
return Object.entries(allPosts).map(([path, post]) => {
const slug = path.split('/').pop()?.replace('.mdx', '') || '';
return {
params: { slug },
const posts = await sanityClient.fetch(allPostsQuery);
return posts.map((post: any) => ({
params: { slug: post.slug },
props: { post },
};
});
}));
}
const { post } = Astro.props as { post: { Content: any; frontmatter: Record<string, any> } };
const { Content, frontmatter } = post;
const { post } = Astro.props as { post: any };
const bodyHtml = renderPortableText(post.body);
---
<BaseLayout title={frontmatter.title} description={frontmatter.excerpt}>
<BaseLayout title={post.title} description={post.excerpt}>
<article class="blog-post">
<header class="post-header">
<div class="container">
<h1>{frontmatter.title}</h1>
<h1>{post.title}</h1>
<p class="post-meta">
<time datetime={frontmatter.date}>
{new Date(frontmatter.date).toLocaleDateString('de-DE', {
<time datetime={post.date}>
{new Date(post.date).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
@@ -34,17 +34,15 @@ const { Content, frontmatter } = post;
</div>
</header>
{frontmatter.coverImage && (
{post.coverImage && (
<div class="post-cover">
<img src={frontmatter.coverImage} alt={frontmatter.title} />
<img src={urlFor(post.coverImage)} alt={post.title} />
</div>
)}
<div class="post-content">
<div class="container">
<div class="prose">
<Content />
</div>
<div class="prose" set:html={bodyHtml} />
</div>
</div>
+12 -16
View File
@@ -1,14 +1,10 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { sanityClient } from '../../sanity/client';
import { urlFor } from '../../sanity/imageUrl';
import { allPostsQuery } from '../../sanity/queries';
const allPostFiles = import.meta.glob<{ frontmatter: Record<string, any> }>('../../../content/blog/*.mdx', { eager: true });
const allPosts = Object.entries(allPostFiles).map(([path, post]) => ({
...post,
slug: path.split('/').pop()?.replace('.mdx', '') || ''
}));
const sortedPosts = allPosts.sort(
(a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf()
);
const allPosts = await sanityClient.fetch(allPostsQuery);
---
<BaseLayout title="Blog" description="Latest news and updates from Theater Ziefen">
@@ -21,33 +17,33 @@ const sortedPosts = allPosts.sort(
<div class="blog-content">
<div class="container">
{sortedPosts.length === 0 ? (
{allPosts.length === 0 ? (
<p class="no-posts">No blog posts yet. Check back soon!</p>
) : (
<div class="posts-list">
{sortedPosts.map((post) => (
{allPosts.map((post: any) => (
<article class="post-item">
{post.frontmatter.coverImage && (
{post.coverImage && (
<div class="post-image">
<img src={post.frontmatter.coverImage} alt={post.frontmatter.title} />
<img src={urlFor(post.coverImage)} alt={post.title} />
</div>
)}
<div class="post-content">
<h2>
<a href={`/blog/${post.slug}`}>
{post.frontmatter.title}
{post.title}
</a>
</h2>
<p class="post-meta">
<time datetime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString('de-DE', {
<time datetime={post.date}>
{new Date(post.date).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</p>
<p class="post-excerpt">{post.frontmatter.excerpt}</p>
<p class="post-excerpt">{post.excerpt}</p>
<a href={`/blog/${post.slug}`} class="read-more">
Read full post →
</a>
+18 -45
View File
@@ -3,39 +3,12 @@ import BaseLayout from '../layouts/BaseLayout.astro';
import AnchorNav from '../components/AnchorNav.astro';
import TheaterSection from '../components/TheaterSection.astro';
import PhotoGallery from '../components/PhotoGallery.astro';
import { sanityClient } from '../sanity/client';
import { renderPortableText } from '../sanity/portableText';
import { urlFor } from '../sanity/imageUrl';
import { theaterHomepageQuery } from '../sanity/queries';
// Import theater homepage content
const theaterFiles = import.meta.glob('../../content/theater/*.mdx', { eager: true });
const theaterHomepage = Object.values(theaterFiles)[0] as any;
const content = theaterHomepage?.frontmatter || {};
// Helper function to render rich-text content
function renderRichText(richText: any, lang: 'de' | 'en'): string {
if (!richText || !richText.children) return '';
let html = '';
richText.children.forEach((child: any) => {
if (child.type === 'p') {
html += '<p>';
child.children.forEach((textNode: any) => {
if (textNode.type === 'text') {
html += textNode.text;
}
});
html += '</p>';
} else if (child.type === 'h3') {
html += '<h3>';
child.children.forEach((textNode: any) => {
if (textNode.type === 'text') {
html += textNode.text;
}
});
html += '</h3>';
}
});
return html;
}
const content = await sanityClient.fetch(theaterHomepageQuery) ?? {};
---
<BaseLayout title={content.title || 'Freilichttheater Ziefen 2028'}>
@@ -69,8 +42,8 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
titleEn={content.projekt?.title_en || 'The Project'}
variant="light"
>
<div class="content-de" data-lang="de" set:html={renderRichText(content.projekt?.content_de, 'de')} />
<div class="content-en" data-lang="en" set:html={renderRichText(content.projekt?.content_en, 'en')} />
<div class="content-de" data-lang="de" set:html={renderPortableText(content.projekt?.content_de)} />
<div class="content-en" data-lang="en" set:html={renderPortableText(content.projekt?.content_en)} />
</TheaterSection>
<!-- Section 2: Wer steckt dahinter -->
@@ -80,8 +53,8 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
titleEn={content.team?.title_en || "Who's Behind It"}
variant="dark"
>
<div class="content-de" data-lang="de" set:html={renderRichText(content.team?.content_de, 'de')} />
<div class="content-en" data-lang="en" set:html={renderRichText(content.team?.content_en, 'en')} />
<div class="content-de" data-lang="de" set:html={renderPortableText(content.team?.content_de)} />
<div class="content-en" data-lang="en" set:html={renderPortableText(content.team?.content_en)} />
{content.team?.organizations && content.team.organizations.length > 0 && (
<div class="grid grid-cols-1 md:grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-12 mt-16">
@@ -90,7 +63,7 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
<Fragment>
{org.logo && (
<div class="mb-4 flex justify-center">
<img src={org.logo} alt={`${org.name} logo`} class="h-20 w-auto object-contain" />
<img src={urlFor(org.logo)} alt={`${org.name} logo`} class="h-20 w-auto object-contain" />
</div>
)}
<h3 class="text-3xl mb-2 text-white">{org.name}</h3>
@@ -125,8 +98,8 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
titleEn={content.stueck?.title_en || 'The Play'}
variant="light"
>
<div class="content-de" data-lang="de" set:html={renderRichText(content.stueck?.content_de, 'de')} />
<div class="content-en" data-lang="en" set:html={renderRichText(content.stueck?.content_en, 'en')} />
<div class="content-de" data-lang="de" set:html={renderPortableText(content.stueck?.content_de)} />
<div class="content-en" data-lang="en" set:html={renderPortableText(content.stueck?.content_en)} />
</TheaterSection>
<!-- Section 4: Mitwirkung -->
@@ -136,8 +109,8 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
titleEn={content.mitwirkung?.title_en || 'Participation in the Play'}
variant="dark"
>
<div class="content-de" data-lang="de" set:html={renderRichText(content.mitwirkung?.content_de, 'de')} />
<div class="content-en" data-lang="en" set:html={renderRichText(content.mitwirkung?.content_en, 'en')} />
<div class="content-de" data-lang="de" set:html={renderPortableText(content.mitwirkung?.content_de)} />
<div class="content-en" data-lang="en" set:html={renderPortableText(content.mitwirkung?.content_en)} />
{content.mitwirkung?.director_name && (
<div class="mt-16 p-12 bg-white/10 rounded-lg text-center">
@@ -212,8 +185,8 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
titleEn={content.presse?.title_en || 'Press'}
variant="dark"
>
<div class="content-de" data-lang="de" set:html={renderRichText(content.presse?.content_de, 'de')} />
<div class="content-en" data-lang="en" set:html={renderRichText(content.presse?.content_en, 'en')} />
<div class="content-de" data-lang="de" set:html={renderPortableText(content.presse?.content_de)} />
<div class="content-en" data-lang="en" set:html={renderPortableText(content.presse?.content_en)} />
</TheaterSection>
<!-- Section 7: Foto Galerie -->
@@ -233,8 +206,8 @@ function renderRichText(richText: any, lang: 'de' | 'en'): string {
titleEn={content.tickets?.title_en || 'Tickets'}
variant="dark"
>
<div class="content-de" data-lang="de" set:html={renderRichText(content.tickets?.content_de, 'de')} />
<div class="content-en" data-lang="en" set:html={renderRichText(content.tickets?.content_en, 'en')} />
<div class="content-de" data-lang="de" set:html={renderPortableText(content.tickets?.content_de)} />
<div class="content-en" data-lang="en" set:html={renderPortableText(content.tickets?.content_en)} />
{content.tickets?.ticket_url && (
<div class="mt-[--spacing-lg] text-center">
+1
View File
@@ -0,0 +1 @@
export { sanityClient } from 'sanity:client'
+11
View File
@@ -0,0 +1,11 @@
import { createImageUrlBuilder } from '@sanity/image-url'
const builder = createImageUrlBuilder({
projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
dataset: import.meta.env.PUBLIC_SANITY_DATASET ?? 'production',
})
export function urlFor(source: any): string {
if (!source) return ''
return builder.image(source).url()
}
+6
View File
@@ -0,0 +1,6 @@
import { toHTML } from '@portabletext/to-html'
export function renderPortableText(blocks: any[]): string {
if (!blocks?.length) return ''
return toHTML(blocks)
}
+34
View File
@@ -0,0 +1,34 @@
export const theaterHomepageQuery = `*[_type == "theaterHomepage" && _id == "theaterHomepage"][0]`
export const allPostsQuery = `
*[_type == "post"] | order(date desc) {
_id,
title,
"slug": slug.current,
date,
excerpt,
coverImage,
body
}
`
export const postBySlugQuery = `
*[_type == "post" && slug.current == $slug][0] {
_id,
title,
"slug": slug.current,
date,
excerpt,
coverImage,
body
}
`
export const pageBySlugQuery = `
*[_type == "page" && slug.current == $slug][0] {
_id,
title,
description,
body
}
`
+5
View File
@@ -0,0 +1,5 @@
import { postSchema } from './post'
import { pageSchema } from './page'
import { theaterHomepageSchema } from './theaterHomepage'
export const schemaTypes = [postSchema, pageSchema, theaterHomepageSchema]
+33
View File
@@ -0,0 +1,33 @@
import { defineType, defineField } from 'sanity'
export const pageSchema = defineType({
name: 'page',
title: 'Pages',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (r) => r.required(),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: { source: 'title' },
}),
defineField({
name: 'description',
title: 'Description',
type: 'string',
}),
defineField({
name: 'body',
title: 'Content',
type: 'array',
of: [{ type: 'block' }],
validation: (r) => r.required(),
}),
],
})
+47
View File
@@ -0,0 +1,47 @@
import { defineType, defineField } from 'sanity'
export const postSchema = defineType({
name: 'post',
title: 'Blog Posts',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (r) => r.required(),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: { source: 'title' },
validation: (r) => r.required(),
}),
defineField({
name: 'date',
title: 'Publication Date',
type: 'datetime',
validation: (r) => r.required(),
}),
defineField({
name: 'excerpt',
title: 'Excerpt',
type: 'string',
validation: (r) => r.required(),
}),
defineField({
name: 'coverImage',
title: 'Cover Image',
type: 'image',
options: { hotspot: true },
}),
defineField({
name: 'body',
title: 'Body',
type: 'array',
of: [{ type: 'block' }, { type: 'image' }],
validation: (r) => r.required(),
}),
],
})
+173
View File
@@ -0,0 +1,173 @@
import { defineType, defineField, defineArrayMember } from 'sanity'
export const theaterHomepageSchema = defineType({
name: 'theaterHomepage',
title: 'Theater Homepage',
type: 'document',
// Singleton: disable create/delete — only update and publish
__experimental_actions: ['update', 'publish'],
fields: [
defineField({
name: 'title',
title: 'Page Title',
type: 'string',
}),
// Hero Section
defineField({
name: 'hero',
title: 'Hero Section',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'subtitle_de', title: 'Subtitle (German)', type: 'text' }),
defineField({ name: 'subtitle_en', title: 'Subtitle (English)', type: 'text' }),
],
}),
// Section 1: Das Projekt / The Project
defineField({
name: 'projekt',
title: 'Section 1: Das Projekt / The Project',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
],
}),
// Section 2: Wer steckt dahinter / Who's Behind It
defineField({
name: 'team',
title: "Section 2: Wer steckt dahinter / Who's Behind It",
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
defineField({
name: 'organizations',
title: 'Organizations',
type: 'array',
of: [
defineArrayMember({
type: 'object',
fields: [
defineField({ name: 'name', title: 'Name', type: 'string' }),
defineField({ name: 'role_de', title: 'Role (German)', type: 'string' }),
defineField({ name: 'role_en', title: 'Role (English)', type: 'string' }),
defineField({ name: 'logo', title: 'Logo', type: 'image', options: { hotspot: false } }),
defineField({ name: 'url', title: 'Website URL', type: 'url' }),
],
}),
],
}),
],
}),
// Section 3: Das Stück / The Play
defineField({
name: 'stueck',
title: 'Section 3: Das Stück / The Play',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
],
}),
// Section 4: Mitwirkung / Participation
defineField({
name: 'mitwirkung',
title: 'Section 4: Mitwirkung / Participation',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'director_name', title: 'Director Name', type: 'string' }),
defineField({ name: 'director_email', title: 'Director Email', type: 'string' }),
defineField({ name: 'director_phone', title: 'Director Phone', type: 'string' }),
],
}),
// Section 5: Termine / Schedule
defineField({
name: 'termine',
title: 'Section 5: Termine / Schedule',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({
name: 'events',
title: 'Events',
type: 'array',
of: [
defineArrayMember({
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Event Title (German)', type: 'string' }),
defineField({ name: 'title_en', title: 'Event Title (English)', type: 'string' }),
defineField({ name: 'date', title: 'Date / Period', type: 'string' }),
defineField({ name: 'description_de', title: 'Description (German)', type: 'text' }),
defineField({ name: 'description_en', title: 'Description (English)', type: 'text' }),
],
}),
],
}),
],
}),
// Section 6: Presse / Press
defineField({
name: 'presse',
title: 'Section 6: Presse / Press',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
],
}),
// Section 7: Foto Galerie / Photo Gallery
defineField({
name: 'gallery',
title: 'Section 7: Foto Galerie / Photo Gallery',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({
name: 'images',
title: 'Gallery Images',
type: 'array',
of: [{ type: 'image' }],
}),
],
}),
// Section 8: Tickets
defineField({
name: 'tickets',
title: 'Section 8: Tickets',
type: 'object',
fields: [
defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'ticket_url', title: 'Ticket Purchase URL', type: 'url' }),
],
}),
],
})
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
{"DocumentConnection":{"type":"DocumentConnection","resolveType":"multiCollectionDocumentList","collections":["user","post","page","theaterHomepage"]},"Node":{"type":"Node","resolveType":"nodeDocument"},"DocumentNode":{"type":"DocumentNode","resolveType":"multiCollectionDocument","createDocument":"create","updateDocument":"update"},"User":{"type":"User","resolveType":"collectionDocument","collection":"user","createUser":"create","updateUser":"update"},"UserConnection":{"type":"UserConnection","resolveType":"collectionDocumentList","collection":"user"},"Post":{"type":"Post","resolveType":"collectionDocument","collection":"post","createPost":"create","updatePost":"update"},"PostConnection":{"type":"PostConnection","resolveType":"collectionDocumentList","collection":"post"},"Page":{"type":"Page","resolveType":"collectionDocument","collection":"page","createPage":"create","updatePage":"update"},"PageConnection":{"type":"PageConnection","resolveType":"collectionDocumentList","collection":"page"},"TheaterHomepage":{"type":"TheaterHomepage","resolveType":"collectionDocument","collection":"theaterHomepage","createTheaterHomepage":"create","updateTheaterHomepage":"update"},"TheaterHomepageConnection":{"type":"TheaterHomepageConnection","resolveType":"collectionDocumentList","collection":"theaterHomepage"}}
-1
View File
File diff suppressed because one or more lines are too long
-5
View File
@@ -1,5 +0,0 @@
import { createClient } from "tinacms/dist/client";
import { queries } from "./types";
export const client = createClient({ url: '/api/tina/gql', token: 'undefined', queries, });
export default client;
-494
View File
@@ -1,494 +0,0 @@
// tina/config.ts
import { defineConfig } from "tinacms";
import { UsernamePasswordAuthJSProvider, TinaUserCollection } from "tinacms-authjs/dist/tinacms";
var config_default = defineConfig({
contentApiUrlOverride: "/api/tina/gql",
authProvider: new UsernamePasswordAuthJSProvider(),
build: {
outputFolder: "admin",
publicFolder: "public"
},
media: {
tina: {
mediaRoot: "images",
publicFolder: "public",
static: false
}
},
schema: {
collections: [
TinaUserCollection,
{
name: "post",
label: "Blog Posts",
path: "content/blog",
format: "mdx",
fields: [
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true
},
{
type: "datetime",
name: "date",
label: "Publication Date",
required: true
},
{
type: "string",
name: "excerpt",
label: "Excerpt",
required: true,
description: "Short summary of the blog post"
},
{
type: "image",
name: "coverImage",
label: "Cover Image",
description: "Featured image for the blog post"
},
{
type: "rich-text",
name: "body",
label: "Body",
isBody: true,
required: true
}
],
defaultItem: () => {
return {
title: "New Blog Post",
date: (/* @__PURE__ */ new Date()).toISOString(),
excerpt: ""
};
}
},
{
name: "page",
label: "Pages",
path: "content/pages",
format: "mdx",
fields: [
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true
},
{
type: "string",
name: "description",
label: "Description",
description: "Meta description for SEO"
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true,
required: true
}
]
},
{
name: "theaterHomepage",
label: "Theater Homepage",
path: "content/theater",
format: "mdx",
ui: {
allowedActions: {
create: false,
delete: false
}
},
fields: [
{
type: "string",
name: "title",
label: "Page Title",
isTitle: true,
required: true
},
// Hero Section
{
type: "object",
name: "hero",
label: "Hero Section",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "string",
name: "subtitle_de",
label: "Subtitle (German)",
required: true,
ui: {
component: "textarea"
}
},
{
type: "string",
name: "subtitle_en",
label: "Subtitle (English)",
required: true,
ui: {
component: "textarea"
}
}
]
},
// Section 1: Das Projekt
{
type: "object",
name: "projekt",
label: "Section 1: Das Projekt / The Project",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true
}
]
},
// Section 2: Wer steckt dahinter
{
type: "object",
name: "team",
label: "Section 2: Wer steckt dahinter / Who's Behind It",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true
},
{
type: "object",
name: "organizations",
label: "Organizations",
list: true,
fields: [
{
type: "string",
name: "name",
label: "Name"
},
{
type: "string",
name: "role_de",
label: "Role (German)"
},
{
type: "string",
name: "role_en",
label: "Role (English)"
},
{
type: "image",
name: "logo",
label: "Logo",
description: "Organization logo image (SVG, PNG, JPG supported)"
},
{
type: "string",
name: "url",
label: "Website URL",
description: "Organization website link"
}
]
}
]
},
// Section 3: Das Stück
{
type: "object",
name: "stueck",
label: "Section 3: Das St\xFCck / The Play",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true
}
]
},
// Section 4: Mitwirkung
{
type: "object",
name: "mitwirkung",
label: "Section 4: Mitwirkung / Participation",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true
},
{
type: "string",
name: "director_name",
label: "Director Name"
},
{
type: "string",
name: "director_email",
label: "Director Email"
},
{
type: "string",
name: "director_phone",
label: "Director Phone"
}
]
},
// Section 5: Termine
{
type: "object",
name: "termine",
label: "Section 5: Termine / Schedule",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "object",
name: "events",
label: "Events",
list: true,
fields: [
{
type: "string",
name: "title_de",
label: "Event Title (German)"
},
{
type: "string",
name: "title_en",
label: "Event Title (English)"
},
{
type: "string",
name: "date",
label: "Date/Period"
},
{
type: "string",
name: "description_de",
label: "Description (German)",
ui: {
component: "textarea"
}
},
{
type: "string",
name: "description_en",
label: "Description (English)",
ui: {
component: "textarea"
}
}
]
}
]
},
// Section 6: Presse
{
type: "object",
name: "presse",
label: "Section 6: Presse / Press",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true
}
]
},
// Section 7: Foto Galerie
{
type: "object",
name: "gallery",
label: "Section 7: Foto Galerie / Photo Gallery",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "image",
name: "images",
label: "Gallery Images",
list: true
}
]
},
// Section 8: Tickets
{
type: "object",
name: "tickets",
label: "Section 8: Tickets",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true
},
{
type: "string",
name: "ticket_url",
label: "Ticket Purchase URL",
description: "External link to ticket partner"
}
]
}
]
}
]
}
});
export {
config_default as default
};
-63
View File
@@ -1,63 +0,0 @@
// @ts-nocheck
import { resolve } from "@tinacms/datalayer";
import type { TinaClient } from "tinacms/dist/client";
import { queries } from "./types";
import database from "../database";
export async function databaseRequest({ query, variables, user }) {
const result = await resolve({
config: {
useRelativeMedia: true,
},
database,
query,
variables,
verbose: true,
ctxUser: user,
});
return result;
}
export async function authenticate({ username, password }) {
return databaseRequest({
query: `query auth($username:String!, $password:String!) {
authenticate(sub:$username, password:$password) {
id:username name email _password: password { passwordChangeRequired }
}
}`,
variables: { username, password },
})
}
export async function authorize(user: { sub: string }) {
return databaseRequest({
query: `query authz { authorize { id:username name email _password: password { passwordChangeRequired }} }`,
variables: {},
user
})
}
function createDatabaseClient<GenQueries = Record<string, unknown>>({
queries,
}: {
queries: (client: {
request: TinaClient<GenQueries>["request"];
}) => GenQueries;
}) {
const request = async ({ query, variables, user }) => {
const data = await databaseRequest({ query, variables, user });
return { data: data.data as any, query, variables, errors: data.errors || null };
};
const q = queries({
request,
});
return { queries: q, request, authenticate, authorize };
}
export const databaseClient = createDatabaseClient({ queries });
export const client = databaseClient;
export default databaseClient;
-114
View File
@@ -1,114 +0,0 @@
fragment UserParts on User {
__typename
users {
__typename
username
name
email
password {
value
passwordChangeRequired
}
}
}
fragment PostParts on Post {
__typename
title
date
excerpt
coverImage
body
}
fragment PageParts on Page {
__typename
title
description
body
}
fragment TheaterHomepageParts on TheaterHomepage {
__typename
title
hero {
__typename
title_de
title_en
subtitle_de
subtitle_en
}
projekt {
__typename
title_de
title_en
content_de
content_en
}
team {
__typename
title_de
title_en
content_de
content_en
organizations {
__typename
name
role_de
role_en
logo
url
}
}
stueck {
__typename
title_de
title_en
content_de
content_en
}
mitwirkung {
__typename
title_de
title_en
content_de
content_en
director_name
director_email
director_phone
}
termine {
__typename
title_de
title_en
events {
__typename
title_de
title_en
date
description_de
description_en
}
}
presse {
__typename
title_de
title_en
content_de
content_en
}
gallery {
__typename
title_de
title_en
images
}
tickets {
__typename
title_de
title_en
content_de
content_en
ticket_url
}
}
-219
View File
@@ -1,219 +0,0 @@
query user($relativePath: String!) {
user(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...UserParts
}
}
query userConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: UserFilter) {
userConnection(
before: $before
after: $after
first: $first
last: $last
sort: $sort
filter: $filter
) {
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
totalCount
edges {
cursor
node {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...UserParts
}
}
}
}
query post($relativePath: String!) {
post(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...PostParts
}
}
query postConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: PostFilter) {
postConnection(
before: $before
after: $after
first: $first
last: $last
sort: $sort
filter: $filter
) {
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
totalCount
edges {
cursor
node {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...PostParts
}
}
}
}
query page($relativePath: String!) {
page(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...PageParts
}
}
query pageConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: PageFilter) {
pageConnection(
before: $before
after: $after
first: $first
last: $last
sort: $sort
filter: $filter
) {
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
totalCount
edges {
cursor
node {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...PageParts
}
}
}
}
query theaterHomepage($relativePath: String!) {
theaterHomepage(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...TheaterHomepageParts
}
}
query theaterHomepageConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: TheaterHomepageFilter) {
theaterHomepageConnection(
before: $before
after: $after
first: $first
last: $last
sort: $sort
filter: $filter
) {
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
totalCount
edges {
cursor
node {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...TheaterHomepageParts
}
}
}
}
-589
View File
@@ -1,589 +0,0 @@
# DO NOT MODIFY THIS FILE. This file is automatically generated by Tina
"""References another document, used as a foreign key"""
scalar Reference
""""""
scalar JSON
type SystemInfo {
filename: String!
title: String
basename: String!
hasReferences: Boolean
breadcrumbs(excludeExtension: Boolean): [String!]!
path: String!
relativePath: String!
extension: String!
template: String!
collection: Collection!
}
type Folder {
name: String!
path: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String!
endCursor: String!
}
""""""
interface Node {
id: ID!
}
""""""
interface Document {
id: ID!
_sys: SystemInfo
_values: JSON!
}
"""A relay-compliant pagination connection"""
interface Connection {
totalCount: Float!
pageInfo: PageInfo!
}
type Query {
getOptimizedQuery(queryString: String!): String
collection(collection: String): Collection!
collections: [Collection!]!
node(id: String): Node!
document(collection: String, relativePath: String): DocumentNode!
user(relativePath: String): User!
authenticate(sub: String!, password: String!): UserUsers
authorize: UserUsers
userConnection(before: String, after: String, first: Float, last: Float, sort: String, filter: UserFilter): UserConnection!
post(relativePath: String): Post!
postConnection(before: String, after: String, first: Float, last: Float, sort: String, filter: PostFilter): PostConnection!
page(relativePath: String): Page!
pageConnection(before: String, after: String, first: Float, last: Float, sort: String, filter: PageFilter): PageConnection!
theaterHomepage(relativePath: String): TheaterHomepage!
theaterHomepageConnection(before: String, after: String, first: Float, last: Float, sort: String, filter: TheaterHomepageFilter): TheaterHomepageConnection!
}
input DocumentFilter {
user: UserFilter
post: PostFilter
page: PageFilter
theaterHomepage: TheaterHomepageFilter
}
type DocumentConnectionEdges {
cursor: String!
node: DocumentNode
}
type DocumentConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [DocumentConnectionEdges]
}
type Collection {
name: String!
slug: String!
label: String
path: String!
format: String
matches: String
templates: [JSON]
fields: [JSON]
documents(before: String, after: String, first: Float, last: Float, sort: String, filter: DocumentFilter, folder: String): DocumentConnection!
}
union DocumentNode = User | Post | Page | TheaterHomepage | Folder
type UserUsersPassword {
value: String!
passwordChangeRequired: Boolean
}
type UserUsers {
username: String!
name: String
email: String
password: UserUsersPassword!
}
type User implements Node & Document {
users: [UserUsers]
id: ID!
_sys: SystemInfo!
_values: JSON!
}
input StringFilter {
startsWith: String
eq: String
exists: Boolean
in: [String]
}
input UserUsersFilter {
username: StringFilter
name: StringFilter
email: StringFilter
}
input UserFilter {
users: UserUsersFilter
}
type UserConnectionEdges {
cursor: String!
node: User
}
type UserConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [UserConnectionEdges]
}
type Post implements Node & Document {
title: String!
date: String!
excerpt: String!
coverImage: String
body: JSON!
id: ID!
_sys: SystemInfo!
_values: JSON!
}
input DatetimeFilter {
after: String
before: String
eq: String
exists: Boolean
in: [String]
}
input ImageFilter {
startsWith: String
eq: String
exists: Boolean
in: [String]
}
input RichTextFilter {
startsWith: String
eq: String
exists: Boolean
}
input PostFilter {
title: StringFilter
date: DatetimeFilter
excerpt: StringFilter
coverImage: ImageFilter
body: RichTextFilter
}
type PostConnectionEdges {
cursor: String!
node: Post
}
type PostConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [PostConnectionEdges]
}
type Page implements Node & Document {
title: String!
description: String
body: JSON!
id: ID!
_sys: SystemInfo!
_values: JSON!
}
input PageFilter {
title: StringFilter
description: StringFilter
body: RichTextFilter
}
type PageConnectionEdges {
cursor: String!
node: Page
}
type PageConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [PageConnectionEdges]
}
type TheaterHomepageHero {
title_de: String!
title_en: String!
subtitle_de: String!
subtitle_en: String!
}
type TheaterHomepageProjekt {
title_de: String!
title_en: String!
content_de: JSON!
content_en: JSON!
}
type TheaterHomepageTeamOrganizations {
name: String
role_de: String
role_en: String
logo: String
url: String
}
type TheaterHomepageTeam {
title_de: String!
title_en: String!
content_de: JSON!
content_en: JSON!
organizations: [TheaterHomepageTeamOrganizations]
}
type TheaterHomepageStueck {
title_de: String!
title_en: String!
content_de: JSON!
content_en: JSON!
}
type TheaterHomepageMitwirkung {
title_de: String!
title_en: String!
content_de: JSON!
content_en: JSON!
director_name: String
director_email: String
director_phone: String
}
type TheaterHomepageTermineEvents {
title_de: String
title_en: String
date: String
description_de: String
description_en: String
}
type TheaterHomepageTermine {
title_de: String!
title_en: String!
events: [TheaterHomepageTermineEvents]
}
type TheaterHomepagePresse {
title_de: String!
title_en: String!
content_de: JSON!
content_en: JSON!
}
type TheaterHomepageGallery {
title_de: String!
title_en: String!
images: [String]
}
type TheaterHomepageTickets {
title_de: String!
title_en: String!
content_de: JSON!
content_en: JSON!
ticket_url: String
}
type TheaterHomepage implements Node & Document {
title: String!
hero: TheaterHomepageHero
projekt: TheaterHomepageProjekt
team: TheaterHomepageTeam
stueck: TheaterHomepageStueck
mitwirkung: TheaterHomepageMitwirkung
termine: TheaterHomepageTermine
presse: TheaterHomepagePresse
gallery: TheaterHomepageGallery
tickets: TheaterHomepageTickets
id: ID!
_sys: SystemInfo!
_values: JSON!
}
input TheaterHomepageHeroFilter {
title_de: StringFilter
title_en: StringFilter
subtitle_de: StringFilter
subtitle_en: StringFilter
}
input TheaterHomepageProjektFilter {
title_de: StringFilter
title_en: StringFilter
content_de: RichTextFilter
content_en: RichTextFilter
}
input TheaterHomepageTeamOrganizationsFilter {
name: StringFilter
role_de: StringFilter
role_en: StringFilter
logo: ImageFilter
url: StringFilter
}
input TheaterHomepageTeamFilter {
title_de: StringFilter
title_en: StringFilter
content_de: RichTextFilter
content_en: RichTextFilter
organizations: TheaterHomepageTeamOrganizationsFilter
}
input TheaterHomepageStueckFilter {
title_de: StringFilter
title_en: StringFilter
content_de: RichTextFilter
content_en: RichTextFilter
}
input TheaterHomepageMitwirkungFilter {
title_de: StringFilter
title_en: StringFilter
content_de: RichTextFilter
content_en: RichTextFilter
director_name: StringFilter
director_email: StringFilter
director_phone: StringFilter
}
input TheaterHomepageTermineEventsFilter {
title_de: StringFilter
title_en: StringFilter
date: StringFilter
description_de: StringFilter
description_en: StringFilter
}
input TheaterHomepageTermineFilter {
title_de: StringFilter
title_en: StringFilter
events: TheaterHomepageTermineEventsFilter
}
input TheaterHomepagePresseFilter {
title_de: StringFilter
title_en: StringFilter
content_de: RichTextFilter
content_en: RichTextFilter
}
input TheaterHomepageGalleryFilter {
title_de: StringFilter
title_en: StringFilter
images: ImageFilter
}
input TheaterHomepageTicketsFilter {
title_de: StringFilter
title_en: StringFilter
content_de: RichTextFilter
content_en: RichTextFilter
ticket_url: StringFilter
}
input TheaterHomepageFilter {
title: StringFilter
hero: TheaterHomepageHeroFilter
projekt: TheaterHomepageProjektFilter
team: TheaterHomepageTeamFilter
stueck: TheaterHomepageStueckFilter
mitwirkung: TheaterHomepageMitwirkungFilter
termine: TheaterHomepageTermineFilter
presse: TheaterHomepagePresseFilter
gallery: TheaterHomepageGalleryFilter
tickets: TheaterHomepageTicketsFilter
}
type TheaterHomepageConnectionEdges {
cursor: String!
node: TheaterHomepage
}
type TheaterHomepageConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [TheaterHomepageConnectionEdges]
}
type Mutation {
addPendingDocument(collection: String!, relativePath: String!, template: String): DocumentNode!
updateDocument(collection: String, relativePath: String!, params: DocumentUpdateMutation!): DocumentNode!
deleteDocument(collection: String, relativePath: String!): DocumentNode!
createDocument(collection: String, relativePath: String!, params: DocumentMutation!): DocumentNode!
createFolder(collection: String, relativePath: String!): DocumentNode!
updatePassword(password: String!): Boolean!
updateUser(relativePath: String!, params: UserMutation!): User!
createUser(relativePath: String!, params: UserMutation!): User!
updatePost(relativePath: String!, params: PostMutation!): Post!
createPost(relativePath: String!, params: PostMutation!): Post!
updatePage(relativePath: String!, params: PageMutation!): Page!
createPage(relativePath: String!, params: PageMutation!): Page!
updateTheaterHomepage(relativePath: String!, params: TheaterHomepageMutation!): TheaterHomepage!
createTheaterHomepage(relativePath: String!, params: TheaterHomepageMutation!): TheaterHomepage!
}
input DocumentUpdateMutation {
user: UserMutation
post: PostMutation
page: PageMutation
theaterHomepage: TheaterHomepageMutation
relativePath: String
}
input DocumentMutation {
user: UserMutation
post: PostMutation
page: PageMutation
theaterHomepage: TheaterHomepageMutation
}
input UserUsersPasswordMutation {
value: String
passwordChangeRequired: Boolean!
}
input UserUsersMutation {
username: String
name: String
email: String
password: UserUsersPasswordMutation
}
input UserMutation {
users: [UserUsersMutation]
}
input PostMutation {
title: String
date: String
excerpt: String
coverImage: String
body: JSON
}
input PageMutation {
title: String
description: String
body: JSON
}
input TheaterHomepageHeroMutation {
title_de: String
title_en: String
subtitle_de: String
subtitle_en: String
}
input TheaterHomepageProjektMutation {
title_de: String
title_en: String
content_de: JSON
content_en: JSON
}
input TheaterHomepageTeamOrganizationsMutation {
name: String
role_de: String
role_en: String
logo: String
url: String
}
input TheaterHomepageTeamMutation {
title_de: String
title_en: String
content_de: JSON
content_en: JSON
organizations: [TheaterHomepageTeamOrganizationsMutation]
}
input TheaterHomepageStueckMutation {
title_de: String
title_en: String
content_de: JSON
content_en: JSON
}
input TheaterHomepageMitwirkungMutation {
title_de: String
title_en: String
content_de: JSON
content_en: JSON
director_name: String
director_email: String
director_phone: String
}
input TheaterHomepageTermineEventsMutation {
title_de: String
title_en: String
date: String
description_de: String
description_en: String
}
input TheaterHomepageTermineMutation {
title_de: String
title_en: String
events: [TheaterHomepageTermineEventsMutation]
}
input TheaterHomepagePresseMutation {
title_de: String
title_en: String
content_de: JSON
content_en: JSON
}
input TheaterHomepageGalleryMutation {
title_de: String
title_en: String
images: [String]
}
input TheaterHomepageTicketsMutation {
title_de: String
title_en: String
content_de: JSON
content_en: JSON
ticket_url: String
}
input TheaterHomepageMutation {
title: String
hero: TheaterHomepageHeroMutation
projekt: TheaterHomepageProjektMutation
team: TheaterHomepageTeamMutation
stueck: TheaterHomepageStueckMutation
mitwirkung: TheaterHomepageMitwirkungMutation
termine: TheaterHomepageTermineMutation
presse: TheaterHomepagePresseMutation
gallery: TheaterHomepageGalleryMutation
tickets: TheaterHomepageTicketsMutation
}
schema {
query: Query
mutation: Mutation
}
-1
View File
@@ -1 +0,0 @@
[]
-1345
View File
File diff suppressed because it is too large Load Diff
-492
View File
@@ -1,492 +0,0 @@
import { defineConfig } from "tinacms";
import { UsernamePasswordAuthJSProvider, TinaUserCollection } from "tinacms-authjs/dist/tinacms";
export default defineConfig({
contentApiUrlOverride: "/api/tina/gql",
authProvider: new UsernamePasswordAuthJSProvider(),
build: {
outputFolder: "admin",
publicFolder: "public",
},
media: {
tina: {
mediaRoot: "images",
publicFolder: "public",
static: false,
},
},
schema: {
collections: [
TinaUserCollection,
{
name: "post",
label: "Blog Posts",
path: "content/blog",
format: "mdx",
fields: [
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true,
},
{
type: "datetime",
name: "date",
label: "Publication Date",
required: true,
},
{
type: "string",
name: "excerpt",
label: "Excerpt",
required: true,
description: "Short summary of the blog post",
},
{
type: "image",
name: "coverImage",
label: "Cover Image",
description: "Featured image for the blog post",
},
{
type: "rich-text",
name: "body",
label: "Body",
isBody: true,
required: true,
},
],
defaultItem: () => {
return {
title: "New Blog Post",
date: new Date().toISOString(),
excerpt: "",
};
},
},
{
name: "page",
label: "Pages",
path: "content/pages",
format: "mdx",
fields: [
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true,
},
{
type: "string",
name: "description",
label: "Description",
description: "Meta description for SEO",
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true,
required: true,
},
],
},
{
name: "theaterHomepage",
label: "Theater Homepage",
path: "content/theater",
format: "mdx",
ui: {
allowedActions: {
create: false,
delete: false,
},
},
fields: [
{
type: "string",
name: "title",
label: "Page Title",
isTitle: true,
required: true,
},
// Hero Section
{
type: "object",
name: "hero",
label: "Hero Section",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "string",
name: "subtitle_de",
label: "Subtitle (German)",
required: true,
ui: {
component: "textarea",
},
},
{
type: "string",
name: "subtitle_en",
label: "Subtitle (English)",
required: true,
ui: {
component: "textarea",
},
},
],
},
// Section 1: Das Projekt
{
type: "object",
name: "projekt",
label: "Section 1: Das Projekt / The Project",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true,
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true,
},
],
},
// Section 2: Wer steckt dahinter
{
type: "object",
name: "team",
label: "Section 2: Wer steckt dahinter / Who's Behind It",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true,
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true,
},
{
type: "object",
name: "organizations",
label: "Organizations",
list: true,
fields: [
{
type: "string",
name: "name",
label: "Name",
},
{
type: "string",
name: "role_de",
label: "Role (German)",
},
{
type: "string",
name: "role_en",
label: "Role (English)",
},
{
type: "image",
name: "logo",
label: "Logo",
description: "Organization logo image (SVG, PNG, JPG supported)",
},
{
type: "string",
name: "url",
label: "Website URL",
description: "Organization website link",
},
],
},
],
},
// Section 3: Das Stück
{
type: "object",
name: "stueck",
label: "Section 3: Das Stück / The Play",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true,
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true,
},
],
},
// Section 4: Mitwirkung
{
type: "object",
name: "mitwirkung",
label: "Section 4: Mitwirkung / Participation",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true,
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true,
},
{
type: "string",
name: "director_name",
label: "Director Name",
},
{
type: "string",
name: "director_email",
label: "Director Email",
},
{
type: "string",
name: "director_phone",
label: "Director Phone",
},
],
},
// Section 5: Termine
{
type: "object",
name: "termine",
label: "Section 5: Termine / Schedule",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "object",
name: "events",
label: "Events",
list: true,
fields: [
{
type: "string",
name: "title_de",
label: "Event Title (German)",
},
{
type: "string",
name: "title_en",
label: "Event Title (English)",
},
{
type: "string",
name: "date",
label: "Date/Period",
},
{
type: "string",
name: "description_de",
label: "Description (German)",
ui: {
component: "textarea",
},
},
{
type: "string",
name: "description_en",
label: "Description (English)",
ui: {
component: "textarea",
},
},
],
},
],
},
// Section 6: Presse
{
type: "object",
name: "presse",
label: "Section 6: Presse / Press",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true,
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true,
},
],
},
// Section 7: Foto Galerie
{
type: "object",
name: "gallery",
label: "Section 7: Foto Galerie / Photo Gallery",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "image",
name: "images",
label: "Gallery Images",
list: true,
},
],
},
// Section 8: Tickets
{
type: "object",
name: "tickets",
label: "Section 8: Tickets",
fields: [
{
type: "string",
name: "title_de",
label: "Title (German)",
required: true,
},
{
type: "string",
name: "title_en",
label: "Title (English)",
required: true,
},
{
type: "rich-text",
name: "content_de",
label: "Content (German)",
required: true,
},
{
type: "rich-text",
name: "content_en",
label: "Content (English)",
required: true,
},
{
type: "string",
name: "ticket_url",
label: "Ticket Purchase URL",
description: "External link to ticket partner",
},
],
},
],
},
],
},
});
-26
View File
@@ -1,26 +0,0 @@
import { createDatabase, createLocalDatabase } from '@tinacms/datalayer'
import { MongodbLevel } from 'mongodb-level'
import { GitHubProvider } from 'tinacms-gitprovider-github'
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === 'true'
const branch = process.env.GITEA_BRANCH || 'main'
export default isLocal
? createLocalDatabase()
: createDatabase({
gitProvider: new GitHubProvider({
branch,
owner: process.env.GITEA_OWNER!,
repo: process.env.GITEA_REPO!,
token: process.env.GITEA_TOKEN!,
octokitOptions: {
// Gitea's GitHub-compatible API
baseUrl: 'https://git.idealconnected.com/api/v1',
},
}),
databaseAdapter: new MongodbLevel({
collectionName: 'tinacms',
dbName: 'tinacms',
mongoUri: process.env.MONGODB_URI!,
}),
})
File diff suppressed because one or more lines are too long