Compare commits

..

No commits in common. "dcb9be9b1919a30ff18f6d0458834e1776e68a87" and "ec91c17919ff27cf81d2460c0839e871931720f2" have entirely different histories.

28 changed files with 11883 additions and 339 deletions

4
.env
View File

@ -23,10 +23,6 @@ DATABASE_URL="file:./dev.db"
N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary"
N8N_API_KEY="sk_live_JwjpTdZrLVvijCKuWylWZbUuhBm6zPDH"
# Cloudflare Email Sending
CLOUDFLARE_API_TOKEN=cfut_8Y0Tcwiv3v0K1LDK9QSvc3BjlU1lZFs1zikACgTw33977b37
CLOUDFLARE_ACCOUNT_ID=3e689750123db91e81d3542d2930a907
# Cloudflare Turnstile — public key (visible in frontend)
TURNSTILE_SITE_KEY=0x4AAAAAAD9IYWC6HPflpneO

View File

@ -55,7 +55,5 @@ jobs:
pnpm install --prod 2>&1
sed -i "s|TURNSTILE_SITE_KEY: \"\"|TURNSTILE_SITE_KEY: \"${{ secrets.TURNSTILE_SITE_KEY }}\"|g" ecosystem.config.cjs
sed -i "s|TURNSTILE_SECRET_KEY: \"\"|TURNSTILE_SECRET_KEY: \"${{ secrets.TURNSTILE_SECRET_KEY }}\"|g" ecosystem.config.cjs
sed -i "s|CLOUDFLARE_API_TOKEN: \"\"|CLOUDFLARE_API_TOKEN: \"${{ secrets.CLOUDFLARE_API_TOKEN }}\"|g" ecosystem.config.cjs
sed -i "s|CLOUDFLARE_ACCOUNT_ID: \"\"|CLOUDFLARE_ACCOUNT_ID: \"${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\"|g" ecosystem.config.cjs
pm2 reload ecosystem.config.cjs --only ${{ env.APP_NAME }} --update-env || \
pm2 start ecosystem.config.cjs --only ${{ env.APP_NAME }} --update-env

1
.gitignore vendored
View File

@ -29,4 +29,3 @@ prisma/dev.db
# opencode - agentes y scripts locales (no subir a producción)
opencode/
.opencode/
.env

View File

@ -8,8 +8,6 @@ module.exports = {
PORT: 3310,
TURNSTILE_SITE_KEY: "",
TURNSTILE_SECRET_KEY: "",
CLOUDFLARE_API_TOKEN: "",
CLOUDFLARE_ACCOUNT_ID: "",
}
},
{
@ -20,8 +18,6 @@ module.exports = {
PORT: 4321,
TURNSTILE_SITE_KEY: "",
TURNSTILE_SECRET_KEY: "",
CLOUDFLARE_API_TOKEN: "",
CLOUDFLARE_ACCOUNT_ID: "",
}
}
]

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"dev": "tinacms dev -c \"astro dev\"",
"build": "astro build",
"postbuild": "node scripts/send-to-n8n.js",
"preview": "astro preview",
@ -24,12 +24,12 @@
"@iconify/vue": "^5.0.0",
"@prisma/client": "^6.19.2",
"@tailwindcss/vite": "^4.1.18",
"@tinacms/cli": "^2.2.2",
"@unpic/astro": "^1.0.2",
"astro": "^7.0.6",
"astro-embed": "^0.12.0",
"astro-google-analytics": "^1.0.3",
"astro-icon": "^1.1.5",
"cloudflare": "^7.0.0",
"dayjs": "^1.11.19",
"googleapis": "^171.4.0",
"prisma": "^6.19.2",
@ -38,6 +38,7 @@
"sharp": "^0.34.5",
"swiper": "^12.1.0",
"tailwindcss": "^4.1.18",
"tinacms": "^2.2.2",
"vue": "^3.5.28"
},
"devDependencies": {

File diff suppressed because it is too large Load Diff

2
public/admin/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
index.html
assets/

View File

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TinaCMS</title>
</head>
<!-- if development -->
<script type="module">
import RefreshRuntime from 'http://localhost:4001/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="http://localhost:4001/@vite/client"></script>
<script>
function handleLoadError() {
// Assets have failed to load
document.getElementById('root').innerHTML = '<style type="text/css"> #no-assets-placeholder body { font-family: sans-serif; font-size: 16px; line-height: 1.4; color: #333; background-color: #f5f5f5; } #no-assets-placeholder { max-width: 600px; margin: 0 auto; padding: 40px; text-align: center; background-color: #fff; box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1); } #no-assets-placeholder h1 { font-size: 24px; margin-bottom: 20px; } #no-assets-placeholder p { margin-bottom: 10px; } #no-assets-placeholder a { color: #0077cc; text-decoration: none; } #no-assets-placeholder a:hover { text-decoration: underline; } </style> <div id="no-assets-placeholder"> <h1>Failed loading TinaCMS assets</h1> <p> Your TinaCMS configuration may be misconfigured, and we could not load the assets for this page. </p> <p> Please visit <a href="https://tina.io/docs/r/FAQ/#13-how-do-i-resolve-failed-loading-tinacms-assets-error">this doc</a> for help. </p> </div> </div>';
}
</script>
<script
type="module"
src="http://localhost:4001/src/main.tsx"
onerror="handleLoadError()"
></script>
<body class="tina-tailwind">
<div id="root"></div>
</body>
</html>

View File

@ -13,7 +13,7 @@ const tl = createTranslator(Astro.currentLocale);
<div class="h-screen pb-20 max-h-[700px] sm:max-h-[900px] md:max-h-[1080px] container bg-[url(/img/DRJBP-1.webp)] bg-no-repeat bg-contain bg-bottom mx-auto mt-16">
<div class="grid md:grid-cols-2 h-full px-6">
<div class="gap-8 md:flex flex-col justify-end mt-14 md:mt-0">
<div class="gap-8 md:flex flex-col justify-end">
<img
src="/img/logo-metalico.webp"
alt="Logo Metalico"

View File

@ -27,19 +27,11 @@ locationArray.filter(Boolean).join(', ');
---
<div class="bg-[#EBE5D0] text-[#003421] p-10">
<div class="flex md:flex-col items-center content-center md:items-baseline">
<Icon name="ph:arrow-circle-down-thin" class="md:text-8xl text-7xl mb-0 md:mb-8" />
{nicedate ? (
<p class="font-light md:text-2xl text-lg ml-2 md:ml-0 md:mb-8 mb-3">
{locationArray.filter(Boolean).join(', ')}<br />
({nicedate}):
</p>
) : (
<p class="font-light md:text-2xl text-lg md:mb-8 mb-3">
{locationArray.filter(Boolean).join(', ')}:
</p>
)}
</div>
<Icon name="ph:arrow-circle-down-thin" class="text-8xl mb-8" />
<p class="font-light md:text-2xl text-lg md:mb-8 mb-3">
{locationArray.filter(Boolean).join(', ')}<br />
({nicedate}):
</p>
<h3 class="md:text-2xl text-lg mb-4 font-bold md:mb-8 hover:underline"><a href={`/${locale}/${getLocalizedRoute(routeKey, locale)}/${data.id}`}>{data.data.title}</a></h3>
<div class="overflow-hidden">

View File

@ -7,23 +7,23 @@ const tl = createTranslator(Astro.currentLocale);
const { props } = Astro.props;
---
<div class="container mx-auto">
<div class="grid md:grid-cols-2 min-w-0">
<div class="bg-[#EBE5D0] p-6 md:p-12 lg:p-24 grid lg:grid-cols-2 min-w-0">
<div class="flex flex-col justify-between min-h-full break-all">
<div class="grid md:grid-cols-2 min-h-100">
<div class="bg-[#EBE5D0] p-12 md:p-24 grid lg:grid-cols-2">
<div class="leftcol flex justify-between flex-col min-h-full">
<h2 class="text-5xl font-secondary text-tertiary font-bold">{tl("values.justice.title")}</h2>
<p class="text-lg text-tertiary break-words">{tl("values.justice.text")}</p>
<p class="text-lg text-tertiary">{tl("values.justice.text")}</p>
</div>
<div class="flex items-end justify-end min-h-full">
<Icon name="icon_justice_1" class="text-6xl md:text-9xl text-tertiary" />
<div class="rightcol flex justify-end align-bottom min-h-full ">
<Icon name="icon_justice_1" class="text-9xl text-tertiary self-end" />
</div>
</div>
<div class="bg-tertiary p-6 md:p-12 lg:p-24 grid lg:grid-cols-2 min-w-0">
<div class="flex flex-col justify-between min-h-full min-w-0 max-w-full">
<h2 class="text-5xl font-secondary font-bold text-[#CBA16A] break-all">{tl("values.integrity.title")}</h2>
<p class="text-[#CBA16A] text-lg break-words">{tl("values.integrity.text")}</p>
<div class="bg-tertiary p-12 md:p-24 grid lg:grid-cols-2">
<div class="leftcol flex justify-between flex-col min-h-full">
<h2 class="text-5xl font-secondary font-bold text-[#CBA16A]">{tl("values.integrity.title")}</h2>
<p class="text-[#CBA16A] text-lg">{tl("values.integrity.text")}</p>
</div>
<div class="flex items-end justify-end min-h-full min-w-0 max-w-full">
<Icon name="icon_justice_2" class="text-[#EBE5D0] text-6xl md:text-9xl max-w-full h-auto" />
<div class="rightcol flex justify-end align-bottom min-h-full ">
<Icon name="icon_justice_2" class="text-[#EBE5D0] text-9xl self-end" />
</div>
</div>
</div>
@ -41,4 +41,4 @@ const { props } = Astro.props;
<p class="text-white text-xl">{tl("values.dialogue.text")}</p>
</div>
</div>
</div>
</div>

View File

@ -92,34 +92,6 @@ const handleSubmit = async (e) => {
</fieldset>
<!-- Submit -->
<button
type="submit"
:disabled="isLoading"
class="bg-[#003421] px-6 py-2 text-sm cursor-pointer transition duration-300"
:class="[
isLoading ? 'opacity-70 cursor-not-allowed' : 'hover:bg-[#EBE5D0] hover:text-[#003421]',
isError ? 'bg-red-600 text-white' : ''
]"
>
<span v-if="isLoading" class="flex items-center gap-2">
<span class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
Enviando...
</span>
<span v-else-if="isSuccess">
Enviado
</span>
<span v-else-if="isError">
Error
</span>
<span v-else>
{{ tl("footer.form.button") }}
</span>
</button>
<div class="flex flex-row justify-between items-center mt-4">
<!-- Social Icons -->
@ -155,6 +127,34 @@ const handleSubmit = async (e) => {
</li>
</ul>
<!-- Submit -->
<button
type="submit"
:disabled="isLoading"
class="bg-[#003421] px-6 py-2 text-sm cursor-pointer transition duration-300"
:class="[
isLoading ? 'opacity-70 cursor-not-allowed' : 'hover:bg-[#EBE5D0] hover:text-[#003421]',
isError ? 'bg-red-600 text-white' : ''
]"
>
<span v-if="isLoading" class="flex items-center gap-2">
<span class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
Enviando...
</span>
<span v-else-if="isSuccess">
Enviado
</span>
<span v-else-if="isError">
Error
</span>
<span v-else>
{{ tl("footer.form.button") }}
</span>
</button>
</div>
</form>
</template>

View File

@ -10,39 +10,39 @@ const tl = createTranslator(Astro.currentLocale);
<h2 class="text-3xl lg:text-5xl text-center font-bold font-secondary my-8 text-[#003421]">{tl("formation.title")}</h2>
<p class="text-lg w-2/3 mx-auto mb-20 text-[#003421] text-justify" set:html={tl("formation.text")}></p>
<div class="grid xl:grid-cols-3 text-[#003421] md:gap-20 gap-10 md:p-0 p-5">
<div class="bg-[#EBE5D0] p-12 relative pb-32 md:pb-52">
<div class="grid xl:grid-cols-3 text-[#003421] gap-20">
<div class="bg-[#EBE5D0] p-12 relative pb-40">
<Icon name="ph:arrow-circle-down-thin" class="text-6xl mb-12 text-[#003421]" />
<h4 class="text-[#003421] text-2xl mb-12">{tl("formation.area.title")}</h4>
<ul class="text-lg list-disc font-normal list-inside leading-8" set:html={tl("formation.area.text")}></ul>
<div class="bottom-8 right-8 md:bottom-12 md:right-12 absolute">
<Icon name="icon_formation_1" class="text-8xl md:text-9xl text-[#003421]" />
<div class="bottom-12 right-12 absolute">
<Icon name="icon_formation_1" class="text-8xl text-[#003421]" />
</div>
</div>
<div class="bg-[#EBE5D0] p-12 relative pb-32 md:pb-52">
<div class="bg-[#EBE5D0] text-[#003421] p-12 relative pb-40">
<Icon name="ph:arrow-circle-down-thin" class="text-6xl mb-12 text-[#003421]" />
<h4 class="text-[#003421] text-2xl mb-12">{tl("formation.consulting.title")}</h4>
<p class="text-lg font-normal">{tl("formation.consulting.text")}</p>
<div class="bottom-8 right-8 md:bottom-12 md:right-12 absolute">
<Icon name="icon_formation_2" class="text-8xl md:text-9xl text-[#003421]" />
<div class="bottom-12 right-12 absolute">
<Icon name="icon_formation_2" class="text-8xl" />
</div>
</div>
<div class="bg-[#EBE5D0] p-12 relative pb-32 md:pb-52">
<div class="bg-[#EBE5D0] text-[#003421] p-12 relative pb-40">
<Icon name="ph:arrow-circle-down-thin" class="text-6xl mb-12 text-[#003421]" />
<h4 class="text-[#003421] text-2xl mb-12" set:html={tl("formation.action.title")}></h4>
<p class="text-lg font-normal" set:html={tl("formation.action.text")}></p>
<div class="bottom-8 right-8 md:bottom-12 md:right-12 absolute">
<Icon name="icon_formation_3" class="text-8xl md:text-9xl text-[#003421]" />
<div class="bottom-12 right-12 absolute">
<Icon name="icon_formation_3" class="text-8xl" />
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,3 @@
// import Cloudflare from "cloudflare";
type TypeEmailData = "CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_INFO" | "CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_POSTULACION";
export async function sendEmail(data: object, to: string, type: TypeEmailData) {
@ -29,110 +27,4 @@ export async function sendEmail(data: object, to: string, type: TypeEmailData) {
console.error(`Error enviando correo (status ${emailResponse.status}): ${responseBody}`);
throw new Error(`Error en servicio de correo: ${emailResponse.status}`);
}
}
// ═══════════════════════════════════════════════════════════════
// CÓDIGO COMENTADO — Cloudflare (Email Workers)
// Descomentar para volver a Cloudflare en lugar de n8n/Brevo
// ═══════════════════════════════════════════════════════════════
// const TEMPLATE_URLS: Record<TypeEmailData, { subject: string; url: string }> = {
// CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_POSTULACION: {
// subject: "Confirmación de inscripción al voluntariado",
// url: "https://pub-910ed90860804520b9202194cf43c0a6.r2.dev/centro_del_reino_paz_y_justicia_postulacion_user.html",
// },
// CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_INFO: {
// subject: "Notificación de contacto institucional",
// url: "https://pub-910ed90860804520b9202194cf43c0a6.r2.dev/centro_del_reino_paz_y_justicia_info.html",
// },
// };
// const TEMPLATE_CACHE = new Map<string, string>();
// async function fetchTemplate(type: TypeEmailData): Promise<string> {
// const cached = TEMPLATE_CACHE.get(type);
// if (cached) return cached;
// const { url } = TEMPLATE_URLS[type];
// const res = await fetch(url);
// if (!res.ok) {
// throw new Error(`Error descargando template ${type}: HTTP ${res.status}`);
// }
// const html = await res.text();
// TEMPLATE_CACHE.set(type, html);
// return html;
// }
// function renderTemplate(template: string, data: Record<string, string>): string {
// let html = template;
// for (const [key, value] of Object.entries(data)) {
// html = html.replaceAll(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value ?? "");
// }
// return html;
// }
// function stripHtml(html: string): string {
// return html.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
// }
// export async function sendEmailCf(data: Record<string, string>, to: string, type: TypeEmailData) {
// const apiToken = import.meta.env.CLOUDFLARE_API_TOKEN;
// const accountId = import.meta.env.CLOUDFLARE_ACCOUNT_ID;
// const client = new Cloudflare({ apiToken });
// const { subject } = TEMPLATE_URLS[type];
// const rawHtml = await fetchTemplate(type);
// const html = renderTemplate(rawHtml, data);
// const text = stripHtml(html);
// const response = await client.emailSending.send({
// account_id: accountId,
// from: "welcome@mail.centrodelreinodepazyjusticia.com",
// to,
// subject,
// html,
// text,
// });
// if (!response.delivered) {
// console.error("Cloudflare email not delivered", response);
// throw new Error("El correo no fue entregado");
// }
// }
// ═══════════════════════════════════════════════════════════════
// ALTERNATIVA: Brevo (SDK directo)
// Descomentar y usar sendEmailViaBrevo() en lugar de lo de arriba
// Requiere: pnpm add @getbrevo/brevo + BREVO_API_KEY en .env
// ═══════════════════════════════════════════════════════════════
// import {
// TransactionalEmailsApi,
// SendSmtpEmail,
// } from "@getbrevo/brevo";
// async function sendEmailViaBrevo(
// data: Record<string, string>,
// to: string,
// type: TypeEmailData
// ) {
// const apiKey = import.meta.env.BREVO_API_KEY;
// const { subject } = TEMPLATE_URLS[type];
// const rawHtml = await fetchTemplate(type);
// const html = renderTemplate(rawHtml, data);
// const text = stripHtml(html);
// const api = new TransactionalEmailsApi();
// api.setApiKey(0, apiKey);
// const email = new SendSmtpEmail();
// email.subject = subject;
// email.sender = { name: "Centro del Reino de Paz y Justicia", email: "welcome@mail.centrodelreinodepazyjusticia.com" };
// email.to = [{ email: to }];
// email.htmlContent = html;
// email.textContent = text;
// const response = await api.sendTransacEmail(email);
// console.log("Brevo email sent:", response.body?.messageId);
// }
}

1
tina/__generated__/_graphql.json generated Normal file

File diff suppressed because one or more lines are too long

1
tina/__generated__/_lookup.json generated Normal file
View File

@ -0,0 +1 @@
{"DocumentConnection":{"type":"DocumentConnection","resolveType":"multiCollectionDocumentList","collections":["news","editorial"]},"Node":{"type":"Node","resolveType":"nodeDocument"},"DocumentNode":{"type":"DocumentNode","resolveType":"multiCollectionDocument","createDocument":"create","updateDocument":"update"},"News":{"type":"News","resolveType":"collectionDocument","collection":"news","createNews":"create","updateNews":"update"},"NewsConnection":{"type":"NewsConnection","resolveType":"collectionDocumentList","collection":"news"},"Editorial":{"type":"Editorial","resolveType":"collectionDocument","collection":"editorial","createEditorial":"create","updateEditorial":"update"},"EditorialConnection":{"type":"EditorialConnection","resolveType":"collectionDocumentList","collection":"editorial"}}

1
tina/__generated__/_schema.json generated Normal file
View File

@ -0,0 +1 @@
{"version":{"fullVersion":"2.4.2","major":"2","minor":"4","patch":"2"},"meta":{"flags":["experimentalData"]},"collections":[{"name":"news","label":"News","path":"src/content/news","format":"md","match":{"include":"**/*"},"ui":{"filename":{"readonly":false}},"fields":[{"type":"string","name":"locale","label":"Language","options":[{"label":"Español","value":"es"},{"label":"English","value":"en"},{"label":"Português","value":"pt"},{"label":"Kinyarwanda","value":"rw"},{"label":"Frances","value":"fr"}],"namespace":["news","locale"],"searchable":true,"uid":false},{"type":"string","name":"title","label":"Title","isTitle":true,"required":true,"namespace":["news","title"],"searchable":true,"uid":false},{"type":"datetime","name":"date","label":"Date","required":true,"namespace":["news","date"],"searchable":true,"uid":false},{"type":"string","name":"slug","label":"Slug","namespace":["news","slug"],"searchable":true,"uid":false},{"type":"string","name":"place","label":"Place","namespace":["news","place"],"searchable":true,"uid":false},{"type":"string","name":"country","label":"Country","namespace":["news","country"],"searchable":true,"uid":false},{"type":"string","name":"city","label":"City","namespace":["news","city"],"searchable":true,"uid":false},{"type":"image","name":"thumbnail","label":"Thumbnail URL","namespace":["news","thumbnail"],"searchable":false,"uid":false},{"type":"image","name":"thumbnail_square","label":"Thumbnail Square URL","namespace":["news","thumbnail_square"],"searchable":false,"uid":false},{"type":"string","name":"tags","label":"Tags","list":true,"namespace":["news","tags"],"searchable":true,"uid":false},{"type":"object","name":"gallery","label":"Gallery","list":true,"fields":[{"type":"image","name":"image","label":"Image","namespace":["news","gallery","image"],"searchable":false,"uid":false}],"namespace":["news","gallery"],"searchable":true,"uid":false},{"type":"string","name":"youtube","label":"YouTube ID","namespace":["news","youtube"],"searchable":true,"uid":false},{"type":"boolean","name":"draft","label":"Draft","namespace":["news","draft"],"searchable":true,"uid":false},{"type":"rich-text","name":"body","label":"Content","isBody":true,"namespace":["news","body"],"searchable":true,"parser":{"type":"markdown"},"uid":false}],"namespace":["news"]},{"name":"editorial","label":"Editorial","path":"src/content/editorial","format":"md","match":{"include":"**/*"},"ui":{"filename":{"readonly":false}},"fields":[{"type":"string","name":"locale","label":"Language","options":[{"label":"Español","value":"es"},{"label":"English","value":"en"},{"label":"Português","value":"pt"},{"label":"Kinyarwanda","value":"rw"},{"label":"Frances","value":"fr"}],"namespace":["editorial","locale"],"searchable":true,"uid":false},{"type":"string","name":"title","label":"Title","isTitle":true,"required":true,"namespace":["editorial","title"],"searchable":true,"uid":false},{"type":"datetime","name":"date","label":"Date","required":true,"namespace":["editorial","date"],"searchable":true,"uid":false},{"type":"string","name":"slug","label":"Slug","namespace":["editorial","slug"],"searchable":true,"uid":false},{"type":"string","name":"place","label":"Place","namespace":["editorial","place"],"searchable":true,"uid":false},{"type":"string","name":"country","label":"Country","namespace":["editorial","country"],"searchable":true,"uid":false},{"type":"string","name":"city","label":"City","namespace":["editorial","city"],"searchable":true,"uid":false},{"type":"image","name":"thumbnail","label":"Thumbnail URL","namespace":["editorial","thumbnail"],"searchable":false,"uid":false},{"type":"image","name":"thumbnail_square","label":"Thumbnail Square URL","namespace":["editorial","thumbnail_square"],"searchable":false,"uid":false},{"type":"string","name":"tags","label":"Tags","list":true,"namespace":["editorial","tags"],"searchable":true,"uid":false},{"type":"object","name":"gallery","label":"Gallery","list":true,"fields":[{"type":"image","name":"image","label":"Image","namespace":["editorial","gallery","image"],"searchable":false,"uid":false}],"namespace":["editorial","gallery"],"searchable":true,"uid":false},{"type":"string","name":"youtube","label":"YouTube ID","namespace":["editorial","youtube"],"searchable":true,"uid":false},{"type":"boolean","name":"draft","label":"Draft","namespace":["editorial","draft"],"searchable":true,"uid":false},{"type":"rich-text","name":"body","label":"Content","isBody":true,"namespace":["editorial","body"],"searchable":true,"parser":{"type":"markdown"},"uid":false}],"namespace":["editorial"]}],"config":{"media":{"tina":{"publicFolder":"public","mediaRoot":"public/images"}}}}

5
tina/__generated__/client.ts generated Normal file
View File

@ -0,0 +1,5 @@
import { createClient } from "tinacms/dist/client";
import { queries } from "./types.js";
export const client = createClient({ url: 'http://localhost:4001/graphql', token: 'null', queries, });
export default client;

243
tina/__generated__/config.prebuild.jsx generated Normal file
View File

@ -0,0 +1,243 @@
// tina/config.ts
import { defineConfig } from "tinacms";
var branch = process.env.HEAD || process.env.VERCEL_GIT_COMMIT_REF || "main";
var config_default = defineConfig({
branch,
clientId: null,
token: null,
build: {
outputFolder: "admin",
publicFolder: "public"
},
media: {
tina: {
mediaRoot: "public/images",
publicFolder: "public"
}
},
schema: {
collections: [
{
name: "news",
label: "News",
path: "src/content/news",
format: "md",
match: {
include: "**/*"
},
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.slug?.toLowerCase() || values?.title?.toLowerCase().replace(/ /g, "-");
}
}
},
fields: [
{
type: "string",
name: "locale",
label: "Language",
options: [
{ label: "Espa\xF1ol", value: "es" },
{ label: "English", value: "en" },
{ label: "Portugu\xEAs", value: "pt" },
{ label: "Kinyarwanda", value: "rw" },
{ label: "Frances", value: "fr" }
]
},
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true
},
{
type: "datetime",
name: "date",
label: "Date",
required: true
},
{
type: "string",
name: "slug",
label: "Slug"
},
{
type: "string",
name: "place",
label: "Place"
},
{
type: "string",
name: "country",
label: "Country"
},
{
type: "string",
name: "city",
label: "City"
},
{
type: "image",
name: "thumbnail",
label: "Thumbnail URL"
},
{
type: "image",
name: "thumbnail_square",
label: "Thumbnail Square URL"
},
{
type: "string",
name: "tags",
label: "Tags",
list: true
},
{
type: "object",
name: "gallery",
label: "Gallery",
list: true,
fields: [
{
type: "image",
name: "image",
label: "Image"
}
]
},
{
type: "string",
name: "youtube",
label: "YouTube ID"
},
{
type: "boolean",
name: "draft",
label: "Draft"
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true
}
]
},
{
name: "editorial",
label: "Editorial",
path: "src/content/editorial",
format: "md",
match: {
include: "**/*"
},
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.slug?.toLowerCase() || values?.title?.toLowerCase().replace(/ /g, "-");
}
}
},
fields: [
{
type: "string",
name: "locale",
label: "Language",
options: [
{ label: "Espa\xF1ol", value: "es" },
{ label: "English", value: "en" },
{ label: "Portugu\xEAs", value: "pt" },
{ label: "Kinyarwanda", value: "rw" },
{ label: "Frances", value: "fr" }
]
},
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true
},
{
type: "datetime",
name: "date",
label: "Date",
required: true
},
{
type: "string",
name: "slug",
label: "Slug"
},
{
type: "string",
name: "place",
label: "Place"
},
{
type: "string",
name: "country",
label: "Country"
},
{
type: "string",
name: "city",
label: "City"
},
{
type: "image",
name: "thumbnail",
label: "Thumbnail URL"
},
{
type: "image",
name: "thumbnail_square",
label: "Thumbnail Square URL"
},
{
type: "string",
name: "tags",
label: "Tags",
list: true
},
{
type: "object",
name: "gallery",
label: "Gallery",
list: true,
fields: [
{
type: "image",
name: "image",
label: "Image"
}
]
},
{
type: "string",
name: "youtube",
label: "YouTube ID"
},
{
type: "boolean",
name: "draft",
label: "Draft"
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true
}
]
}
]
}
});
export {
config_default as default
};

41
tina/__generated__/frags.gql generated Normal file
View File

@ -0,0 +1,41 @@
fragment NewsParts on News {
__typename
locale
title
date
slug
place
country
city
thumbnail
thumbnail_square
tags
gallery {
__typename
image
}
youtube
draft
body
}
fragment EditorialParts on Editorial {
__typename
locale
title
date
slug
place
country
city
thumbnail
thumbnail_square
tags
gallery {
__typename
image
}
youtube
draft
body
}

109
tina/__generated__/queries.gql generated Normal file
View File

@ -0,0 +1,109 @@
query news($relativePath: String!) {
news(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...NewsParts
}
}
query newsConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: NewsFilter) {
newsConnection(
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
}
...NewsParts
}
}
}
}
query editorial($relativePath: String!) {
editorial(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...EditorialParts
}
}
query editorialConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: EditorialFilter) {
editorialConnection(
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
}
...EditorialParts
}
}
}
}

306
tina/__generated__/schema.gql generated Normal file
View File

@ -0,0 +1,306 @@
# 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!
news(relativePath: String): News!
newsConnection(before: String, after: String, first: Float, last: Float, sort: String, filter: NewsFilter): NewsConnection!
editorial(relativePath: String): Editorial!
editorialConnection(before: String, after: String, first: Float, last: Float, sort: String, filter: EditorialFilter): EditorialConnection!
}
input DocumentFilter {
news: NewsFilter
editorial: EditorialFilter
}
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 = News | Editorial | Folder
type NewsGallery {
image: String
}
type News implements Node & Document {
locale: String
title: String!
date: String!
slug: String
place: String
country: String
city: String
thumbnail: String
thumbnail_square: String
tags: [String]
gallery: [NewsGallery]
youtube: String
draft: Boolean
body: JSON
id: ID!
_sys: SystemInfo!
_values: JSON!
}
input StringFilter {
startsWith: String
eq: String
exists: Boolean
in: [String]
}
input DatetimeFilter {
after: String
before: String
eq: String
exists: Boolean
in: [String]
}
input ImageFilter {
startsWith: String
eq: String
exists: Boolean
in: [String]
}
input NewsGalleryFilter {
image: ImageFilter
}
input BooleanFilter {
eq: Boolean
exists: Boolean
}
input RichTextFilter {
startsWith: String
eq: String
exists: Boolean
}
input NewsFilter {
locale: StringFilter
title: StringFilter
date: DatetimeFilter
slug: StringFilter
place: StringFilter
country: StringFilter
city: StringFilter
thumbnail: ImageFilter
thumbnail_square: ImageFilter
tags: StringFilter
gallery: NewsGalleryFilter
youtube: StringFilter
draft: BooleanFilter
body: RichTextFilter
}
type NewsConnectionEdges {
cursor: String!
node: News
}
type NewsConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [NewsConnectionEdges]
}
type EditorialGallery {
image: String
}
type Editorial implements Node & Document {
locale: String
title: String!
date: String!
slug: String
place: String
country: String
city: String
thumbnail: String
thumbnail_square: String
tags: [String]
gallery: [EditorialGallery]
youtube: String
draft: Boolean
body: JSON
id: ID!
_sys: SystemInfo!
_values: JSON!
}
input EditorialGalleryFilter {
image: ImageFilter
}
input EditorialFilter {
locale: StringFilter
title: StringFilter
date: DatetimeFilter
slug: StringFilter
place: StringFilter
country: StringFilter
city: StringFilter
thumbnail: ImageFilter
thumbnail_square: ImageFilter
tags: StringFilter
gallery: EditorialGalleryFilter
youtube: StringFilter
draft: BooleanFilter
body: RichTextFilter
}
type EditorialConnectionEdges {
cursor: String!
node: Editorial
}
type EditorialConnection implements Connection {
pageInfo: PageInfo!
totalCount: Float!
edges: [EditorialConnectionEdges]
}
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!
updateNews(relativePath: String!, params: NewsMutation!): News!
createNews(relativePath: String!, params: NewsMutation!): News!
updateEditorial(relativePath: String!, params: EditorialMutation!): Editorial!
createEditorial(relativePath: String!, params: EditorialMutation!): Editorial!
}
input DocumentUpdateMutation {
news: NewsMutation
editorial: EditorialMutation
relativePath: String
}
input DocumentMutation {
news: NewsMutation
editorial: EditorialMutation
}
input NewsGalleryMutation {
image: String
}
input NewsMutation {
locale: String
title: String
date: String
slug: String
place: String
country: String
city: String
thumbnail: String
thumbnail_square: String
tags: [String]
gallery: [NewsGalleryMutation]
youtube: String
draft: Boolean
body: JSON
}
input EditorialGalleryMutation {
image: String
}
input EditorialMutation {
locale: String
title: String
date: String
slug: String
place: String
country: String
city: String
thumbnail: String
thumbnail_square: String
tags: [String]
gallery: [EditorialGalleryMutation]
youtube: String
draft: Boolean
body: JSON
}
schema {
query: Query
mutation: Mutation
}

1
tina/__generated__/static-media.json generated Normal file
View File

@ -0,0 +1 @@
[]

210
tina/__generated__/types.js generated Normal file
View File

@ -0,0 +1,210 @@
export function gql(strings, ...args) {
let str = "";
strings.forEach((string, i) => {
str += string + (args[i] || "");
});
return str;
}
export const NewsPartsFragmentDoc = gql`
fragment NewsParts on News {
__typename
locale
title
date
slug
place
country
city
thumbnail
thumbnail_square
tags
gallery {
__typename
image
}
youtube
draft
body
}
`;
export const EditorialPartsFragmentDoc = gql`
fragment EditorialParts on Editorial {
__typename
locale
title
date
slug
place
country
city
thumbnail
thumbnail_square
tags
gallery {
__typename
image
}
youtube
draft
body
}
`;
export const NewsDocument = gql`
query news($relativePath: String!) {
news(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...NewsParts
}
}
${NewsPartsFragmentDoc}`;
export const NewsConnectionDocument = gql`
query newsConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: NewsFilter) {
newsConnection(
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
}
...NewsParts
}
}
}
}
${NewsPartsFragmentDoc}`;
export const EditorialDocument = gql`
query editorial($relativePath: String!) {
editorial(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...EditorialParts
}
}
${EditorialPartsFragmentDoc}`;
export const EditorialConnectionDocument = gql`
query editorialConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: EditorialFilter) {
editorialConnection(
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
}
...EditorialParts
}
}
}
}
${EditorialPartsFragmentDoc}`;
export function getSdk(requester) {
return {
news(variables, options) {
return requester(NewsDocument, variables, options);
},
newsConnection(variables, options) {
return requester(NewsConnectionDocument, variables, options);
},
editorial(variables, options) {
return requester(EditorialDocument, variables, options);
},
editorialConnection(variables, options) {
return requester(EditorialConnectionDocument, variables, options);
}
};
}
import { createClient } from "tinacms/dist/client";
const generateRequester = (client) => {
const requester = async (doc, vars, options) => {
let url = client.apiUrl;
if (options?.branch) {
const index = client.apiUrl.lastIndexOf("/");
url = client.apiUrl.substring(0, index + 1) + options.branch;
}
const data = await client.request({
query: doc,
variables: vars,
url
}, options);
return { data: data?.data, errors: data?.errors, query: doc, variables: vars || {} };
};
return requester;
};
export const ExperimentalGetTinaClient = () => getSdk(
generateRequester(
createClient({
url: "http://localhost:4001/graphql",
queries
})
)
);
export const queries = (client) => {
const requester = generateRequester(client);
return getSdk(requester);
};

737
tina/__generated__/types.ts generated Normal file
View File

@ -0,0 +1,737 @@
//@ts-nocheck
// DO NOT MODIFY THIS FILE. This file is automatically generated by Tina
export function gql(strings: TemplateStringsArray, ...args: string[]): string {
let str = ''
strings.forEach((string, i) => {
str += string + (args[i] || '')
})
return str
}
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
/** References another document, used as a foreign key */
Reference: { input: any; output: any; }
JSON: { input: any; output: any; }
};
export type SystemInfo = {
__typename?: 'SystemInfo';
filename: Scalars['String']['output'];
title?: Maybe<Scalars['String']['output']>;
basename: Scalars['String']['output'];
hasReferences?: Maybe<Scalars['Boolean']['output']>;
breadcrumbs: Array<Scalars['String']['output']>;
path: Scalars['String']['output'];
relativePath: Scalars['String']['output'];
extension: Scalars['String']['output'];
template: Scalars['String']['output'];
collection: Collection;
};
export type SystemInfoBreadcrumbsArgs = {
excludeExtension?: InputMaybe<Scalars['Boolean']['input']>;
};
export type Folder = {
__typename?: 'Folder';
name: Scalars['String']['output'];
path: Scalars['String']['output'];
};
export type PageInfo = {
__typename?: 'PageInfo';
hasPreviousPage: Scalars['Boolean']['output'];
hasNextPage: Scalars['Boolean']['output'];
startCursor: Scalars['String']['output'];
endCursor: Scalars['String']['output'];
};
export type Node = {
id: Scalars['ID']['output'];
};
export type Document = {
id: Scalars['ID']['output'];
_sys?: Maybe<SystemInfo>;
_values: Scalars['JSON']['output'];
};
/** A relay-compliant pagination connection */
export type Connection = {
totalCount: Scalars['Float']['output'];
pageInfo: PageInfo;
};
export type Query = {
__typename?: 'Query';
getOptimizedQuery?: Maybe<Scalars['String']['output']>;
collection: Collection;
collections: Array<Collection>;
node: Node;
document: DocumentNode;
news: News;
newsConnection: NewsConnection;
editorial: Editorial;
editorialConnection: EditorialConnection;
};
export type QueryGetOptimizedQueryArgs = {
queryString: Scalars['String']['input'];
};
export type QueryCollectionArgs = {
collection?: InputMaybe<Scalars['String']['input']>;
};
export type QueryNodeArgs = {
id?: InputMaybe<Scalars['String']['input']>;
};
export type QueryDocumentArgs = {
collection?: InputMaybe<Scalars['String']['input']>;
relativePath?: InputMaybe<Scalars['String']['input']>;
};
export type QueryNewsArgs = {
relativePath?: InputMaybe<Scalars['String']['input']>;
};
export type QueryNewsConnectionArgs = {
before?: InputMaybe<Scalars['String']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
first?: InputMaybe<Scalars['Float']['input']>;
last?: InputMaybe<Scalars['Float']['input']>;
sort?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<NewsFilter>;
};
export type QueryEditorialArgs = {
relativePath?: InputMaybe<Scalars['String']['input']>;
};
export type QueryEditorialConnectionArgs = {
before?: InputMaybe<Scalars['String']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
first?: InputMaybe<Scalars['Float']['input']>;
last?: InputMaybe<Scalars['Float']['input']>;
sort?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<EditorialFilter>;
};
export type DocumentFilter = {
news?: InputMaybe<NewsFilter>;
editorial?: InputMaybe<EditorialFilter>;
};
export type DocumentConnectionEdges = {
__typename?: 'DocumentConnectionEdges';
cursor: Scalars['String']['output'];
node?: Maybe<DocumentNode>;
};
export type DocumentConnection = Connection & {
__typename?: 'DocumentConnection';
pageInfo: PageInfo;
totalCount: Scalars['Float']['output'];
edges?: Maybe<Array<Maybe<DocumentConnectionEdges>>>;
};
export type Collection = {
__typename?: 'Collection';
name: Scalars['String']['output'];
slug: Scalars['String']['output'];
label?: Maybe<Scalars['String']['output']>;
path: Scalars['String']['output'];
format?: Maybe<Scalars['String']['output']>;
matches?: Maybe<Scalars['String']['output']>;
templates?: Maybe<Array<Maybe<Scalars['JSON']['output']>>>;
fields?: Maybe<Array<Maybe<Scalars['JSON']['output']>>>;
documents: DocumentConnection;
};
export type CollectionDocumentsArgs = {
before?: InputMaybe<Scalars['String']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
first?: InputMaybe<Scalars['Float']['input']>;
last?: InputMaybe<Scalars['Float']['input']>;
sort?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<DocumentFilter>;
folder?: InputMaybe<Scalars['String']['input']>;
};
export type DocumentNode = News | Editorial | Folder;
export type NewsGallery = {
__typename?: 'NewsGallery';
image?: Maybe<Scalars['String']['output']>;
};
export type News = Node & Document & {
__typename?: 'News';
locale?: Maybe<Scalars['String']['output']>;
title: Scalars['String']['output'];
date: Scalars['String']['output'];
slug?: Maybe<Scalars['String']['output']>;
place?: Maybe<Scalars['String']['output']>;
country?: Maybe<Scalars['String']['output']>;
city?: Maybe<Scalars['String']['output']>;
thumbnail?: Maybe<Scalars['String']['output']>;
thumbnail_square?: Maybe<Scalars['String']['output']>;
tags?: Maybe<Array<Maybe<Scalars['String']['output']>>>;
gallery?: Maybe<Array<Maybe<NewsGallery>>>;
youtube?: Maybe<Scalars['String']['output']>;
draft?: Maybe<Scalars['Boolean']['output']>;
body?: Maybe<Scalars['JSON']['output']>;
id: Scalars['ID']['output'];
_sys: SystemInfo;
_values: Scalars['JSON']['output'];
};
export type StringFilter = {
startsWith?: InputMaybe<Scalars['String']['input']>;
eq?: InputMaybe<Scalars['String']['input']>;
exists?: InputMaybe<Scalars['Boolean']['input']>;
in?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
};
export type DatetimeFilter = {
after?: InputMaybe<Scalars['String']['input']>;
before?: InputMaybe<Scalars['String']['input']>;
eq?: InputMaybe<Scalars['String']['input']>;
exists?: InputMaybe<Scalars['Boolean']['input']>;
in?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
};
export type ImageFilter = {
startsWith?: InputMaybe<Scalars['String']['input']>;
eq?: InputMaybe<Scalars['String']['input']>;
exists?: InputMaybe<Scalars['Boolean']['input']>;
in?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
};
export type NewsGalleryFilter = {
image?: InputMaybe<ImageFilter>;
};
export type BooleanFilter = {
eq?: InputMaybe<Scalars['Boolean']['input']>;
exists?: InputMaybe<Scalars['Boolean']['input']>;
};
export type RichTextFilter = {
startsWith?: InputMaybe<Scalars['String']['input']>;
eq?: InputMaybe<Scalars['String']['input']>;
exists?: InputMaybe<Scalars['Boolean']['input']>;
};
export type NewsFilter = {
locale?: InputMaybe<StringFilter>;
title?: InputMaybe<StringFilter>;
date?: InputMaybe<DatetimeFilter>;
slug?: InputMaybe<StringFilter>;
place?: InputMaybe<StringFilter>;
country?: InputMaybe<StringFilter>;
city?: InputMaybe<StringFilter>;
thumbnail?: InputMaybe<ImageFilter>;
thumbnail_square?: InputMaybe<ImageFilter>;
tags?: InputMaybe<StringFilter>;
gallery?: InputMaybe<NewsGalleryFilter>;
youtube?: InputMaybe<StringFilter>;
draft?: InputMaybe<BooleanFilter>;
body?: InputMaybe<RichTextFilter>;
};
export type NewsConnectionEdges = {
__typename?: 'NewsConnectionEdges';
cursor: Scalars['String']['output'];
node?: Maybe<News>;
};
export type NewsConnection = Connection & {
__typename?: 'NewsConnection';
pageInfo: PageInfo;
totalCount: Scalars['Float']['output'];
edges?: Maybe<Array<Maybe<NewsConnectionEdges>>>;
};
export type EditorialGallery = {
__typename?: 'EditorialGallery';
image?: Maybe<Scalars['String']['output']>;
};
export type Editorial = Node & Document & {
__typename?: 'Editorial';
locale?: Maybe<Scalars['String']['output']>;
title: Scalars['String']['output'];
date: Scalars['String']['output'];
slug?: Maybe<Scalars['String']['output']>;
place?: Maybe<Scalars['String']['output']>;
country?: Maybe<Scalars['String']['output']>;
city?: Maybe<Scalars['String']['output']>;
thumbnail?: Maybe<Scalars['String']['output']>;
thumbnail_square?: Maybe<Scalars['String']['output']>;
tags?: Maybe<Array<Maybe<Scalars['String']['output']>>>;
gallery?: Maybe<Array<Maybe<EditorialGallery>>>;
youtube?: Maybe<Scalars['String']['output']>;
draft?: Maybe<Scalars['Boolean']['output']>;
body?: Maybe<Scalars['JSON']['output']>;
id: Scalars['ID']['output'];
_sys: SystemInfo;
_values: Scalars['JSON']['output'];
};
export type EditorialGalleryFilter = {
image?: InputMaybe<ImageFilter>;
};
export type EditorialFilter = {
locale?: InputMaybe<StringFilter>;
title?: InputMaybe<StringFilter>;
date?: InputMaybe<DatetimeFilter>;
slug?: InputMaybe<StringFilter>;
place?: InputMaybe<StringFilter>;
country?: InputMaybe<StringFilter>;
city?: InputMaybe<StringFilter>;
thumbnail?: InputMaybe<ImageFilter>;
thumbnail_square?: InputMaybe<ImageFilter>;
tags?: InputMaybe<StringFilter>;
gallery?: InputMaybe<EditorialGalleryFilter>;
youtube?: InputMaybe<StringFilter>;
draft?: InputMaybe<BooleanFilter>;
body?: InputMaybe<RichTextFilter>;
};
export type EditorialConnectionEdges = {
__typename?: 'EditorialConnectionEdges';
cursor: Scalars['String']['output'];
node?: Maybe<Editorial>;
};
export type EditorialConnection = Connection & {
__typename?: 'EditorialConnection';
pageInfo: PageInfo;
totalCount: Scalars['Float']['output'];
edges?: Maybe<Array<Maybe<EditorialConnectionEdges>>>;
};
export type Mutation = {
__typename?: 'Mutation';
addPendingDocument: DocumentNode;
updateDocument: DocumentNode;
deleteDocument: DocumentNode;
createDocument: DocumentNode;
createFolder: DocumentNode;
updateNews: News;
createNews: News;
updateEditorial: Editorial;
createEditorial: Editorial;
};
export type MutationAddPendingDocumentArgs = {
collection: Scalars['String']['input'];
relativePath: Scalars['String']['input'];
template?: InputMaybe<Scalars['String']['input']>;
};
export type MutationUpdateDocumentArgs = {
collection?: InputMaybe<Scalars['String']['input']>;
relativePath: Scalars['String']['input'];
params: DocumentUpdateMutation;
};
export type MutationDeleteDocumentArgs = {
collection?: InputMaybe<Scalars['String']['input']>;
relativePath: Scalars['String']['input'];
};
export type MutationCreateDocumentArgs = {
collection?: InputMaybe<Scalars['String']['input']>;
relativePath: Scalars['String']['input'];
params: DocumentMutation;
};
export type MutationCreateFolderArgs = {
collection?: InputMaybe<Scalars['String']['input']>;
relativePath: Scalars['String']['input'];
};
export type MutationUpdateNewsArgs = {
relativePath: Scalars['String']['input'];
params: NewsMutation;
};
export type MutationCreateNewsArgs = {
relativePath: Scalars['String']['input'];
params: NewsMutation;
};
export type MutationUpdateEditorialArgs = {
relativePath: Scalars['String']['input'];
params: EditorialMutation;
};
export type MutationCreateEditorialArgs = {
relativePath: Scalars['String']['input'];
params: EditorialMutation;
};
export type DocumentUpdateMutation = {
news?: InputMaybe<NewsMutation>;
editorial?: InputMaybe<EditorialMutation>;
relativePath?: InputMaybe<Scalars['String']['input']>;
};
export type DocumentMutation = {
news?: InputMaybe<NewsMutation>;
editorial?: InputMaybe<EditorialMutation>;
};
export type NewsGalleryMutation = {
image?: InputMaybe<Scalars['String']['input']>;
};
export type NewsMutation = {
locale?: InputMaybe<Scalars['String']['input']>;
title?: InputMaybe<Scalars['String']['input']>;
date?: InputMaybe<Scalars['String']['input']>;
slug?: InputMaybe<Scalars['String']['input']>;
place?: InputMaybe<Scalars['String']['input']>;
country?: InputMaybe<Scalars['String']['input']>;
city?: InputMaybe<Scalars['String']['input']>;
thumbnail?: InputMaybe<Scalars['String']['input']>;
thumbnail_square?: InputMaybe<Scalars['String']['input']>;
tags?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
gallery?: InputMaybe<Array<InputMaybe<NewsGalleryMutation>>>;
youtube?: InputMaybe<Scalars['String']['input']>;
draft?: InputMaybe<Scalars['Boolean']['input']>;
body?: InputMaybe<Scalars['JSON']['input']>;
};
export type EditorialGalleryMutation = {
image?: InputMaybe<Scalars['String']['input']>;
};
export type EditorialMutation = {
locale?: InputMaybe<Scalars['String']['input']>;
title?: InputMaybe<Scalars['String']['input']>;
date?: InputMaybe<Scalars['String']['input']>;
slug?: InputMaybe<Scalars['String']['input']>;
place?: InputMaybe<Scalars['String']['input']>;
country?: InputMaybe<Scalars['String']['input']>;
city?: InputMaybe<Scalars['String']['input']>;
thumbnail?: InputMaybe<Scalars['String']['input']>;
thumbnail_square?: InputMaybe<Scalars['String']['input']>;
tags?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
gallery?: InputMaybe<Array<InputMaybe<EditorialGalleryMutation>>>;
youtube?: InputMaybe<Scalars['String']['input']>;
draft?: InputMaybe<Scalars['Boolean']['input']>;
body?: InputMaybe<Scalars['JSON']['input']>;
};
export type NewsPartsFragment = { __typename: 'News', locale?: string | null, title: string, date: string, slug?: string | null, place?: string | null, country?: string | null, city?: string | null, thumbnail?: string | null, thumbnail_square?: string | null, tags?: Array<string | null> | null, youtube?: string | null, draft?: boolean | null, body?: any | null, gallery?: Array<{ __typename: 'NewsGallery', image?: string | null } | null> | null };
export type EditorialPartsFragment = { __typename: 'Editorial', locale?: string | null, title: string, date: string, slug?: string | null, place?: string | null, country?: string | null, city?: string | null, thumbnail?: string | null, thumbnail_square?: string | null, tags?: Array<string | null> | null, youtube?: string | null, draft?: boolean | null, body?: any | null, gallery?: Array<{ __typename: 'EditorialGallery', image?: string | null } | null> | null };
export type NewsQueryVariables = Exact<{
relativePath: Scalars['String']['input'];
}>;
export type NewsQuery = { __typename?: 'Query', news: { __typename: 'News', id: string, locale?: string | null, title: string, date: string, slug?: string | null, place?: string | null, country?: string | null, city?: string | null, thumbnail?: string | null, thumbnail_square?: string | null, tags?: Array<string | null> | null, youtube?: string | null, draft?: boolean | null, body?: any | null, _sys: { __typename?: 'SystemInfo', filename: string, basename: string, hasReferences?: boolean | null, breadcrumbs: Array<string>, path: string, relativePath: string, extension: string }, gallery?: Array<{ __typename: 'NewsGallery', image?: string | null } | null> | null } };
export type NewsConnectionQueryVariables = Exact<{
before?: InputMaybe<Scalars['String']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
first?: InputMaybe<Scalars['Float']['input']>;
last?: InputMaybe<Scalars['Float']['input']>;
sort?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<NewsFilter>;
}>;
export type NewsConnectionQuery = { __typename?: 'Query', newsConnection: { __typename?: 'NewsConnection', totalCount: number, pageInfo: { __typename?: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string, endCursor: string }, edges?: Array<{ __typename?: 'NewsConnectionEdges', cursor: string, node?: { __typename: 'News', id: string, locale?: string | null, title: string, date: string, slug?: string | null, place?: string | null, country?: string | null, city?: string | null, thumbnail?: string | null, thumbnail_square?: string | null, tags?: Array<string | null> | null, youtube?: string | null, draft?: boolean | null, body?: any | null, _sys: { __typename?: 'SystemInfo', filename: string, basename: string, hasReferences?: boolean | null, breadcrumbs: Array<string>, path: string, relativePath: string, extension: string }, gallery?: Array<{ __typename: 'NewsGallery', image?: string | null } | null> | null } | null } | null> | null } };
export type EditorialQueryVariables = Exact<{
relativePath: Scalars['String']['input'];
}>;
export type EditorialQuery = { __typename?: 'Query', editorial: { __typename: 'Editorial', id: string, locale?: string | null, title: string, date: string, slug?: string | null, place?: string | null, country?: string | null, city?: string | null, thumbnail?: string | null, thumbnail_square?: string | null, tags?: Array<string | null> | null, youtube?: string | null, draft?: boolean | null, body?: any | null, _sys: { __typename?: 'SystemInfo', filename: string, basename: string, hasReferences?: boolean | null, breadcrumbs: Array<string>, path: string, relativePath: string, extension: string }, gallery?: Array<{ __typename: 'EditorialGallery', image?: string | null } | null> | null } };
export type EditorialConnectionQueryVariables = Exact<{
before?: InputMaybe<Scalars['String']['input']>;
after?: InputMaybe<Scalars['String']['input']>;
first?: InputMaybe<Scalars['Float']['input']>;
last?: InputMaybe<Scalars['Float']['input']>;
sort?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<EditorialFilter>;
}>;
export type EditorialConnectionQuery = { __typename?: 'Query', editorialConnection: { __typename?: 'EditorialConnection', totalCount: number, pageInfo: { __typename?: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string, endCursor: string }, edges?: Array<{ __typename?: 'EditorialConnectionEdges', cursor: string, node?: { __typename: 'Editorial', id: string, locale?: string | null, title: string, date: string, slug?: string | null, place?: string | null, country?: string | null, city?: string | null, thumbnail?: string | null, thumbnail_square?: string | null, tags?: Array<string | null> | null, youtube?: string | null, draft?: boolean | null, body?: any | null, _sys: { __typename?: 'SystemInfo', filename: string, basename: string, hasReferences?: boolean | null, breadcrumbs: Array<string>, path: string, relativePath: string, extension: string }, gallery?: Array<{ __typename: 'EditorialGallery', image?: string | null } | null> | null } | null } | null> | null } };
export const NewsPartsFragmentDoc = gql`
fragment NewsParts on News {
__typename
locale
title
date
slug
place
country
city
thumbnail
thumbnail_square
tags
gallery {
__typename
image
}
youtube
draft
body
}
`;
export const EditorialPartsFragmentDoc = gql`
fragment EditorialParts on Editorial {
__typename
locale
title
date
slug
place
country
city
thumbnail
thumbnail_square
tags
gallery {
__typename
image
}
youtube
draft
body
}
`;
export const NewsDocument = gql`
query news($relativePath: String!) {
news(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...NewsParts
}
}
${NewsPartsFragmentDoc}`;
export const NewsConnectionDocument = gql`
query newsConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: NewsFilter) {
newsConnection(
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
}
...NewsParts
}
}
}
}
${NewsPartsFragmentDoc}`;
export const EditorialDocument = gql`
query editorial($relativePath: String!) {
editorial(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
...EditorialParts
}
}
${EditorialPartsFragmentDoc}`;
export const EditorialConnectionDocument = gql`
query editorialConnection($before: String, $after: String, $first: Float, $last: Float, $sort: String, $filter: EditorialFilter) {
editorialConnection(
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
}
...EditorialParts
}
}
}
}
${EditorialPartsFragmentDoc}`;
export type Requester<C= {}> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<R>
export function getSdk<C>(requester: Requester<C>) {
return {
news(variables: NewsQueryVariables, options?: C): Promise<{data: NewsQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: NewsQueryVariables, query: string}> {
return requester<{data: NewsQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: NewsQueryVariables, query: string}, NewsQueryVariables>(NewsDocument, variables, options);
},
newsConnection(variables?: NewsConnectionQueryVariables, options?: C): Promise<{data: NewsConnectionQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: NewsConnectionQueryVariables, query: string}> {
return requester<{data: NewsConnectionQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: NewsConnectionQueryVariables, query: string}, NewsConnectionQueryVariables>(NewsConnectionDocument, variables, options);
},
editorial(variables: EditorialQueryVariables, options?: C): Promise<{data: EditorialQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: EditorialQueryVariables, query: string}> {
return requester<{data: EditorialQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: EditorialQueryVariables, query: string}, EditorialQueryVariables>(EditorialDocument, variables, options);
},
editorialConnection(variables?: EditorialConnectionQueryVariables, options?: C): Promise<{data: EditorialConnectionQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: EditorialConnectionQueryVariables, query: string}> {
return requester<{data: EditorialConnectionQuery, errors?: { message: string, locations: { line: number, column: number }[], path: string[] }[], variables: EditorialConnectionQueryVariables, query: string}, EditorialConnectionQueryVariables>(EditorialConnectionDocument, variables, options);
}
};
}
export type Sdk = ReturnType<typeof getSdk>;
// TinaSDK generated code
import { createClient, TinaClient } from "tinacms/dist/client";
const generateRequester = (
client: TinaClient,
) => {
const requester: (
doc: any,
vars?: any,
options?: {
branch?: string,
/**
* Aside from `method` and `body`, all fetch options are passed
* through to underlying fetch request
*/
fetchOptions?: Omit<Parameters<typeof fetch>[1], 'body' | 'method'>,
},
client
) => Promise<any> = async (doc, vars, options) => {
let url = client.apiUrl
if (options?.branch) {
const index = client.apiUrl.lastIndexOf('/')
url = client.apiUrl.substring(0, index + 1) + options.branch
}
const data = await client.request({
query: doc,
variables: vars,
url,
}, options)
return { data: data?.data, errors: data?.errors, query: doc, variables: vars || {} }
}
return requester
}
/**
* @experimental this class can be used but may change in the future
**/
export const ExperimentalGetTinaClient = () =>
getSdk(
generateRequester(
createClient({
url: "http://localhost:4001/graphql",
queries,
})
)
)
export const queries = (
client: TinaClient,
) => {
const requester = generateRequester(client)
return getSdk(requester)
}

9
tina/auth.ts Normal file
View File

@ -0,0 +1,9 @@
import { TinaAuth } from "tinacms";
const branch = process.env.HEAD || process.env.VERCEL_GIT_COMMIT_REF || "main";
export const auth = TinaAuth({
branch,
clientId: null,
token: null,
});

241
tina/config.ts Normal file
View File

@ -0,0 +1,241 @@
import { defineConfig } from "tinacms";
const branch = process.env.HEAD || process.env.VERCEL_GIT_COMMIT_REF || "main";
export default defineConfig({
branch,
clientId: null,
token: null,
build: {
outputFolder: "admin",
publicFolder: "public",
},
media: {
tina: {
mediaRoot: "public/images",
publicFolder: "public",
},
},
schema: {
collections: [
{
name: "news",
label: "News",
path: "src/content/news",
format: "md",
match: {
include: "**/*",
},
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.slug?.toLowerCase() || values?.title?.toLowerCase().replace(/ /g, '-');
},
},
},
fields: [
{
type: "string",
name: "locale",
label: "Language",
options: [
{ label: "Español", value: "es" },
{ label: "English", value: "en" },
{ label: "Português", value: "pt" },
{ label: "Kinyarwanda", value: "rw" },
{ label: "Frances", value: "fr" }
],
},
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true,
},
{
type: "datetime",
name: "date",
label: "Date",
required: true,
},
{
type: "string",
name: "slug",
label: "Slug",
},
{
type: "string",
name: "place",
label: "Place",
},
{
type: "string",
name: "country",
label: "Country",
},
{
type: "string",
name: "city",
label: "City",
},
{
type: "image",
name: "thumbnail",
label: "Thumbnail URL",
},
{
type: "image",
name: "thumbnail_square",
label: "Thumbnail Square URL",
},
{
type: "string",
name: "tags",
label: "Tags",
list: true,
},
{
type: "object",
name: "gallery",
label: "Gallery",
list: true,
fields: [
{
type: "image",
name: "image",
label: "Image",
},
],
},
{
type: "string",
name: "youtube",
label: "YouTube ID",
},
{
type: "boolean",
name: "draft",
label: "Draft",
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true,
},
],
},
{
name: "editorial",
label: "Editorial",
path: "src/content/editorial",
format: "md",
match: {
include: "**/*",
},
ui: {
filename: {
readonly: false,
slugify: (values) => {
return values?.slug?.toLowerCase() || values?.title?.toLowerCase().replace(/ /g, '-');
},
},
},
fields: [
{
type: "string",
name: "locale",
label: "Language",
options: [
{ label: "Español", value: "es" },
{ label: "English", value: "en" },
{ label: "Português", value: "pt" },
{ label: "Kinyarwanda", value: "rw" },
{ label: "Frances", value: "fr" }
],
},
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true,
},
{
type: "datetime",
name: "date",
label: "Date",
required: true,
},
{
type: "string",
name: "slug",
label: "Slug",
},
{
type: "string",
name: "place",
label: "Place",
},
{
type: "string",
name: "country",
label: "Country",
},
{
type: "string",
name: "city",
label: "City",
},
{
type: "image",
name: "thumbnail",
label: "Thumbnail URL",
},
{
type: "image",
name: "thumbnail_square",
label: "Thumbnail Square URL",
},
{
type: "string",
name: "tags",
label: "Tags",
list: true,
},
{
type: "object",
name: "gallery",
label: "Gallery",
list: true,
fields: [
{
type: "image",
name: "image",
label: "Image",
},
],
},
{
type: "string",
name: "youtube",
label: "YouTube ID",
},
{
type: "boolean",
name: "draft",
label: "Draft",
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true,
},
],
},
],
},
});

1
tina/tina-lock.json Normal file

File diff suppressed because one or more lines are too long