This commit is contained in:
vorpax
2026-02-02 04:14:53 +01:00
parent 187821c88e
commit 6be40f5a6a
44 changed files with 12015 additions and 1688 deletions

4
tina/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__generated__
node_modules
.env
*.log

View File

@@ -0,0 +1,27 @@
import { AbstractAuthProvider } from "tinacms";
export class OIDCAuthProvider extends AbstractAuthProvider {
async authenticate(): Promise<any> {
window.location.href = "/auth/login";
}
async getUser(): Promise<any> {
// Appel à une route /api/me pour récupérer l'utilisateur
const res = await fetch("/api/me", {
credentials: "include",
});
if (res.ok) {
return await res.json();
}
return null;
}
async getToken(): Promise<{ id_token: string }> {
// Le token est géré par les cookies, pas besoin de le retourner
return { id_token: "" };
}
async logout(): Promise<void> {
window.location.href = "/auth/logout";
}
}

617
tina/config.ts Normal file
View File

@@ -0,0 +1,617 @@
import { defineConfig, LocalAuthProvider } from "tinacms";
import { OIDCAuthProvider } from "./auth/oidc-provider";
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true";
export default defineConfig({
branch: "main",
// Self-hosted: pas de clientId/token
contentApiUrlOverride: isLocal
? undefined
: "https://tina.hec-ia.com/api/tina/gql",
authProvider: isLocal
? new LocalAuthProvider()
: new OIDCAuthProvider(),
build: {
outputFolder: "admin",
publicFolder: "public",
},
media: {
tina: {
mediaRoot: "uploads",
publicFolder: "public",
},
},
search: {
tina: {
indexerToken: "", // Mode local
stopwordLanguages: ["fra"],
},
},
schema: {
collections: [
{
name: "blog",
label: "Articles de blog",
path: "src/data/blog",
format: "md",
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.title
?.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
},
},
},
fields: [
{
type: "string" as const,
name: "title",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string" as const,
name: "description",
label: "Description",
required: true,
ui: {
component: "textarea",
},
},
{
type: "string" as const,
name: "author",
label: "Auteur",
required: false,
},
{
type: "datetime" as const,
name: "pubDatetime",
label: "Date de publication",
required: true,
},
{
type: "datetime" as const,
name: "modDatetime",
label: "Date de modification",
required: false,
},
{
type: "string" as const,
name: "tags",
label: "Tags",
list: true,
required: false,
},
{
type: "boolean" as const,
name: "draft",
label: "Brouillon",
required: false,
},
{
type: "boolean" as const,
name: "featured",
label: "Mis en avant",
required: false,
},
{
type: "string" as const,
name: "canonicalURL",
label: "URL canonique",
required: false,
},
{
type: "boolean" as const,
name: "hideEditPost",
label: "Cacher le lien d'édition",
required: false,
},
{
type: "string" as const,
name: "timezone",
label: "Fuseau horaire",
required: false,
},
{
type: "image" as const,
name: "ogImage",
label: "Image Open Graph",
required: false,
},
{
type: "rich-text" as const,
name: "body",
label: "Contenu",
isBody: true,
},
],
},
{
name: "events",
label: "Événements",
path: "src/data/events",
format: "md",
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.title
?.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
},
},
},
fields: [
{
type: "string" as const,
name: "title",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string" as const,
name: "description",
label: "Description",
required: true,
ui: {
component: "textarea",
},
},
{
type: "string" as const,
name: "author",
label: "Auteur",
required: false,
},
{
type: "datetime" as const,
name: "pubDatetime",
label: "Date de publication",
required: true,
},
{
type: "datetime" as const,
name: "modDatetime",
label: "Date de modification",
required: false,
},
{
type: "string" as const,
name: "tags",
label: "Tags",
list: true,
required: false,
},
{
type: "boolean" as const,
name: "draft",
label: "Brouillon",
required: false,
},
{
type: "boolean" as const,
name: "featured",
label: "Mis en avant",
required: false,
},
{
type: "string" as const,
name: "canonicalURL",
label: "URL canonique",
required: false,
},
{
type: "boolean" as const,
name: "hideEditPost",
label: "Cacher le lien d'édition",
required: false,
},
{
type: "string" as const,
name: "timezone",
label: "Fuseau horaire",
required: false,
},
{
type: "datetime" as const,
name: "eventDate",
label: "Date de l'événement",
required: true,
},
{
type: "datetime" as const,
name: "eventEndDate",
label: "Date de fin",
required: false,
},
{
type: "string" as const,
name: "location",
label: "Lieu",
required: false,
},
{
type: "string" as const,
name: "registrationLink",
label: "Lien d'inscription",
required: false,
},
{
type: "image" as const,
name: "ogImage",
label: "Image Open Graph",
required: false,
},
{
type: "rich-text" as const,
name: "body",
label: "Contenu",
isBody: true,
},
],
},
{
name: "workshops",
label: "Ateliers",
path: "src/data/workshops",
format: "md",
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.title
?.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
},
},
},
fields: [
{
type: "string" as const,
name: "title",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string" as const,
name: "description",
label: "Description",
required: true,
ui: {
component: "textarea",
},
},
{
type: "string" as const,
name: "author",
label: "Auteur",
required: false,
},
{
type: "datetime" as const,
name: "pubDatetime",
label: "Date de publication",
required: true,
},
{
type: "datetime" as const,
name: "modDatetime",
label: "Date de modification",
required: false,
},
{
type: "string" as const,
name: "tags",
label: "Tags",
list: true,
required: false,
},
{
type: "boolean" as const,
name: "draft",
label: "Brouillon",
required: false,
},
{
type: "boolean" as const,
name: "featured",
label: "Mis en avant",
required: false,
},
{
type: "string" as const,
name: "canonicalURL",
label: "URL canonique",
required: false,
},
{
type: "boolean" as const,
name: "hideEditPost",
label: "Cacher le lien d'édition",
required: false,
},
{
type: "string" as const,
name: "timezone",
label: "Fuseau horaire",
required: false,
},
{
type: "datetime" as const,
name: "workshopDate",
label: "Date de l'atelier",
required: true,
},
{
type: "string" as const,
name: "duration",
label: "Durée",
required: false,
},
{
type: "string" as const,
name: "level",
label: "Niveau",
options: [
{ label: "Débutant", value: "beginner" },
{ label: "Intermédiaire", value: "intermediate" },
{ label: "Avancé", value: "advanced" },
],
required: false,
},
{
type: "string" as const,
name: "materials",
label: "Matériaux/Supports",
required: false,
},
{
type: "image" as const,
name: "ogImage",
label: "Image Open Graph",
required: false,
},
{
type: "rich-text" as const,
name: "body",
label: "Contenu",
isBody: true,
},
],
},
{
name: "news",
label: "Actualités",
path: "src/data/news",
format: "md",
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.title
?.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
},
},
},
fields: [
{
type: "string" as const,
name: "title",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string" as const,
name: "description",
label: "Description",
required: true,
ui: {
component: "textarea",
},
},
{
type: "string" as const,
name: "author",
label: "Auteur",
required: false,
},
{
type: "datetime" as const,
name: "pubDatetime",
label: "Date de publication",
required: true,
},
{
type: "datetime" as const,
name: "modDatetime",
label: "Date de modification",
required: false,
},
{
type: "string" as const,
name: "tags",
label: "Tags",
list: true,
required: false,
},
{
type: "boolean" as const,
name: "draft",
label: "Brouillon",
required: false,
},
{
type: "boolean" as const,
name: "featured",
label: "Mis en avant",
required: false,
},
{
type: "string" as const,
name: "canonicalURL",
label: "URL canonique",
required: false,
},
{
type: "boolean" as const,
name: "hideEditPost",
label: "Cacher le lien d'édition",
required: false,
},
{
type: "string" as const,
name: "timezone",
label: "Fuseau horaire",
required: false,
},
{
type: "image" as const,
name: "ogImage",
label: "Image Open Graph",
required: false,
},
{
type: "rich-text" as const,
name: "body",
label: "Contenu",
isBody: true,
},
],
},
{
name: "technical",
label: "Articles techniques",
path: "src/data/technical",
format: "md",
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.title
?.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
},
},
},
fields: [
{
type: "string" as const,
name: "title",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string" as const,
name: "description",
label: "Description",
required: true,
ui: {
component: "textarea",
},
},
{
type: "string" as const,
name: "author",
label: "Auteur",
required: false,
},
{
type: "datetime" as const,
name: "pubDatetime",
label: "Date de publication",
required: true,
},
{
type: "datetime" as const,
name: "modDatetime",
label: "Date de modification",
required: false,
},
{
type: "string" as const,
name: "tags",
label: "Tags",
list: true,
required: false,
},
{
type: "boolean" as const,
name: "draft",
label: "Brouillon",
required: false,
},
{
type: "boolean" as const,
name: "featured",
label: "Mis en avant",
required: false,
},
{
type: "string" as const,
name: "canonicalURL",
label: "URL canonique",
required: false,
},
{
type: "boolean" as const,
name: "hideEditPost",
label: "Cacher le lien d'édition",
required: false,
},
{
type: "string" as const,
name: "timezone",
label: "Fuseau horaire",
required: false,
},
{
type: "string" as const,
name: "difficulty",
label: "Difficulté",
options: [
{ label: "Débutant", value: "beginner" },
{ label: "Intermédiaire", value: "intermediate" },
{ label: "Avancé", value: "advanced" },
],
required: false,
},
{
type: "string" as const,
name: "readingTime",
label: "Temps de lecture",
required: false,
},
{
type: "image" as const,
name: "ogImage",
label: "Image Open Graph",
required: false,
},
{
type: "rich-text" as const,
name: "body",
label: "Contenu",
isBody: true,
},
],
},
],
},
});

1
tina/tina-lock.json Normal file

File diff suppressed because one or more lines are too long