Compare commits

..

No commits in common. "main" and "identitySection" have entirely different histories.

504 changed files with 7143 additions and 39851 deletions

View File

@ -1,23 +0,0 @@
# Cloudflare Turnstile — public key (visible in frontend)
TURNSTILE_SITE_KEY=0x4AA...
# Cloudflare Turnstile — secret key (server-side, keep safe!)
# In production, set this as a Gitea Actions secret named TURNSTILE_SECRET_KEY
TURNSTILE_SECRET_KEY=0x4AA...
# Supabase PostgreSQL
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public"
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public"
SUPABASE_URL="http://localhost:8000"
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
POSTGRES_PASSWORD=postgres
# Admin auth — API key (para consumo externo de /api/admin/*)
ADMIN_API_KEY="CHANGE_ME_admin_api_key"
# Admin auth — credenciales del seed (solo se usan en scripts/seed-admin.ts)
ADMIN_EMAIL="admin@example.com"
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="CHANGE_ME_password"
ADMIN_NOMBRE="Admin"

View File

@ -1,72 +0,0 @@
name: Deploy cdrdpyj
on:
push:
branches: [staging, production]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set target dir & PM2 env
run: |
if [ "${{ gitea.ref_name }}" = "production" ]; then
echo "TARGET_DIR=/var/www/node/cdrdpyj" >> $GITHUB_ENV
echo "APP_NAME=cdrdpyj-live" >> $GITHUB_ENV
else
echo "TARGET_DIR=/var/www/node/dev.cdrdjyp" >> $GITHUB_ENV
echo "APP_NAME=cdrdpyj" >> $GITHUB_ENV
fi
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Copy build to VPS
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
source: "dist/*,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,ecosystem.config.cjs,prisma/schema.prisma,prisma/migrations,prisma.config.ts"
target: "${{ env.TARGET_DIR }}"
- name: Restart PM2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
set -e
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
npm install -g pnpm@11 2>&1
cd ${{ env.TARGET_DIR }}
cat > .env <<'ENVEOF'
DATABASE_URL="${{ secrets.DATABASE_URL }}"
DIRECT_URL="${{ secrets.DIRECT_URL }}"
GOOGLE_SERVICE_ACCOUNT_EMAIL="${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}"
GOOGLE_PRIVATE_KEY="${{ secrets.GOOGLE_PRIVATE_KEY }}"
GOOGLE_SHEET_ID="${{ secrets.GOOGLE_SHEET_ID }}"
EMAIL_API_KEY="${{ secrets.EMAIL_API_KEY }}"
TURNSTILE_SITE_KEY="${{ secrets.TURNSTILE_SITE_KEY }}"
TURNSTILE_SECRET_KEY="${{ secrets.TURNSTILE_SECRET_KEY }}"
CLOUDFLARE_API_TOKEN="${{ secrets.CLOUDFLARE_API_TOKEN }}"
CLOUDFLARE_ACCOUNT_ID="${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"
ADMIN_API_KEY="${{ secrets.ADMIN_API_KEY }}"
ENVEOF
pnpm install --prod 2>&1
pnpm exec prisma generate 2>&1
pnpm exec prisma migrate deploy 2>&1
pm2 reload ecosystem.config.cjs --only ${{ env.APP_NAME }} --update-env || \
pm2 start ecosystem.config.cjs --only ${{ env.APP_NAME }} --update-env

21
.gitignore vendored
View File

@ -15,29 +15,10 @@ pnpm-debug.log*
# environment variables
.env
.env.keys
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
/generated/prisma
prisma/*.db
prisma/dev.db
data/
# opencode - agentes y scripts locales (no subir a producción)
opencode/
.opencode/
docs/
# guía técnica local
docs/GUIA-TECNICA.md
# scripts de migración (locales, no en producción)
scripts/migrate-sheets-to-db.ts
scripts/seed-form-config.ts
scripts/
.env.example

View File

@ -1,186 +0,0 @@
---
name: dynamic-form
description: Use when working with the volunteer registration dynamic multi-step form. Relevant files: DynamicForm.vue, formulario-inscripcion.json, [section]/index.astro, i18n system, reglamento HTML.
---
# Dynamic Multi-Step Volunteer Registration Form
## Stack
- **Framework:** Astro (SSR, `output: "server"`, Node.js standalone)
- **UI:** Vue 3 + Tailwind CSS v4 + daisyUI v5
- **i18n:** Astro built-in i18n with `prefixDefaultLocale: true`, custom translator via `createTranslator(locale)`
## Architecture
### Form Config (JSON-driven)
The form structure is defined in `public/forms/formulario-inscripcion.json`:
```json
{
"action": "/api/formulario/send",
"submit_label": "form.submit",
"steps": [
{
"label": "form.step1_reglamento",
"fields": [ ... ]
}
]
}
```
Each field supports:
- `type`: `text`, `email`, `phone`, `date`, `textarea`, `radio`, `checkbox`, `select`, `autocomplete`
- `required`: boolean (for checkbox, individual options can also have `required: true`)
- `colspan`: `2` or `3` for grid layout when step has `column: 2` or `column: 3`
- `readonly`: displays value from `formData` (shared across steps). Supports `valueFrom` to reference another field's key.
- `showWhen`: `{ field: "other_key", value: "expected_value" }` — conditional display; for checkboxes checks array inclusion
- `levels`: proficiency radios for each selected checkbox option (e.g., Básico/Intermedio/Avanzado)
- `placeholder`: translation key for placeholder text
- `source`: URL to JSON file for `select`/`autocomplete` options (e.g., `/forms/paises.json`)
### Key Files
| File | Purpose |
|------|---------|
| `src/components/forms/DynamicForm.vue` | Core form component (751 lines) |
| `public/forms/formulario-inscripcion.json` | Form structure config (8 steps) |
| `public/forms/paises.json` | Country list for `select`/`autocomplete` |
| `src/pages/[locale]/[section]/index.astro` | Routes news, editorial, AND formulario |
| `src/i18n/index.ts` | Translator, `routeTranslations`, `I18nKey` type |
| `src/i18n/es.json` | Spanish translations (all form keys) |
| `public/reglamento/es.html` | Reglamento content (section-based HTML) |
### Field Types
| Type | Rendering | Behavior |
|------|-----------|----------|
| `text` | `<input>` inside daisyUI `<label class="input">` | Phosphor icon via `getFieldIcon()` |
| `email` | Same as text, `type="email"` | Regex validation on non-empty |
| `phone` | Same as text, `type="tel"` | Regex `/^\d+$/` — strips non-digit chars on input |
| `phone_country` | daisyUI `join` with autocomplete input (left) + `<input type="tel">` (right) | Input filters countries by name or code as you type. Dropdown shows only `🇨🇴 +57`. On select, stores `dial_code` in `phoneParts[key].code` and combines with number in `formData[key]`. Selected display shows `🇨🇴 +57`. |
| `date` | `<input type="date">` | Calendar icon |
| `textarea` | `<textarea>` with absolute icon | 4 rows, `resize-none` |
| `radio` | Inline `<input type="radio">` | `flex flex-wrap gap-x-5 gap-y-1` |
| `checkbox` | Stacked checkboxes with optional `levels` radios | Array in `formData` |
| `select` | `<select>` populated from `field.source` JSON | `getSelectOptions(field)` loads & caches |
| `autocomplete` | `<input>` with dropdown suggestions | `filteredSuggestions()` filters by input |
### DynamicForm.vue Features
- **Multi-step navigation:** stepper with numbered circles + step labels, clickable to revisit completed steps. Gold (`#CBA16A`) for active, cream (`#EBE6D2`) with checkmark for completed.
- **Validation:** per-step via `validateStep()`; checks required fields (with trim), email format, checkbox required options, level selection. Errors show `ph:warning-circle` icon + "Este campo es obligatorio".
- **Step counter:** "Paso X de Y" centered above buttons on mobile, inline with "Siguiente" on desktop. Divider line separates fields from navigation.
- **Reglamento scroll requirement:** fetches HTML from `/reglamento/{locale}.html` with fallback to `es`, renders in `max-h-[500px] md:max-h-[720px] overflow-y-auto`. User must scroll to bottom (`scrollTop + clientHeight >= scrollHeight - 10`) before checkboxes (step 1) and "Siguiente" button become enabled. Info banner + tooltip shown until requirement met.
- **Conditional fields:** `shouldShow()` evaluates `showWhen`; for checkboxes source, checks array inclusion; for others, strict equality.
- **Levels system:** when checkbox option is selected, level radios appear inline (`ml-9 mt-1`). Levels also validated.
- **Readonly fields:** display data from other steps via `valueFrom` cross-reference.
- **Select/autocomplete:** lazy-loads options from `field.source` URL, cached in `suggestionsCache`. Autocomplete filters by input value, shows dropdown on focus/input.
- **Icons:** Phosphor icons mapped via `getFieldIcon()` by field key or type.
- **Submit:** POSTs `{ formData, responses, turnstileToken, honeypot }` to config's `action` URL (`/api/formulario/send`). Success screen shows logo (`/img/logo-metalico.webp`), success title, info detail, and "Volver al inicio" link. Handles loading/spinner state during submit.
- **Bot protection:** Cloudflare Turnstile widget renders on the last step. `turnstileToken` ref updated via callback. Honeypot hidden field (`name="website"`, `class="hidden"`, `tabindex="-1"`) traps basic bots. Both values included in payload.
- **Payload shape:** `responses` array with `{ key, value, section_id: "step_N" }`. For levels, each level is a separate response entry. Does NOT include empty fields. Extra fields: `turnstileToken` (string), `honeypot` (string).
- **Mount reset:** `isSuccess.value = false` on fetch to prevent stale success state on re-navigation.
### Validation Logic (validateStep)
- **Checkbox required:** if individual options have `required: true`, all must be selected; if no option is individually required but field is `required`, at least 1 selection is needed
- **Text/email/phone/date/textarea/radio required:** checks `!val || val.trim() === ""`
- **Phone format:** validates `/^\d+$/` when non-empty — `formatErrors` tracks format issues separately from `stepErrors`; shows "Solo se permiten números" vs "Este campo es obligatorio"
- **Phone country (`phone_country`):** two-piece component (select + input). `phoneParts` reactive tracks code and number separately; combined value stored in `formData[key]`. Input sanitizes non-digits via `event.target.value = cleaned` + `phoneParts` tracking. Validates: required (code + number filled), min 7 digits, max 15 total (code + number).
### i18n System
- `createTranslator(locale)` returns `tl(key, vars?)` function — supports `{name}` interpolation (used in `form.select_placeholder`)
- `I18nKey = keyof typeof es` — all keys must exist in `es.json`
- Fallback chain: requested locale → `es` → raw key string
- `routeTranslations` now includes `formulario` key (was missing initially):
```ts
formulario: {
es: "formulario", en: "form", fr: "formulaire",
he: "טופס", uk: "форма", pt: "formulário",
ru: "форма", rw: "formulaire", kr: "fòm"
}
```
- `getRouteKeyFromSlug(slug)`: resolves URL segment to route key (returns `"news"` default for unknown)
- `getLocalizedRoute(route, locale)`: builds locale-aware URL segment
### Routing
The `[section]/index.astro` page handles three route keys:
- `"formulario"` + locale `es` → renders DynamicForm with MainLayout
- `"formulario"` + other locale → `Astro.redirect` to that locale's home page
- `"news"` / `"editorial"` → existing content list behavior
Form URLs:
- `/es/formulario` (active)
- `/en/form`, `/fr/formulaire`, etc. (redirect to home)
### Color Palette
| Token | Hex | Usage |
|-------|-----|-------|
| Primary green | `#22523F` | Headings, stepper active, buttons, reglamento headings, focus outlines, hover backgrounds |
| Cream | `#EBE6D2` | Form background, stepper completed circles |
| Gold | `#CBA16A` | Active step circles |
| Input text | `#1a1a1a` | Input/textarea values, checkbox/radio labels |
| Readonly text | `#6B7280` | Readonly field values |
| Checked accent | `#4A8C6F` | Checkbox/radio checked background and border |
### Responsive Behavior
| Element | Mobile (< sm) | Desktop (sm+) |
|---------|---------------|---------------|
| Container | `max-w-3xl px-4` | `max-w-3xl mx-auto` |
| Form padding | `p-5` | `sm:p-8 lg:p-10` |
| Grid gap | `gap-4` | `sm:gap-5` |
| Title size | `text-lg` | `sm:text-xl` |
| Button font | `text-xs` | `sm:text-sm` |
| Step counter | Centered above buttons | Next to "Siguiente" button |
| Nav layout | Column (counter, then row of buttons) | Row with `justify-between` via `sm:contents` |
| Reglamento height | `max-h-[500px]` | `md:max-h-[720px]` |
### API Endpoint: `POST /api/formulario/send`
| File | Purpose |
|------|---------|
| `src/pages/api/formulario/send.ts` | Receives form data, validates Turnstile, writes to Google Sheets |
**Security checks (in order):**
1. **Honeypot** — if `body.honeypot` is non-empty → `403 Solicitud rechazada`
2. **Turnstile** — verifies `body.turnstileToken` against Cloudflare API → `403 Captcha inválido` if fail
3. If `TURNSTILE_SECRET_KEY` env var is empty/unset, Turnstile check is skipped (dev mode)
**Env vars needed at runtime:**
- `TURNSTILE_SITE_KEY` — public key (in `ecosystem.config.cjs`, injected via `sed` in CI)
- `TURNSTILE_SECRET_KEY` — secret key (injected from `secrets.TURNSTILE_SECRET_KEY` in CI)
### Reglamento HTML
Stored in `public/reglamento/{locale}.html`. Must NOT contain `<html>`, `<head>`, `<body>`, or `<style>` tags — only `<section>`, `<h1>`, `<h2>`, `<p>`, `<ul>`, `<li>` elements. Loaded via `fetch()` and injected with `v-html` inside a `.reglamento-content` div with scoped CSS styles in the component's `<style>` block.
## Important Conventions
- NEVER run `pnpm run build` as a verification step — the user will handle builds manually or via CI.
- Translation keys in `es.json` use dot notation: `"form.field_name": "Label"`
- Checkbox `formData` is initialized as `[]`, other fields as `""`
- Step labels: `form.step1_reglamento`, `form.step2``form.step8`
- The `RegisterModal` on the home page was replaced with a direct link to `/{locale}/{formSlug}`
- All i18n keys used in DynamicForm.vue must exist in `es.json` (strict `I18nKey` typing)
- Input icons are mapped via `getFieldIcon()` by key or type — add new entries when adding fields (uses `ph:` prefix Phosphor icons)
- `valueFrom` on `readonly` fields allows displaying cross-step data (e.g., show `nombre` from step 2 on step 8)
- `select` and `autocomplete` fields require a `source` URL pointing to a JSON array of `{ label, value }` objects
- `suggestionsCache` is a shared ref — data loads once per `source` URL and is reused across all fields pointing to the same source
- Reglamento is fetched via locale fallback chain: `props.locale``"es"` — only fetched once per step 0 visit via `reglamentoFetchAttempted` guard
- Turnstile widget renders only on the last step (`isLastStep`) and only if `props.turnstileSiteKey` is non-empty. The token is obtained via `window.turnstile.render()` callback. Script loads from `https://challenges.cloudflare.com/turnstile/v0/api.js` on `onMounted`.
- Honeypot field (`name="website"`) is hidden via daisyUI `hidden` class and `tabindex="-1"` — browsers/autofill may still fill it but human users never see it.
## Language Style
- All Spanish text must use **international Spanish (neutral/formal)**, NOT Argentine voseo.
- Use **usted** forms: "Seleccione", "Indique", "Complete", "Ingrese", "Inténtelo", "Desplácese", "Debe leer", etc.
- Use **su** instead of **tu**: "su país", "su interés", "sus datos".
- Exception: the `formulario-inscripcion.json` field labels and option labels can keep their existing text — only new text generated for `_help` tooltips or translations must follow this rule.

View File

@ -1,28 +0,0 @@
{
"allowEmptyTranslations": false,
"defaultLanguage": "en",
"forceKeyUPPERCASE": true,
"jsonSpace": 2,
"keySeparator": ".",
"lineEnding": "\n",
"supportedFolders": [
"i18n"
],
"workspaceFolders": [
{
"name": "src",
"path": "src/i18n",
"visibleColumns": [
"es",
"fr",
"he",
"pt",
"uk"
],
"hiddenColumns": []
}
],
"defaultWorkspaceFolder": "src",
"translationService": "Coming soon",
"translationServiceApiKey": "Coming soon"
}

View File

@ -41,7 +41,3 @@ All commands are run from the root of the project, from a terminal:
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
Adding gitea/workflows/deploy.yaml to test github ci/cd runner
Testing runner change spmeting v12

View File

@ -4,36 +4,22 @@ import tailwindcss from "@tailwindcss/vite";
import { imageService } from "@unpic/astro/service";
import markdoc from "@astrojs/markdoc";
import icon from "astro-icon";
import node from "@astrojs/node";
import vue from "@astrojs/vue";
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
site: "https://centrodelreinodepazyjusticia.com/",
//base: '/mockup/',
integrations: [markdoc(), icon(), vue(), react()],
integrations: [markdoc(), icon()],
i18n: {
locales: ["es", "en", "fr", "he", "uk", "pt", "ru", "rw", "kr"],
locales: ["es", "en", "fr", "he", "uk", "pt-br"],
defaultLocale: "es",
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: true,
},
},
image: {
domains: ['placehold.co', 'ik.imagekit.io', 'picsum.photos'],
domains: ['placehold.co','ik.imagekit.io','picsum.photos'],
service: imageService(),
},
output: "server",
adapter: node({
mode: "standalone",
}),
});

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
module.exports = {
apps: [
{
name: "cdrdpyj",
cwd: __dirname,
script: "dist/server/entry.mjs",
interpreter_args: "--env-file=.env",
env: {
NODE_ENV: "staging",
PORT: 3310,
}
},
{
name: "cdrdpyj-live",
cwd: __dirname,
script: "dist/server/entry.mjs",
interpreter_args: "--env-file=.env",
env: {
NODE_ENV: "production",
PORT: 4321,
}
}
]
};

6416
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,25 @@
{
"name": "cdrdpyj",
"name": "",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"postbuild": "node scripts/send-to-n8n.js",
"preview": "astro preview",
"astro": "astro",
"db:migrate-sheets": "npx tsx scripts/migrate-sheets-to-db.ts",
"db:seed-form-config": "npx tsx scripts/seed-form-config.ts",
"db:setup": "npx tsx scripts/seed-form-config.ts && npx tsx scripts/migrate-sheets-to-db.ts"
"astro": "astro"
},
"dependencies": {
"@astrojs/markdoc": "^2.0.3",
"@astrojs/node": "^11.0.2",
"@astrojs/react": "^6.0.1",
"@astrojs/vue": "^7.0.1",
"@coreui/icons": "^3.0.1",
"@dotenvx/dotenvx": "^1.52.0",
"@fontsource-variable/kameron": "^5.2.8",
"@fontsource-variable/kreon": "^5.2.8",
"@fontsource-variable/rokkitt": "^5.2.8",
"@fontsource/poppins": "^5.2.7",
"@astrojs/markdoc": "^0.15.10",
"@iconify-json/ph": "^1.2.2",
"@iconify/vue": "^5.0.0",
"@prisma/client": "^6.19.2",
"@tailwindcss/vite": "^4.1.18",
"@unpic/astro": "^1.0.2",
"astro": "^7.0.6",
"astro-embed": "^0.12.0",
"astro-google-analytics": "^1.0.3",
"astro": "^5.17.1",
"astro-icon": "^1.1.5",
"cloudflare": "^7.0.0",
"dayjs": "^1.11.19",
"googleapis": "^171.4.0",
"prisma": "^6.19.2",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"sharp": "^0.34.5",
"swiper": "^12.1.0",
"tailwindcss": "^4.1.18",
"vue": "^3.5.28"
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"daisyui": "^5.5.18",
"dotenv": "^17.4.2"
"@tailwindcss/typography": "^0.5.19"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
allowBuilds:
'@prisma/client': true
'@prisma/engines': true
better-sqlite3: true
core-js: true
esbuild: true
prisma: true
protobufjs: true
sharp: true

View File

@ -1,19 +0,0 @@
// This file was generated by Prisma and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
engine: "classic",
datasource: {
url:
process.env.DIRECT_URL ||
process.env.DATABASE_URL ||
"postgresql://localhost:5432/postgres?schema=public",
},
});

View File

@ -1,8 +0,0 @@
-- CreateTable
CREATE TABLE "Contact" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"nombre" TEXT NOT NULL,
"email" TEXT NOT NULL,
"mensaje" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -1,2 +0,0 @@
-- CreateIndex
CREATE INDEX "Contact_email_createdAt_idx" ON "Contact"("email", "createdAt");

View File

@ -1,24 +0,0 @@
/*
Warnings:
- Added the required column `updatedAt` to the `Contact` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Contact" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"nombre" TEXT NOT NULL,
"email" TEXT NOT NULL,
"mensaje" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Contact" ("createdAt", "email", "id", "mensaje", "nombre") SELECT "createdAt", "email", "id", "mensaje", "nombre" FROM "Contact";
DROP TABLE "Contact";
ALTER TABLE "new_Contact" RENAME TO "Contact";
CREATE UNIQUE INDEX "Contact_email_key" ON "Contact"("email");
CREATE INDEX "Contact_email_createdAt_idx" ON "Contact"("email", "createdAt");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "Contact" (
"id" SERIAL NOT NULL,
"nombre" TEXT NOT NULL,
"email" TEXT NOT NULL,
"mensaje" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Contact_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Contact_email_key" ON "Contact"("email");
-- CreateIndex
CREATE INDEX "Contact_email_createdAt_idx" ON "Contact"("email", "createdAt");

View File

@ -1,174 +0,0 @@
/*
Warnings:
- You are about to drop the `Contact` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "Contact";
-- CreateTable
CREATE TABLE "contact" (
"id" SERIAL NOT NULL,
"nombre" TEXT NOT NULL,
"email" TEXT NOT NULL,
"mensaje" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "contact_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "formularios" (
"id" UUID NOT NULL,
"numero_voluntario" SERIAL NOT NULL,
"nombre" TEXT NOT NULL,
"segundo_nombre" TEXT,
"apellido" TEXT NOT NULL,
"documento_nro" TEXT,
"documento_tipo" TEXT,
"correo" TEXT NOT NULL,
"fecha_nacimiento" DATE,
"nacionalidad" TEXT,
"sexo" TEXT,
"direccion" TEXT,
"ciudad" TEXT,
"estado" TEXT,
"pais" TEXT,
"codigo_postal" TEXT,
"telefono" TEXT,
"whatsapp" TEXT,
"profesion" TEXT,
"lugar_trabajo" TEXT,
"nivel_academico" TEXT,
"status" TEXT NOT NULL DEFAULT 'pendiente',
"form_version" TEXT,
"email_sent_at" TIMESTAMPTZ,
"submitted_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "formularios_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "respuestas" (
"id" UUID NOT NULL,
"formulario_id" UUID NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"seccion" TEXT NOT NULL DEFAULT '',
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "respuestas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "secciones" (
"id" UUID NOT NULL,
"title_key" TEXT NOT NULL,
"title" TEXT,
"orden" INTEGER NOT NULL DEFAULT 0,
"columns" INTEGER NOT NULL DEFAULT 1,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "secciones_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "campos" (
"id" UUID NOT NULL,
"key" TEXT NOT NULL,
"label_key" TEXT NOT NULL,
"tipo" TEXT NOT NULL,
"seccion_id" UUID,
"required" BOOLEAN NOT NULL DEFAULT false,
"orden" INTEGER NOT NULL DEFAULT 0,
"options" JSONB,
"source" TEXT,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "campos_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "form_versions" (
"id" UUID NOT NULL,
"version" TEXT NOT NULL,
"config" JSONB NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "form_versions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "admins" (
"id" UUID NOT NULL,
"auth_user_id" UUID NOT NULL,
"email" TEXT NOT NULL,
"nombre" TEXT NOT NULL,
"rol" TEXT NOT NULL DEFAULT 'coordinador',
"activo" BOOLEAN NOT NULL DEFAULT true,
"ultimo_acceso" TIMESTAMPTZ,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admins_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "contact_email_key" ON "contact"("email");
-- CreateIndex
CREATE INDEX "contact_email_createdAt_idx" ON "contact"("email", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "formularios_numero_voluntario_key" ON "formularios"("numero_voluntario");
-- CreateIndex
CREATE INDEX "formularios_correo_idx" ON "formularios"("correo");
-- CreateIndex
CREATE INDEX "formularios_status_idx" ON "formularios"("status");
-- CreateIndex
CREATE INDEX "formularios_pais_idx" ON "formularios"("pais");
-- CreateIndex
CREATE INDEX "formularios_submitted_at_idx" ON "formularios"("submitted_at");
-- CreateIndex
CREATE INDEX "respuestas_formulario_id_idx" ON "respuestas"("formulario_id");
-- CreateIndex
CREATE INDEX "respuestas_key_idx" ON "respuestas"("key");
-- CreateIndex
CREATE INDEX "respuestas_seccion_idx" ON "respuestas"("seccion");
-- CreateIndex
CREATE INDEX "secciones_orden_idx" ON "secciones"("orden");
-- CreateIndex
CREATE UNIQUE INDEX "campos_key_key" ON "campos"("key");
-- CreateIndex
CREATE INDEX "campos_seccion_id_idx" ON "campos"("seccion_id");
-- CreateIndex
CREATE INDEX "campos_tipo_idx" ON "campos"("tipo");
-- CreateIndex
CREATE UNIQUE INDEX "admins_auth_user_id_key" ON "admins"("auth_user_id");
-- CreateIndex
CREATE UNIQUE INDEX "admins_email_key" ON "admins"("email");
-- CreateIndex
CREATE INDEX "admins_email_idx" ON "admins"("email");
-- AddForeignKey
ALTER TABLE "respuestas" ADD CONSTRAINT "respuestas_formulario_id_fkey" FOREIGN KEY ("formulario_id") REFERENCES "formularios"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "campos" ADD CONSTRAINT "campos_seccion_id_fkey" FOREIGN KEY ("seccion_id") REFERENCES "secciones"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,23 +0,0 @@
-- Enable pg_trgm for trigram-based GIN indexes
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- CreateIndex
CREATE INDEX "formularios_nombre_trgm_idx" ON "formularios" USING GIN ("nombre" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_apellido_trgm_idx" ON "formularios" USING GIN ("apellido" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_correo_trgm_idx" ON "formularios" USING GIN ("correo" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_telefono_trgm_idx" ON "formularios" USING GIN ("telefono" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_whatsapp_trgm_idx" ON "formularios" USING GIN ("whatsapp" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_documento_nro_trgm_idx" ON "formularios" USING GIN ("documento_nro" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "respuestas_value_trgm_idx" ON "respuestas" USING GIN ("value" gin_trgm_ops);

View File

@ -1,14 +0,0 @@
-- CreateIndex
CREATE INDEX "formularios_ciudad_trgm_idx" ON "formularios" USING GIN ("ciudad" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_estado_trgm_idx" ON "formularios" USING GIN ("estado" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_profesion_trgm_idx" ON "formularios" USING GIN ("profesion" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_nivel_academico_trgm_idx" ON "formularios" USING GIN ("nivel_academico" gin_trgm_ops);
-- CreateIndex
CREATE INDEX "formularios_nacionalidad_trgm_idx" ON "formularios" USING GIN ("nacionalidad" gin_trgm_ops);

View File

@ -1,25 +0,0 @@
-- AlterTable
ALTER TABLE "admins" ALTER COLUMN "auth_user_id" DROP NOT NULL;
-- AlterTable
ALTER TABLE "admins" ADD COLUMN "password_hash" TEXT;
-- CreateTable
CREATE TABLE "sessions" (
"id" UUID NOT NULL,
"token_hash" TEXT NOT NULL,
"admin_id" UUID NOT NULL,
"expires_at" TIMESTAMPTZ NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "sessions_token_hash_key" ON "sessions"("token_hash");
-- CreateIndex
CREATE INDEX "sessions_admin_id_idx" ON "sessions"("admin_id");
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "admins" ADD COLUMN "username" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "admins_username_key" ON "admins"("username");

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -1,160 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
model Contact {
id Int @id @default(autoincrement())
nombre String
email String @unique
mensaje String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email, createdAt])
@@map("contact")
}
model Formulario {
id String @id @default(uuid()) @db.Uuid
numero_voluntario Int @unique @default(autoincrement())
nombre String
segundo_nombre String?
apellido String
documento_nro String?
documento_tipo String?
correo String
fecha_nacimiento DateTime? @db.Date
nacionalidad String?
sexo String?
direccion String?
ciudad String?
estado String?
pais String?
codigo_postal String?
telefono String?
whatsapp String?
profesion String?
lugar_trabajo String?
nivel_academico String?
status String @default("pendiente")
form_version String?
email_sent_at DateTime? @db.Timestamptz
submitted_at DateTime @default(now()) @db.Timestamptz
updated_at DateTime @updatedAt @db.Timestamptz
respuestas Respuesta[]
@@index([correo])
@@index([status])
@@index([pais])
@@index([submitted_at])
@@index([nombre(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_nombre_trgm_idx")
@@index([apellido(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_apellido_trgm_idx")
@@index([correo(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_correo_trgm_idx")
@@index([telefono(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_telefono_trgm_idx")
@@index([whatsapp(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_whatsapp_trgm_idx")
@@index([documento_nro(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_documento_nro_trgm_idx")
@@index([ciudad(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_ciudad_trgm_idx")
@@index([estado(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_estado_trgm_idx")
@@index([profesion(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_profesion_trgm_idx")
@@index([nivel_academico(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_nivel_academico_trgm_idx")
@@index([nacionalidad(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_nacionalidad_trgm_idx")
@@map("formularios")
}
model Respuesta {
id String @id @default(uuid()) @db.Uuid
formulario_id String @db.Uuid
key String
value String
seccion String @default("")
created_at DateTime @default(now()) @db.Timestamptz
formulario Formulario @relation(fields: [formulario_id], references: [id], onDelete: Cascade)
@@index([formulario_id])
@@index([key])
@@index([seccion])
@@index([value(ops: raw("gin_trgm_ops"))], type: Gin, map: "respuestas_value_trgm_idx")
@@map("respuestas")
}
model Seccion {
id String @id @default(uuid()) @db.Uuid
title_key String
title String?
orden Int @default(0)
columns Int @default(1)
created_at DateTime @default(now()) @db.Timestamptz
campos Campo[]
@@index([orden])
@@map("secciones")
}
model Campo {
id String @id @default(uuid()) @db.Uuid
key String @unique
label_key String
tipo String
seccion_id String? @db.Uuid
required Boolean @default(false)
orden Int @default(0)
options Json? @db.JsonB
source String?
created_at DateTime @default(now()) @db.Timestamptz
seccion Seccion? @relation(fields: [seccion_id], references: [id], onDelete: SetNull)
@@index([seccion_id])
@@index([tipo])
@@map("campos")
}
model FormVersion {
id String @id @default(uuid()) @db.Uuid
version String
config Json @db.JsonB
active Boolean @default(false)
created_at DateTime @default(now()) @db.Timestamptz
@@map("form_versions")
}
model Admin {
id String @id @default(uuid()) @db.Uuid
auth_user_id String? @unique @db.Uuid
email String @unique
username String? @unique
nombre String
password_hash String?
rol String @default("coordinador")
activo Boolean @default(true)
ultimo_acceso DateTime? @db.Timestamptz
created_at DateTime @default(now()) @db.Timestamptz
sessions Session[]
@@index([email])
@@map("admins")
}
model Session {
id String @id @default(uuid()) @db.Uuid
token_hash String @unique
admin_id String @db.Uuid
expires_at DateTime @db.Timestamptz
created_at DateTime @default(now()) @db.Timestamptz
admin Admin @relation(fields: [admin_id], references: [id], onDelete: Cascade)
@@index([admin_id])
@@map("sessions")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 655 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 749 B

View File

@ -1,249 +0,0 @@
{
"action": "/api/formulario/send",
"submit_label": "form.submit",
"steps": [
{
"label": "form.step1_reglamento",
"fields": [
{
"key": "aceptacion_reglamento",
"label": "form.aceptacion_reglamento",
"type": "checkbox",
"required": true,
"options": [
{ "value": "leido_acepto", "label": "form.aceptacion.leido", "required": true },
{ "value": "codigo_etica", "label": "form.aceptacion.codigo_etica", "required": true },
{ "value": "datos_personales", "label": "form.aceptacion.datos", "required": true },
{ "value": "participacion_voluntaria", "label": "form.aceptacion.voluntaria", "required": true },
{ "value": "informacion_verdadera", "label": "form.aceptacion.verdadera", "required": true },
{ "value": "firma_electronica_validez", "label": "form.aceptacion.firma_validez", "required": true }
]
}
]
},
{
"label": "form.step2",
"column": 2,
"fields": [
{ "key": "nombre", "label": "form.nombre", "type": "text", "required": true },
{ "key": "segundo_nombre", "label": "form.segundo_nombre", "type": "text" },
{ "key": "apellido", "label": "form.apellido", "type": "text", "required": true },
{ "key": "documentos", "label": "form.documentos", "type": "text", "required": true },
{ "key": "correo", "label": "form.correo", "type": "email", "required": true, "colspan": 2 },
{ "key": "reglamento_firma", "label": "form.reglamento_firma", "type": "text", "required": true, "colspan": 2, "placeholder": "form.reglamento_firma_placeholder" },
{ "key": "fecha_nacimiento", "label": "form.fecha_nacimiento", "type": "date", "required": true },
{ "key": "nacionalidad", "label": "form.nacionalidad", "type": "autocomplete", "required": true, "source": "/forms/paises.json" },
{
"key": "sexo",
"label": "form.sexo",
"type": "radio",
"colspan": 2,
"required": true,
"options": [
{ "value": "M", "label": "form.sexo.m" },
{ "value": "F", "label": "form.sexo.f" }
]
},
{ "key": "direccion_completa", "label": "form.direccion", "type": "text", "required": true, "colspan": 2 },
{ "key": "direccion_postal", "label": "form.direccion_postal", "type": "text", "required": false, "colspan": 2 },
{ "key": "ciudad", "label": "form.ciudad", "type": "text", "required": true },
{ "key": "estado", "label": "form.estado", "type": "text" },
{ "key": "pais", "label": "form.pais", "type": "select", "required": true, "source": "/forms/paises.json" },
{ "key": "postal", "label": "form.codigo_postal", "type": "text", "required": true },
{ "key": "telefono", "label": "form.mobile", "type": "phone_country", "required": true, "source": "/forms/paises.json" },
{ "key": "whatsapp", "label": "form.whatsapp", "type": "phone_country", "source": "/forms/paises.json" }
]
},
{
"label": "form.step3",
"column": 2,
"fields": [
{ "key": "profesion", "label": "form.profesion", "type": "text", "required": true },
{ "key": "lugar_trabajo_actual", "label": "form.lugar_trabajo_actual", "type": "text", "required": true },
{ "key": "nivel_academico", "label": "form.nivel_academico", "type": "text", "required": true, "colspan": 2 },
{
"key": "idioma",
"label": "form.idioma",
"type": "checkbox",
"required": true,
"colspan": 2,
"levels": {
"options": [
{ "value": "Basico", "label": "form.nivel.basico" },
{ "value": "Intermedio", "label": "form.nivel.intermedio" },
{ "value": "Avanzado", "label": "form.nivel.avanzado" }
]
},
"options": [
{ "value": "Espanol", "label": "form.idioma.esp" },
{ "value": "Ingles", "label": "form.idioma.ing" },
{ "value": "Hebreo", "label": "form.idioma.heb" },
{ "value": "Portugues", "label": "form.idioma.port" },
{ "value": "Frances", "label": "form.idioma.franc" },
{ "value": "Otro", "label": "form.idioma.otro" }
]
},
{ "key": "idioma_otro", "label": "form.idioma_otro", "type": "text", "required": false, "colspan": 2, "showWhen": { "field": "idioma", "value": "Otro" } },
{ "key": "voluntariado_anterior", "label": "form.voluntariado_anterior", "type": "radio", "required": true, "colspan": 2, "options":
[
{ "value": "si", "label": "form.voluntariado_anterior.si" },
{ "value": "no", "label": "form.voluntariado_anterior.no" }
]
},
{ "key": "caso_si", "label": "form.caso_si", "type": "textarea", "required": false, "colspan": 2, "showWhen": { "field": "voluntariado_anterior", "value": "si" } }
]
},
{
"label": "form.step4",
"fields": [
{
"key": "areas_colaborar",
"label": "form.areas_colaborar",
"type": "checkbox",
"required": true,
"options": [
{ "value": "Ayuda Humanitaria", "label": "form.areas.ayuda_humanitaria" },
{ "value": "Educacion", "label": "form.areas.educacion" },
{ "value": "Desarrollo Comunitario", "label": "form.areas.desarrollo_comunitario" },
{ "value": "Liderazgo", "label": "form.areas.liderazgo" },
{ "value": "Logistica", "label": "form.areas.logistica" },
{ "value": "Organizacion de Eventos", "label": "form.areas.organizacion_eventos" },
{ "value": "Comunicacion Institucional", "label": "form.areas.comunicacion_institucional" },
{ "value": "Fotografia y Medios", "label": "form.areas.fotografia_medios" },
{ "value": "Recaudacion de Fondos", "label": "form.areas.recaudacion_fondos" },
{ "value": "Gestion de Proyectos", "label": "form.areas.gestion_proyectos" },
{ "value": "Traduccion e Interpretacion", "label": "form.areas.traduccion_interpretacion" },
{ "value": "Asesoria Juridica", "label": "form.areas.asesoria_juridica" },
{ "value": "Servicios Medicos", "label": "form.areas.servicios_medicos" },
{ "value": "Diplomacia Publica", "label": "form.areas.diplomacia_publica" },
{ "value": "Administracion", "label": "form.areas.administracion" },
{ "value": "Tecnologia e Innovacion", "label": "form.areas.tecnologia_innovacion" },
{ "value": "Otra", "label": "form.areas.otra" }
]
},
{ "key": "areas_otra", "label": "form.areas_otra", "type": "text", "required": false, "showWhen": { "field": "areas_colaborar", "value": "Otra" } }
]
},
{
"label": "form.step5",
"fields": [
{
"key": "dias_disponibles",
"label": "form.dias_disponibles",
"type": "checkbox",
"required": true,
"options": [
{ "value": "Lunes", "label": "form.dias.lunes" },
{ "value": "Martes", "label": "form.dias.martes" },
{ "value": "Miercoles", "label": "form.dias.miercoles" },
{ "value": "Jueves", "label": "form.dias.jueves" },
{ "value": "Viernes", "label": "form.dias.viernes" },
{ "value": "Sabado", "label": "form.dias.sabado" },
{ "value": "Domingo", "label": "form.dias.domingo" }
]
},
{
"key": "horario_preferido",
"label": "form.horario_preferido",
"type": "checkbox",
"required": true,
"options": [
{ "value": "Manana", "label": "form.horario.manana" },
{ "value": "Tarde", "label": "form.horario.tarde" },
{ "value": "Noche", "label": "form.horario.noche" },
{ "value": "Segun necesidades", "label": "form.horario.segun" }
]
},
{
"key": "fuera_ciudad",
"label": "form.fuera_ciudad",
"type": "radio",
"required": true,
"options": [
{ "value": "si", "label": "form.si" },
{ "value": "no", "label": "form.no" }
]
},
{
"key": "misiones_internacionales",
"label": "form.misiones_internacionales",
"type": "radio",
"required": true,
"options": [
{ "value": "si", "label": "form.si" },
{ "value": "no", "label": "form.no" }
]
}
]
},
{
"label": "form.step6",
"fields": [
{
"key": "condicion_medica",
"label": "form.condicion_medica",
"type": "radio",
"required": true,
"options": [
{ "value": "si", "label": "form.si" },
{ "value": "no", "label": "form.no" }
]
},
{
"key": "condicion_medica_cual",
"label": "form.condicion_medica_cual",
"type": "textarea",
"required": true,
"showWhen": { "field": "condicion_medica", "value": "si" }
},
{
"key": "alergias",
"label": "form.alergias",
"type": "textarea",
"required": false
},
{
"key": "medicamentos",
"label": "form.medicamentos",
"type": "textarea",
"required": false
}
]
},
{
"label": "form.step7",
"fields": [
{ "key": "emergencia_nombre", "label": "form.emergencia_nombre", "type": "text", "required": true },
{ "key": "emergencia_parentesco", "label": "form.emergencia_parentesco", "type": "text", "required": true },
{ "key": "emergencia_telefono", "label": "form.emergencia_telefono", "type": "phone_country", "required": true, "source": "/forms/paises.json" },
{ "key": "emergencia_correo", "label": "form.emergencia_correo", "type": "email", "required": false }
]
},
{
"label": "form.step8",
"column": 2,
"fields": [
{ "key": "confirmar_nombre", "label": "form.confirmar_nombre", "type": "text", "readonly": true, "valueFrom": "nombre_completo" },
{ "key": "confirmar_firma", "label": "form.confirmar_firma", "type": "text", "readonly": true, "valueFrom": "reglamento_firma" },
{
"key": "declaracion_voluntario",
"label": "form.declaracion_voluntario",
"type": "checkbox",
"required": true,
"colspan": 2,
"options": [
{ "value": "informacion_verdadera", "label": "form.declaracion.verdadera", "required": true },
{ "value": "acepto_reglamento", "label": "form.declaracion.acepto_reglamento", "required": true },
{ "value": "compromiso_etica", "label": "form.declaracion.compromiso_etica", "required": true },
{ "value": "autorizo_datos", "label": "form.declaracion.autorizo_datos", "required": true },
{ "value": "confidencialidad", "label": "form.declaracion.confidencialidad", "required": true },
{ "value": "participacion_voluntaria", "label": "form.declaracion.voluntaria", "required": true }
]
},
{ "key": "confirmacion", "label": "form.confirmacion", "type": "checkbox", "colspan": 2, "required": true, "options": [
{ "value": "confirmado", "label": "form.confirmacion_label", "required": true }
] }
]
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 39 39" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M35.031,28.826l-7.433,0c-0.01,0 -0.019,0 -0.029,-0.001l-2.807,0l-5.141,8.91c-0.064,0.111 -0.18,0.173 -0.3,0.173c-0.058,0 -0.118,-0.015 -0.172,-0.046c-0.069,-0.04 -0.118,-0.099 -0.146,-0.167l-5.127,-8.87l-10.267,0c-0.124,0 -0.238,-0.066 -0.299,-0.173c-0.062,-0.107 -0.062,-0.238 -0.001,-0.345l5.131,-8.911l-5.131,-8.885c-0.061,-0.106 -0.061,-0.238 0,-0.345c0.062,-0.107 0.176,-0.173 0.3,-0.173l10.244,0l5.136,-8.92c0.095,-0.165 0.306,-0.222 0.471,-0.127c0.041,0.024 0.076,0.055 0.103,0.09c0.036,0.027 0.067,0.062 0.091,0.103l5.109,8.855l10.267,0c0.123,0 0.237,0.066 0.299,0.173c0.062,0.107 0.062,0.238 0,0.345l-5.134,8.897l5.135,8.899c0.062,0.106 0.062,0.238 0,0.345c-0.062,0.107 -0.175,0.173 -0.299,0.173m-15.662,-28.826c-10.697,0 -19.369,8.672 -19.369,19.369c0,10.697 8.672,19.369 19.369,19.369c10.698,0 19.369,-8.672 19.369,-19.369c0,-10.697 -8.671,-19.369 -19.369,-19.369" style="fill:#cca16a;fill-rule:nonzero;"/><path d="M27.223,15.618c-1.957,0.335 -3.144,2.679 -3.7,3.581c-1.187,-9.169 -12.661,-7.836 -12.661,-7.836c0,-0 -0.75,7.043 6.248,10.34c-0.07,0.001 -0.14,0.006 -0.211,0.006c-2.644,-0 -5.04,-1.059 -6.787,-2.776l0,0.42c0,4.867 3.946,8.813 8.813,8.813l0.281,-0c4.853,-0 8.486,-4.009 8.813,-8.312c0.107,-1.405 0.811,-2.104 1.874,-2.323c-0.431,-1.29 -1.283,-2.15 -2.67,-1.913" style="fill:#cca16a;fill-rule:nonzero;"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

BIN
public/img/hero-image.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,187 +0,0 @@
<h1>General Volunteer Program Regulations</h1>
<p>Volunteer Terms, Conditions, and Code of Conduct</p>
<section id="introduccion">
<h2>Introduction</h2>
<p>The Kingdom of Peace and Justice Centers mission is to promote peace, justice, leadership, education, humanitarian aid, social responsibility, and cooperation among communities and nations.</p>
<p>The following Regulations establish the legal, ethical, and institutional framework that governs the participation of volunteers, defining their rights, obligations, and principles of conduct, in order to ensure that all volunteer activities are carried out in accordance with the values of the Center and applicable law.</p>
</section>
<section id="objeto-voluntariado">
<h2>1. Purpose of the Volunteering</h2>
<p>The volunteer participates freely and voluntarily in the events of the Center for the purpose of contributing to the fulfillment of its institutional mission in the areas of education, community development, humanitarian assistance, leadership, public diplomacy, promotion of peace and justice, and service to society.</p>
<p>All activities shall be carried out with a spirit of service, responsibility, integrity, respect, cooperation, and commitment to the common good.</p>
</section>
<section id="naturaleza-voluntariado">
<h2>2. Nature of Volunteering</h2>
<p>The volunteers participation is exclusively voluntary in nature and does not constitute, under any circumstances, an employment, contractual, corporate, representative, agency, or subordinate relationship with the Center.</p>
<p>Participation in the events does not generate any right to salary, employment benefits, social security, compensation, or any other economic remuneration, except with the express written authorization of the Center regarding reimbursement of expenses.</p>
</section>
<section id="requisitos-voluntario">
<h2>3. Requirements to Become a Volunteer</h2>
<p>The volunteer declares that:</p>
<ul>
<li>The information provided is truthful, complete, and up to date.</li>
<li>They are physically and mentally fit to participate in the activities.</li>
<li>There is no legal incapacity or restriction to their participation.</li>
<li>They will comply with the policies, regulations, and procedures established by the Center.</li>
</ul>
</section>
<section id="seguro">
<h2>4. Insurance Coverage</h2>
<p>Unless expressly communicated in writing by the Center, each volunteer shall be responsible for having the medical, accident, and other insurance coverage they deem necessary for their participation.</p>
<p>The purchase of any insurance by the Center does not replace the personal responsibility of the volunteer.</p>
</section>
<section id="responsabilidad-medica">
<h2>5. Medical Liability</h2>
<p>The volunteer states that their state of health allows them to participate in the events and agrees to promptly report any medical condition that may affect their safety or that of third parties.</p>
</section>
<section id="cumplimiento-normas">
<h2>6. Compliance with Rules</h2>
<p>The volunteer agrees to comply with the instructions given by Center representatives, respect safety rules, and to always act with responsibility, professionalism, and respect toward all persons.</p>
</section>
<section id="confidencialidad">
<h2>7. Confidentiality</h2>
<p>All information to which the volunteer has access during their participation shall be considered confidential.</p>
<p>The volunteer agrees not to disclose institutional, financial, strategic, personal, or any other reserved information, even after their collaboration with the Center has ended.</p>
</section>
<section id="proteccion-datos">
<h2>8. Protection of Personal Data</h2>
<p>The volunteer authorizes the Center to collect, store, and process their personal data exclusively for purposes related to:</p>
<ul>
<li>the administration of the volunteer program;</li>
<li>institutional communication;</li>
<li>the organization of events;</li>
<li>compliance with legal obligations;</li>
<li>the safety of participants.</li>
</ul>
</section>
<section id="uso-imagen">
<h2>9. Image Use</h2>
<p>The volunteer authorizes the Center to use photographs, audio recordings, videos, and images obtained during the events for institutional, educational, informational, promotional, and fundraising purposes, unless they expressly state otherwise in writing.</p>
</section>
<section id="propiedad-intelectual">
<h2>10. Intellectual Property</h2>
<p>Any document, photograph, video, audiovisual material, presentation, publication, content, or work developed by the volunteer within the institutional events will be the exclusive property of the Center, unless otherwise agreed in writing.</p>
</section>
<section id="uso-bienes">
<h2>11. Property Use and Care</h2>
<p>The volunteer will use the Centers property, equipment, and materials responsibly, agreeing to maintain them in good condition and return them when required or at the end of their participation.</p>
</section>
<section id="terminacion-voluntariado">
<h2>12. Termination of Volunteer Participation</h2>
<p>The Center may suspend or terminate the participation of any volunteer when there is noncompliance with these Regulations, conduct that is incompatible with institutional values, risks to the organization, or any other justified reason.</p>
<p>The volunteer may withdraw freely by providing reasonable advance notice.</p>
</section>
<section id="limitacion-responsabilidad">
<h2>13. Limitation of Liability</h2>
<p>The volunteer acknowledges that they participate freely in the Centers activities and accept the normal risks inherent to them.</p>
<p>To the extent permitted by applicable law, the Center, its directors, employees, representatives, collaborators, and other volunteers will not be liable for damages arising from the volunteers participation, except in cases of willful misconduct or gross negligence.</p>
</section>
<section id="declaracion-riesgos">
<h2>14. Assumption of Risk Statement</h2>
<p>The volunteer declares that they are aware that certain events may involve risks arising from travel, community work, humanitarian assistance, public events, institutional visits, or other events of the program.</p>
<p>The volunteer accepts participation under their own responsibility and voluntarily assumes such risks, except when there is willful misconduct or gross negligence attributable to the Center.</p>
</section>
<section id="conducta-institucional">
<h2>15. Institutional Conduct</h2>
<p>The volunteer agrees to maintain conduct consistent with the principles and values of the Center.</p>
<p>The following are prohibited:</p>
<ul>
<li>committing acts of physical or verbal violence;</li>
<li>discriminating against or harassing any person;</li>
<li>consuming illegal drugs or appearing under the influence;</li>
<li>carrying weapons without legal and express authorization;</li>
<li>requesting donations or administering financial resources on behalf of the Center without written authorization;</li>
<li>using the name of the Center for personal, political, or commercial purposes.</li>
</ul>
</section>
<section id="proteccion-menores">
<h2>16. Safeguarding Minors and Vulnerable Individuals</h2>
<p>When events involve minors or individuals in situations of vulnerability, the volunteer shall strictly comply with the protection protocols established by the Center and applicable law.</p>
</section>
<section id="conflicto-intereses">
<h2>17. Conflict of Interest</h2>
<p>The volunteer shall report any personal, professional, economic, or family situation that could create a conflict of interest with the activities of the Center.</p>
</section>
<section id="cumplimiento-normativo">
<h2>18. Regulatory Compliance</h2>
<p>The volunteer agrees to comply with all applicable legislation related to human rights, protection of personal data, anti-corruption, prevention of money laundering, equality, nondiscrimination, and all other applicable rules.</p>
</section>
<section id="uso-imagen-institucional">
<h2>19. Use of the Institutional Name and Image</h2>
<p>The Centers name, logo, trademarks, corporate image, and other distinctive signs may only be used with prior written authorization.</p>
</section>
<section id="redes-sociales">
<h2>20. Social Media and Communications</h2>
<p>The volunteer may not issue public statements, interviews, communications, or publications on behalf of the Center without prior authorization.</p>
<p>They also agree to use social media responsibly, avoiding publications that may affect the institutional image.</p>
</section>
<section id="donaciones">
<h2>21. Donations</h2>
<p>The status of volunteer does not grant any right over the goods, resources, projects, or donations administered by the Center.</p>
<p>Any fundraising campaign must have prior written authorization.</p>
</section>
<section id="fuerza-mayor">
<h2>22. Force Majeure</h2>
<p>The Center may suspend, modify, or cancel any event due to force majeure, security reasons, armed conflicts, health emergencies, natural disasters, or any other circumstance beyond its control, without this generating any liability.</p>
</section>
<section id="legislacion-jurisdiccion">
<h2>23. Applicable Law and Jurisdiction</h2>
<p>These Regulations shall be governed by the laws in force in the country where the volunteer events are carried out.</p>
<p>Any dispute shall be submitted to the jurisdiction of the competent courts of that country.</p>
</section>
<section id="codigo-etica">
<h2>Volunteer Code of Conduct</h2>
<p>Every volunteer commits to:</p>
<ul>
<li>Act with honesty, integrity, and transparency.</li>
<li>Respect the dignity and rights of all people.</li>
<li>Promote a culture of peace, justice, and solidarity.</li>
<li>Maintain absolute confidentiality.</li>
<li>Avoid conflicts of interest.</li>
<li>Represent the Center with respect and professionalism.</li>
<li>Comply with applicable law and institutional policies.</li>
<li>Protect the reputation and values of the Center.</li>
<li>Always act with a spirit of service.</li>
</ul>
</section>
<section id="politica-privacidad">
<h2>Data Protection and Privacy Policy</h2>
<p>The Center recognizes the importance of protecting the privacy of its volunteers and agrees to process all personal information in accordance with applicable law.</p>
<p>Personal data shall be used exclusively for:</p>
<ul>
<li>Administration of the volunteer program.</li>
<li>Organization of activities and events.</li>
<li>Institutional communication.</li>
<li>Compliance with legal obligations.</li>
<li>Protection and safety of participants.</li>
<li>Preparation of statistics and institutional reports.</li>
</ul>
<p>The Center shall adopt reasonable technical, administrative, and organizational measures to protect personal information against loss, unauthorized access, alteration, disclosure, or improper use.</p>
<p>Volunteers may exercise the rights granted to them by applicable law regarding access, rectification, updating, or deletion of their personal data.</p>
</section>

View File

@ -1,187 +0,0 @@
<h1>Reglamento General del Programa de Voluntariado</h1>
<p>Términos, Condiciones y Código de Ética para Voluntarios</p>
<section id="introduccion">
<h2>Introducción</h2>
<p>El Centro del Reino de Paz y Justicia (CRPJ) tiene como misión promover la paz, la justicia, el liderazgo, la educación, la ayuda humanitaria, la responsabilidad social y la cooperación entre comunidades y naciones.</p>
<p>El presente Reglamento establece el marco jurídico, ético e institucional que regula la participación de los voluntarios, definiendo sus derechos, obligaciones y principios de actuación, con el fin de garantizar que todas las actividades de voluntariado se desarrollen conforme a los valores del CRPJ y a la legislación aplicable.</p>
</section>
<section id="objeto-voluntariado">
<h2>1. Objeto del Voluntariado</h2>
<p>El voluntario participa libre y participa libremente, por un deseo propio y personal, en las actividades del CRPJ con el propósito de contribuir al cumplimiento de la misión institucional en las áreas de educación, desarrollo comunitario, asistencia humanitaria, liderazgo, diplomacia pública, promoción de la paz, la justicia y el servicio a la sociedad.</p>
<p>Toda actividad deberá desarrollarse con espíritu de servicio, responsabilidad, integridad, respeto, cooperación y compromiso con el bienestar común.</p>
</section>
<section id="naturaleza-voluntariado">
<h2>2. Naturaleza del Voluntariado</h2>
<p>La labor de quien participa tiene carácter exclusivamente voluntario y no constituye, bajo ninguna circunstancia, una relación laboral, contractual, societaria, de representación, mandato o subordinación con el CRPJ.</p>
<p>La participación en las actividades no genera derecho a salario, prestaciones laborales, seguridad social, indemnizaciones ni cualquier otra remuneración económica, salvo autorización expresa y por escrito del CRPJ respecto del reembolso de gastos.</p>
</section>
<section id="requisitos-voluntario">
<h2>3. Requisitos para ser Voluntario</h2>
<p>El voluntario declara que:</p>
<ul>
<li>La información proporcionada por el voluntario es veraz, completa y actualizada.</li>
<li>Se encuentra física y mentalmente apto para participar en las actividades.</li>
<li>No existe impedimento legal para su participación.</li>
<li>Cumplirá las políticas, reglamentos y procedimientos establecidos por el CRPJ.</li>
</ul>
</section>
<section id="seguro">
<h2>4. Seguro</h2>
<p>Salvo comunicación expresa y por escrito del CRPJ, cada voluntario será responsable de contar con la cobertura médica, de accidentes y demás seguros que considere necesarios para su participación.</p>
<p>La contratación de cualquier seguro por parte del CRPJ no sustituye la responsabilidad personal del voluntario.</p>
</section>
<section id="responsabilidad-medica">
<h2>5. Responsabilidad Médica</h2>
<p>El voluntario manifiesta que su estado de salud le permite participar en las actividades y se compromete a informar oportunamente cualquier condición médica que pueda afectar su seguridad o la de terceros.</p>
</section>
<section id="cumplimiento-normas">
<h2>6. Cumplimiento de Normas</h2>
<p>El voluntario se compromete a cumplir las instrucciones impartidas por los responsables del CRPJ, respetar las normas de seguridad y actuar siempre con responsabilidad, profesionalismo y respeto hacia todas las personas.</p>
</section>
<section id="confidencialidad">
<h2>7. Confidencialidad</h2>
<p>Toda la información a la que el voluntario tenga acceso durante su participación será considerada confidencial.</p>
<p>El voluntario se obliga a no divulgar información institucional, financiera, estratégica, personal o cualquier otra información reservada, aun después de finalizar su colaboración con el CRPJ.</p>
</section>
<section id="proteccion-datos">
<h2>8. Protección de Datos Personales</h2>
<p>El voluntario autoriza al CRPJ a recopilar, almacenar y tratar sus datos personales exclusivamente para fines relacionados con:</p>
<ul>
<li>la administración del programa de voluntariado;</li>
<li>la comunicación institucional;</li>
<li>la organización de actividades;</li>
<li>el cumplimiento de obligaciones legales;</li>
<li>la seguridad de los participantes.</li>
</ul>
</section>
<section id="uso-imagen">
<h2>9. Uso de Imagen</h2>
<p>El voluntario autoriza al CRPJ a utilizar fotografías, grabaciones de audio, videos e imágenes obtenidas durante las actividades para fines institucionales, educativos, informativos, promocionales y de recaudación de fondos, salvo manifestación expresa y escrita en caso contrario.</p>
</section>
<section id="propiedad-intelectual">
<h2>10. Propiedad Intelectual</h2>
<p>Todo documento, fotografía, video, material audiovisual, presentación, publicación, contenido o trabajo desarrollado por el voluntario dentro de las actividades institucionales será propiedad exclusiva del CRPJ, salvo acuerdo escrito en caso contrario.</p>
</section>
<section id="uso-bienes">
<h2>11. Uso y Cuidado de Bienes</h2>
<p>El voluntario deberá utilizar responsablemente los bienes, equipos y materiales del CRPJ, comprometiéndose a conservarlos y devolverlos cuando le sean requeridos o al finalizar su participación.</p>
</section>
<section id="terminacion-voluntariado">
<h2>12. Terminación del Voluntariado</h2>
<p>El CRPJ podrá suspender o finalizar la participación de cualquier voluntario cuando exista incumplimiento del presente Reglamento, conducta incompatible con los valores institucionales, riesgos para la organización o cualquier otra causa justificada.</p>
<p>El voluntario podrá retirarse libremente notificándolo con una anticipación razonable.</p>
</section>
<section id="limitacion-responsabilidad">
<h2>13. Limitación de Responsabilidad</h2>
<p>El voluntario reconoce que participa libremente en las actividades del CRPJ y acepta los riesgos normales inherentes a las mismas.</p>
<p>En la medida permitida por la legislación aplicable, el CRPJ, sus directivos, empleados, representantes, colaboradores y demás voluntarios no serán responsables por daños derivados de la participación del voluntario, salvo en casos de dolo o negligencia grave.</p>
</section>
<section id="declaracion-riesgos">
<h2>14. Declaración de Asunción de Riesgos</h2>
<p>El voluntario declara conocer que determinadas actividades pueden implicar riesgos derivados de desplazamientos, trabajo comunitario, asistencia humanitaria, eventos públicos, visitas institucionales u otras actividades propias del programa.</p>
<p>El voluntario acepta participar bajo su propia responsabilidad y asume voluntariamente dichos riesgos, salvo cuando exista dolo o negligencia grave imputable al CRPJ.</p>
</section>
<section id="conducta-institucional">
<h2>15. Conducta Institucional</h2>
<p>El voluntario se compromete a mantener una conducta acorde con los principios y valores del CRPJ.</p>
<p>Queda prohibido:</p>
<ul>
<li>realizar actos de violencia física o verbal;</li>
<li>discriminar o acosar a cualquier persona;</li>
<li>consumir drogas ilegales o presentarse bajo sus efectos;</li>
<li>portar armas sin autorización legal y expresa;</li>
<li>solicitar donaciones o administrar recursos económicos en nombre del CRPJ sin autorización escrita;</li>
<li>utilizar el nombre del CRPJ para fines personales, políticos o comerciales.</li>
</ul>
</section>
<section id="proteccion-menores">
<h2>16. Protección de Menores y Personas Vulnerables</h2>
<p>Cuando las actividades involucren menores de edad o personas en situación de vulnerabilidad, el voluntario deberá cumplir estrictamente los protocolos de protección establecidos por el CRPJ y la legislación aplicable.</p>
</section>
<section id="conflicto-intereses">
<h2>17. Conflicto de Intereses</h2>
<p>El voluntario deberá informar cualquier situación personal, profesional, económica o familiar que pudiera generar un conflicto de interés con las actividades del CRPJ.</p>
</section>
<section id="cumplimiento-normativo">
<h2>18. Cumplimiento Normativo</h2>
<p>El voluntario se compromete a cumplir toda la legislación aplicable relacionada con derechos humanos, protección de datos personales, anticorrupción, prevención del lavado de dinero, igualdad, no discriminación y demás normas aplicables.</p>
</section>
<section id="uso-imagen-institucional">
<h2>19. Uso del Nombre e Imagen Institucional</h2>
<p>El nombre, logotipo, marcas, imagen corporativa y demás signos distintivos del CRPJ únicamente podrán utilizarse con autorización previa y escrita.</p>
</section>
<section id="redes-sociales">
<h2>20. Redes Sociales y Comunicaciones</h2>
<p>El voluntario no podrá emitir declaraciones públicas, entrevistas, comunicados o publicaciones en representación del CRPJ sin autorización previa.</p>
<p>Asimismo, se compromete a utilizar responsablemente las redes sociales evitando publicaciones que puedan afectar la imagen institucional.</p>
</section>
<section id="donaciones">
<h2>21. Donaciones</h2>
<p>La condición de voluntario no otorga derecho alguno sobre los bienes, recursos, proyectos o donaciones administrados por el CRPJ.</p>
<p>Toda campaña de recaudación de fondos deberá contar con autorización previa y escrita.</p>
</section>
<section id="fuerza-mayor">
<h2>22. Fuerza Mayor</h2>
<p>El CRPJ podrá suspender, modificar o cancelar cualquier actividad debido a causas de fuerza mayor, razones de seguridad, conflictos armados, emergencias sanitarias, desastres naturales o cualquier otra circunstancia fuera de su control, sin que ello genere responsabilidad alguna.</p>
</section>
<section id="legislacion-jurisdiccion">
<h2>23. Legislación Aplicable y Jurisdicción</h2>
<p>El presente Reglamento se regirá por la legislación vigente del país donde se desarrollen las actividades del voluntariado.</p>
<p>Toda controversia será sometida a la jurisdicción de los tribunales competentes de dicho país.</p>
</section>
<section id="codigo-etica">
<h2>Código de Ética del Voluntario</h2>
<p>Todo voluntario se compromete a:</p>
<ul>
<li>Actuar con honestidad, integridad y transparencia.</li>
<li>Respetar la dignidad y los derechos de todas las personas.</li>
<li>Promover una cultura de paz, justicia y solidaridad.</li>
<li>Mantener absoluta confidencialidad.</li>
<li>Evitar conflictos de interés.</li>
<li>Representar al CRPJ con respeto y profesionalismo.</li>
<li>Cumplir la legislación aplicable y las políticas institucionales.</li>
<li>Proteger la reputación y los valores del CRPJ.</li>
<li>Actuar siempre con espíritu de servicio.</li>
</ul>
</section>
<section id="politica-privacidad">
<h2>Política de Privacidad y Tratamiento de Datos Personales</h2>
<p>El CRPJ reconoce la importancia de proteger la privacidad de sus voluntarios y se compromete a tratar toda información personal conforme a la legislación aplicable.</p>
<p>Los datos personales serán utilizados exclusivamente para:</p>
<ul>
<li>Administración del programa de voluntariado.</li>
<li>Organización de actividades y eventos.</li>
<li>Comunicación institucional.</li>
<li>Cumplimiento de obligaciones legales.</li>
<li>Protección y seguridad de los participantes.</li>
<li>Elaboración de estadísticas e informes institucionales.</li>
</ul>
<p>El CRPJ adoptará medidas técnicas, administrativas y organizativas razonables para proteger la información personal contra pérdida, acceso no autorizado, alteración, divulgación o uso indebido.</p>
<p>Los voluntarios podrán ejercer los derechos que les otorgue la legislación aplicable respecto del acceso, rectificación, actualización o eliminación de sus datos personales.</p>
</section>

View File

@ -1,187 +0,0 @@
<h1>Regulamento Geral do Programa de Voluntariado</h1>
<p>Termos, Condições e Código de Ética para Voluntários</p>
<section id="introduccion">
<h2>Introdução</h2>
<p>O Centro do Reino de Paz e Justiça (CRPJ) tem como missão promover a paz, a justiça, a liderança, a educação, a ajuda humanitária, a responsabilidade social e a cooperação entre comunidades e nações.</p>
<p>O presente Regulamento estabelece o marco jurídico, ético e institucional que disciplina a participação dos voluntários, definindo seus direitos, deveres e princípios de atuação, com o objetivo de assegurar que todas as atividades de voluntariado sejam desenvolvidas em conformidade com os valores do CRPJ e com a legislação aplicável.</p>
</section>
<section id="objeto-voluntariado">
<h2>1. Objeto do Voluntariado</h2>
<p>O voluntário participa de forma livre, por desejo próprio e pessoal, das atividades do CRPJ com o propósito de contribuir para o cumprimento da missão institucional nas áreas de educação, desenvolvimento comunitário, assistência humanitária, liderança, diplomacia pública, promoção da paz, da justiça e serviço à sociedade.</p>
<p>Todas as atividades deverão ser desenvolvidas com espírito de serviço, responsabilidade, integridade, respeito, cooperação e compromisso com o bem comum.</p>
</section>
<section id="naturaleza-voluntariado">
<h2>2. Natureza do Voluntariado</h2>
<p>A participação do voluntário possui caráter exclusivamente voluntário e não constitui, em hipótese alguma, vínculo empregatício, contratual, societário, de representação, de mandato ou de subordinação com o CRPJ.</p>
<p>A participação nas atividades não gera direito a salário, benefícios trabalhistas, previdência social, indenizações nem qualquer outra forma de remuneração, salvo mediante autorização expressa e por escrito do CRPJ para reembolso de despesas.</p>
</section>
<section id="requisitos-voluntario">
<h2>3. Requisitos para ser Voluntário</h2>
<p>O voluntário declara que:</p>
<ul>
<li>As informações fornecidas são verdadeiras, completas e atualizadas.</li>
<li>Encontra-se física e mentalmente apto para participar das atividades.</li>
<li>Não possui qualquer impedimento legal para sua participação.</li>
<li>Cumprirá as políticas, os regulamentos e os procedimentos estabelecidos pelo CRPJ.</li>
</ul>
</section>
<section id="seguro">
<h2>4. Seguro</h2>
<p>Salvo comunicação expressa e por escrito do CRPJ, cada voluntário será responsável por possuir cobertura médica, seguro contra acidentes e quaisquer outros seguros que considere necessários para sua participação.</p>
<p>A contratação de qualquer seguro pelo CRPJ não substitui a responsabilidade pessoal do voluntário.</p>
</section>
<section id="responsabilidad-medica">
<h2>5. Responsabilidade Médica</h2>
<p>O voluntário declara que seu estado de saúde lhe permite participar das atividades e compromete-se a informar prontamente qualquer condição médica que possa afetar sua segurança ou a de terceiros.</p>
</section>
<section id="cumplimiento-normas">
<h2>6. Cumprimento das Normas</h2>
<p>O voluntário compromete-se a cumprir as instruções dos responsáveis pelo CRPJ, respeitar as normas de segurança e atuar sempre com responsabilidade, profissionalismo e respeito para com todas as pessoas.</p>
</section>
<section id="confidencialidad">
<h2>7. Confidencialidade</h2>
<p>Todas as informações às quais o voluntário tiver acesso durante sua participação serão consideradas confidenciais.</p>
<p>O voluntário compromete-se a não divulgar informações institucionais, financeiras, estratégicas, pessoais ou quaisquer outras informações confidenciais, mesmo após o término de sua colaboração com o CRPJ.</p>
</section>
<section id="proteccion-dados">
<h2>8. Proteção de Dados Pessoais</h2>
<p>O voluntário autoriza o CRPJ a coletar, armazenar e tratar seus dados pessoais exclusivamente para as seguintes finalidades:</p>
<ul>
<li>administração do Programa de Voluntariado;</li>
<li>comunicação institucional;</li>
<li>organização de atividades;</li>
<li>cumprimento de obrigações legais;</li>
<li>segurança dos participantes.</li>
</ul>
</section>
<section id="uso-imagen">
<h2>9. Uso de Imagem</h2>
<p>O voluntário autoriza o CRPJ a utilizar fotografias, gravações de áudio, vídeos e imagens obtidos durante as atividades para fins institucionais, educacionais, informativos, promocionais e de captação de recursos, salvo manifestação expressa e por escrito em sentido contrário.</p>
</section>
<section id="propiedad-intelectual">
<h2>10. Propriedade Intelectual</h2>
<p>Todo documento, fotografia, vídeo, material audiovisual, apresentação, publicação, conteúdo ou trabalho desenvolvido pelo voluntário no âmbito das atividades institucionais será de propriedade exclusiva do CRPJ, salvo acordo em contrário formalizado por escrito.</p>
</section>
<section id="uso-bienes">
<h2>11. Uso e Conservação dos Bens</h2>
<p>O voluntário deverá utilizar os bens, equipamentos e materiais do CRPJ de forma responsável, comprometendo-se a conservá-los e devolvê-los sempre que solicitado ou ao término de sua participação.</p>
</section>
<section id="terminacion-voluntariado">
<h2>12. Encerramento do Voluntariado</h2>
<p>O CRPJ poderá suspender ou encerrar a participação de qualquer voluntário em caso de descumprimento do presente Regulamento, conduta incompatível com os valores institucionais, riscos para a organização ou qualquer outra causa devidamente justificada.</p>
<p>O voluntário poderá desligar-se do Programa a qualquer momento, mediante comunicação com antecedência razoável.</p>
</section>
<section id="limitacion-responsabilidad">
<h2>13. Limitação de Responsabilidade</h2>
<p>O voluntário reconhece que participa das atividades do CRPJ de forma livre e voluntária e aceita os riscos normais inerentes a elas.</p>
<p>Na medida permitida pela legislação aplicável, o CRPJ, seus dirigentes, empregados, representantes, colaboradores e demais voluntários não serão responsáveis por danos decorrentes da participação do voluntário, salvo nos casos de dolo ou culpa grave.</p>
</section>
<section id="declaracion-riesgos">
<h2>14. Declaração de Assunção de Riscos</h2>
<p>O voluntário declara estar ciente de que determinadas atividades podem envolver riscos decorrentes de deslocamentos, trabalho comunitário, assistência humanitária, eventos públicos, visitas institucionais ou outras atividades próprias do Programa.</p>
<p>O voluntário aceita participar sob sua própria responsabilidade e assume voluntariamente tais riscos, salvo quando houver dolo ou culpa grave imputável ao CRPJ.</p>
</section>
<section id="conducta-institucional">
<h2>15. Conduta Institucional</h2>
<p>O voluntário compromete-se a manter conduta compatível com os princípios e valores do CRPJ.</p>
<p>É proibido ao voluntário:</p>
<ul>
<li>praticar atos de violência física ou verbal;</li>
<li>discriminar ou assediar qualquer pessoa;</li>
<li>consumir drogas ilícitas ou apresentar-se sob seus efeitos;</li>
<li>portar armas sem autorização legal e expressa;</li>
<li>solicitar doações ou administrar recursos financeiros em nome do CRPJ sem autorização por escrito;</li>
<li>utilizar o nome do CRPJ para fins pessoais, políticos ou comerciais.</li>
</ul>
</section>
<section id="proteccion-menores">
<h2>16. Proteção de Menores e Pessoas em Situação de Vulnerabilidade</h2>
<p>Quando as atividades envolverem menores de idade ou pessoas em situação de vulnerabilidade, o voluntário deverá cumprir rigorosamente os protocolos de proteção estabelecidos pelo CRPJ e a legislação aplicável.</p>
</section>
<section id="conflicto-intereses">
<h2>17. Conflito de Interesses</h2>
<p>O voluntário deverá comunicar qualquer situação de natureza pessoal, profissional, econômica ou familiar que possa gerar conflito de interesses em relação às atividades do CRPJ.</p>
</section>
<section id="cumplimiento-normativo">
<h2>18. Cumprimento Normativo</h2>
<p>O voluntário compromete-se a cumprir toda a legislação aplicável relacionada aos direitos humanos, à proteção de dados pessoais, ao combate à corrupção, à prevenção da lavagem de dinheiro, à igualdade, à não discriminação e às demais normas aplicáveis.</p>
</section>
<section id="uso-imagen-institucional">
<h2>19. Uso do Nome e da Identidade Institucional</h2>
<p>O nome, o logotipo, as marcas, a identidade institucional e os demais sinais distintivos do CRPJ somente poderão ser utilizados mediante autorização prévia e por escrito.</p>
</section>
<section id="redes-sociales">
<h2>20. Redes Sociais e Comunicações</h2>
<p>O voluntário não poderá conceder declarações públicas, entrevistas, comunicados ou realizar publicações em nome do CRPJ sem autorização prévia.</p>
<p>Da mesma forma, compromete-se a utilizar as redes sociais de forma responsável, evitando publicações que possam prejudicar a imagem institucional.</p>
</section>
<section id="donaciones">
<h2>21. Doações</h2>
<p>A condição de voluntário não confere qualquer direito sobre os bens, recursos, projetos ou doações administrados pelo CRPJ.</p>
<p>Toda campanha de captação de recursos deverá ser previamente autorizada por escrito.</p>
</section>
<section id="fuerza-mayor">
<h2>22. Caso Fortuito ou Força Maior</h2>
<p>O CRPJ poderá suspender, alterar ou cancelar qualquer atividade em razão de caso fortuito ou força maior, motivos de segurança, conflitos armados, emergências sanitárias, desastres naturais ou quaisquer outras circunstâncias alheias ao seu controle, sem que isso gere qualquer responsabilidade.</p>
</section>
<section id="legislacion-jurisdiccion">
<h2>23. Legislação Aplicável e Jurisdição</h2>
<p>O presente Regulamento será regido pela legislação vigente do país onde forem desenvolvidas as atividades de voluntariado.</p>
<p>Qualquer controvérsia será submetida à jurisdição dos tribunais competentes desse país.</p>
</section>
<section id="codigo-etica">
<h2>Código de Ética do Voluntário</h2>
<p>Todo voluntário compromete-se a:</p>
<ul>
<li>Atuar com honestidade, integridade e transparência.</li>
<li>Respeitar a dignidade e os direitos de todas as pessoas.</li>
<li>Promover uma cultura de paz, justiça e solidariedade.</li>
<li>Manter absoluto sigilo.</li>
<li>Evitar conflitos de interesses.</li>
<li>Representar o CRPJ com respeito e profissionalismo.</li>
<li>Cumprir a legislação aplicável e as políticas institucionais.</li>
<li>Preservar a reputação e os valores do CRPJ.</li>
<li>Atuar sempre com espírito de serviço.</li>
</ul>
</section>
<section id="politica-privacidad">
<h2>Política de Privacidade e Tratamento de Dados Pessoais</h2>
<p>O CRPJ reconhece a importância de proteger a privacidade de seus voluntários e compromete-se a tratar todas as informações pessoais em conformidade com a legislação aplicável.</p>
<p>Os dados pessoais serão utilizados exclusivamente para:</p>
<ul>
<li>Administração do Programa de Voluntariado;</li>
<li>Organização de atividades e eventos;</li>
<li>Comunicação institucional;</li>
<li>Cumprimento de obrigações legais;</li>
<li>Proteção e segurança dos participantes;</li>
<li>Elaboração de estatísticas e relatórios institucionais.</li>
</ul>
<p>O CRPJ adotará medidas técnicas, administrativas e organizacionais razoáveis para proteger as informações pessoais contra perda, acesso não autorizado, alteração, divulgação ou uso indevido.</p>
<p>Os voluntários poderão exercer os direitos assegurados pela legislação aplicável em relação ao acesso, à retificação, à atualização ou à exclusão de seus dados pessoais.</p>
</section>

View File

@ -1,172 +0,0 @@
{
"name": "📊 Reporte de Traducciones - cdrdpyj",
"nodes": [
{
"id": "webhook-1",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.2,
"position": [250, 300],
"parameters": {
"path": "news-summary",
"httpMethod": "POST",
"authentication": "headerAuth",
"responseMode": "onReceived",
"responseData": "{{ $json }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"id": null,
"name": "crpj-summary-key"
}
}
},
{
"id": "code-1",
"name": "Formatear datos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [480, 300],
"parameters": {
"language": "javaScript",
"code": "const langNames = {\n es: 'Español',\n en: 'English',\n fr: 'Français',\n pt: 'Português',\n rw: 'Kinyarwanda',\n he: 'עברית',\n uk: 'Українська',\n ru: 'Русский',\n kr: 'Kreyòl',\n};\n\nconst ORDER = ['es', 'en', 'fr', 'pt', 'rw', 'he', 'uk', 'ru', 'kr'];\nconst payload = $input.first().json.body;\n\nconst summaryRows = [];\nfor (const code of ORDER) {\n const s = payload.summary[code];\n if (!s) continue;\n summaryRows.push({\n json: {\n language: langNames[code] || code,\n code,\n totalEs: payload.totals.spanishArticles,\n translated: s.translated,\n missing: s.missing,\n percent: s.percent,\n },\n });\n}\n\nconst detailRows = [];\nconst orphanCount = payload.orphaned?.length || 0;\n\nif (orphanCount > 0) {\n const warnRow = { json: { date: '', spanishTitle: `⚠️ ${orphanCount} archivo(s) huérfano(s) — sin versión en español`, spanishUrl: '' } };\n for (const lang of ORDER) warnRow.json[lang] = '';\n detailRows.push(warnRow);\n}\n\nfor (const art of payload.articles) {\n const row = { json: { date: art.date, spanishTitle: art.spanishTitle, spanishUrl: art.spanishUrl || '' } };\n for (const lang of ORDER) row.json[lang] = art.files[lang] ? '✅' : '❌';\n detailRows.push(row);\n}\n\nfor (const orphan of payload.orphaned) {\n const row = { json: { date: orphan.groupId, spanishTitle: `⚠️ Huérfano: solo en ${orphan.languages.join(', ')}`, spanishUrl: orphan.urls?.[orphan.languages[0]] || '' } };\n for (const lang of ORDER) row.json[lang] = orphan.languages.includes(lang) ? '⚠️' : '—';\n detailRows.push(row);\n}\n\nreturn [summaryRows, detailRows];\n",
"mode": "raw"
},
"nodesOnOutput": {
"main": [
{
"type": "n8n-nodes-base.googleSheets",
"index": [
0
]
},
{
"type": "n8n-nodes-base.googleSheets",
"index": [
1
]
}
]
}
},
{
"id": "sheets-summary",
"name": "Sheets - Resumen",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [720, 180],
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "",
"mode": "id"
},
"sheetName": "Resumen",
"columns": {
"mappingMode": "defineBelow",
"value": {
"A": "={{ $json.language }}",
"B": "={{ $json.code }}",
"C": "={{ $json.totalEs }}",
"D": "={{ $json.translated }}",
"E": "={{ $json.missing }}",
"F": "={{ $json.percent }}"
}
},
"options": {
"cellFormat": "USER_ENTERED",
"dataLocationOnSheet": "A:F"
},
"handshake": false
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": null,
"name": "Google Sheets"
}
}
},
{
"id": "sheets-detail",
"name": "Sheets - Detalle",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [720, 420],
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "",
"mode": "id"
},
"sheetName": "Detalle",
"columns": {
"mappingMode": "defineBelow",
"value": {
"A": "={{ $json.date }}",
"B": "={{ $json.spanishTitle }}",
"C": "={{ $json.spanishUrl }}",
"D": "={{ $json.es }}",
"E": "={{ $json.en }}",
"F": "={{ $json.fr }}",
"G": "={{ $json.pt }}",
"H": "={{ $json.rw }}",
"I": "={{ $json.he }}",
"J": "={{ $json.uk }}",
"K": "={{ $json.ru }}",
"L": "={{ $json.kr }}"
}
},
"options": {
"cellFormat": "USER_ENTERED",
"dataLocationOnSheet": "A:L"
},
"handshake": false
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": null,
"name": "Google Sheets"
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Formatear datos",
"type": "main",
"index": 0
}
]
]
},
"Formatear datos": {
"main": [
[
{
"node": "Sheets - Resumen",
"type": "main",
"index": 0
}
],
[
{
"node": "Sheets - Detalle",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"staticData": null,
"tags": [],
"versionId": "1.0"
}

View File

@ -1,230 +0,0 @@
import '@dotenvx/dotenvx/config';
import { readFileSync, readdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { google } from 'googleapis';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const NEWS_DIR = join(ROOT, 'src', 'content', 'news');
const LANGUAGES = ['es', 'en', 'fr', 'pt', 'rw', 'he', 'uk', 'ru', 'kr'];
function extractField(fm, field) {
const re = new RegExp(`^${field}:\\s*(.*)$`, 'm');
const m = fm.match(re);
if (!m) return '';
let val = m[1].trim();
if ((val.startsWith("'") && val.endsWith("'")) ||
(val.startsWith('"') && val.endsWith('"'))) {
val = val.slice(1, -1);
}
return val;
}
const ROUTE_TRANSLATIONS = {
es: "noticias", en: "news", fr: "informations",
he: "\u05d7\u05d3\u05e9\u05d5\u05ea", uk: "noticias", pt: "noticias",
ru: "\u043d\u043e\u0432\u043e\u0441\u0442\u0438", rw: "amakuru", kr: "nouvel",
};
function articleUrl(locale, slug) {
const route = ROUTE_TRANSLATIONS[locale] || 'news';
return `https://www.centrodelreinodepazyjusticia.com/${locale}/${route}/${slug}`;
}
async function clearSheet(sheetName) {
const saEmail = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
const pk = process.env.GOOGLE_PRIVATE_KEY;
const sid = process.env.GOOGLE_SHEET_ID;
if (!saEmail || !pk || !sid) {
console.log(`[send-to-n8n] Skipping sheet clear — missing Google credentials`);
return;
}
const auth = new google.auth.GoogleAuth({
credentials: {
client_email: saEmail,
private_key: pk.replace(/\\n/g, '\n'),
},
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
await sheets.spreadsheets.values.clear({
spreadsheetId: sid,
range: `${sheetName}!A:Z`,
});
console.log(`[send-to-n8n] Cleared sheet: ${sheetName}`);
}
function parseMeta(filePath) {
const raw = readFileSync(filePath, 'utf-8');
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!m) return {};
const fm = m[1];
return {
locale: extractField(fm, 'locale'),
title: extractField(fm, 'title'),
date: extractField(fm, 'date'),
draft: extractField(fm, 'draft'),
slug: extractField(fm, 'slug'),
};
}
function normFilename(fileName) {
const name = fileName.replace(/\.md$/, '');
return name.replace(/^(\d{4}-\d{2}-\d{2})-0(\d)$/, '$1-$2');
}
async function main() {
const groups = {};
for (const lang of LANGUAGES) {
const dir = join(NEWS_DIR, lang);
if (!existsSync(dir)) continue;
for (const file of readdirSync(dir).filter(f => f.endsWith('.md'))) {
const meta = parseMeta(join(dir, file));
if (meta.draft === 'true') continue;
const key = normFilename(file);
if (!groups[key]) groups[key] = {};
groups[key][lang] = {
file,
title: meta.title || '',
date: meta.date || '',
slug: meta.slug || '',
};
}
}
const allKeys = Object.keys(groups).sort();
const esKeys = allKeys.filter(k => groups[k].es);
const summary = Object.fromEntries(LANGUAGES.map(l => {
const matches = esKeys.filter(k => groups[k][l]).length;
return [l, {
total: allKeys.filter(k => groups[k][l]).length,
translated: matches,
missing: esKeys.length - matches,
percent: esKeys.length > 0 ? Math.round((matches / esKeys.length) * 100) : 0,
}];
}));
const articles = esKeys.map(k => {
const es = groups[k].es;
const files = Object.fromEntries(LANGUAGES.map(l => [l, groups[k][l]?.file || null]));
const slugs = Object.fromEntries(LANGUAGES.map(l => [l, groups[k][l]?.slug || null]));
const urls = Object.fromEntries(LANGUAGES.map(l => {
const entry = groups[k][l];
return [l, entry?.slug ? articleUrl(l, entry.slug) : null];
}));
return {
groupId: k,
date: es?.date || '',
spanishTitle: es?.title || '',
spanishFile: es?.file || '',
spanishSlug: es?.slug || '',
spanishUrl: es?.slug ? articleUrl('es', es.slug) : '',
files,
slugs,
urls,
};
});
const orphaned = allKeys.filter(k => !groups[k].es).map(k => {
const langs = Object.keys(groups[k]);
const files = Object.fromEntries(langs.map(l => [l, groups[k][l].file]));
const slugs = Object.fromEntries(langs.map(l => [l, groups[k][l].slug]));
const urls = Object.fromEntries(langs.map(l => {
const entry = groups[k][l];
return [l, entry?.slug ? articleUrl(l, entry.slug) : null];
}));
return {
groupId: k,
languages: langs,
files,
slugs,
urls,
};
});
const payload = {
timestamp: new Date().toISOString(),
source: 'cdrdpyj-postbuild',
site: 'centrodelreinodepazyjusticia.com',
summary,
articles,
orphaned,
totals: {
spanishArticles: esKeys.length,
totalGroups: allKeys.length,
orphanedArticles: orphaned.length,
languages: Object.fromEntries(LANGUAGES.map(l => {
const c = allKeys.filter(k => groups[k][l]).length;
return [l, c];
})),
},
};
const orphanCount = orphaned.length;
if (orphanCount > 0) {
console.log(`[send-to-n8n] ⚠️ ${orphanCount} orphaned article(s) — missing Spanish version`);
orphaned.forEach(o => {
console.log(` ⚠️ ${o.groupId} solo en: ${o.languages.join(', ')}`);
Object.entries(o.urls).forEach(([l, u]) => console.log(` ${l}: ${u}`));
});
}
const url = process.env.N8N_WEBHOOK_URL;
if (!url) {
console.log('[send-to-n8n] N8N_WEBHOOK_URL not set — showing sample payload');
console.log('Summary:', JSON.stringify(payload.summary, null, 2));
console.log('Sample articles (first 5):');
payload.articles.slice(0, 5).forEach(a => {
console.log(` ${a.groupId} | ${a.date} | ${a.spanishTitle.slice(0, 60)}...`);
console.log(` → ES: ${a.spanishUrl}`);
const langs = Object.entries(a.urls).filter(([_, v]) => v).map(([k]) => k).join(', ');
console.log(` → Present in: ${langs || 'ES only'}`);
});
if (orphanCount) {
console.log('Orphaned articles:');
payload.orphaned.forEach(o => {
console.log(` ${o.groupId}`);
Object.entries(o.urls).forEach(([l, u]) => console.log(` ${l}: ${u}`));
});
}
console.log(`[send-to-n8n] Total: ${articles.length} ES articles, ${orphanCount} orphans`);
return;
}
if (process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL && process.env.GOOGLE_SHEET_ID) {
try {
await clearSheet('Resumen');
await clearSheet('Detalle');
} catch (e) {
console.error(`[send-to-n8n] Warning: could not clear sheets (${e.message})`);
}
}
const apiKey = process.env.N8N_API_KEY;
const apiKeyHeader = process.env.N8N_API_KEY_HEADER || 'X-API-Key';
const headers = { 'Content-Type': 'application/json' };
if (apiKey) headers[apiKeyHeader] = apiKey;
fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload),
})
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
console.log(`[send-to-n8n] OK — ${articles.length} ES articles, ${orphanCount} orphans`);
})
.catch(e => {
console.error(`[send-to-n8n] Warning: n8n unreachable (${e.message}) — build continues`);
});
}
main().catch(e => {
console.error(`[send-to-n8n] Fatal error: ${e.message}`);
process.exit(1);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,68 +1,18 @@
---
import { ClientRouter } from "astro:transitions";
import { GoogleAnalytics } from "astro-google-analytics";
const {
title = "Centro del Reino de Paz y Justicia",
description = "",
image = null,
url = null,
date = null,
noindex = false,
description =""
} = Astro.props;
const imageUrl = image ? new URL(image, Astro.site).toString() : null;
const canonicalURL = new URL(url || Astro.url.pathname, Astro.site);
---
<head>
<template>
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<GoogleAnalytics id="G-26KM3HWW9J" />
<title>{title}</title>
<meta property="og:site_name" content={title} />
<meta name="description" content={description} />
<!-- Open Graph -->
{url && <meta property="og:url" content={url} />}
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<link rel="canonical" href={canonicalURL} />
<meta name="telegram:channel" content="@CentroRPJ" />
{imageUrl && <meta property="og:image" content={imageUrl} />}
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
{
date && (
<meta
property="article:published_time"
content={
date instanceof Date
? `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
)}-${String(date.getDate()).padStart(2, "0")}`
: date
}
/>
)
}
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{imageUrl && <meta name="twitter:image" content={imageUrl} />}
{url && <meta name="twitter:url" content={url} />}
</head>
<ClientRouter />
</head>
</template>

View File

@ -2,13 +2,11 @@
const { props } = Astro.props;
import { Icon } from "astro-icon/components";
import Button from "./ui/Button.astro";
import { getLocalizedRoute } from '@/i18n';
const background = props.bgImage || props.bgColor;
const formUrl = `/${Astro.currentLocale}/${getLocalizedRoute("formulario", Astro.currentLocale)}`;
---
<div style={{ background: props.bgImage ? `url('${props.bgImage}') center no-repeat; background-size:contain;` : props.bgColor }} class={`aspect-auto xl:aspect-square py-8 px-8 xl:px-16 flex flex-col justify-evenl`}>
<div style={{ background: props.bgImage ? `url('${props.bgImage}') center/cover no-repeat` : props.bgColor }} class={`aspect-square bg-cover py-12 px-16 flex flex-col justify-evenly`}>
<div class="flex flex-col gap-6">
{props.hasIcon && (
@ -22,14 +20,15 @@ const formUrl = `/${Astro.currentLocale}/${getLocalizedRoute("formulario", Astro
{props.hasButton && (
<div class=" flex self-start mt-3">
<Button class="px-6 py-3 uppercase" url={props.url} variant="primary" title={props.buttonLabel} />
<div class=" flex self-start">
<Button class="px-6 py-3 uppercase" url="#" variant="primary" title={props.buttonLabel} />
</div>
)}
{props.hasInput && (
<div class="flex gap-2 w-full mt-5">
<Button class="px-6 py-3 uppercase bg-[#22523F]" url={formUrl} variant="secondary" title={props.buttonLabel} />
<div class="flex border gap-2 w-full">
<input type="email" placeholder="Correo" class="flex-1 p-2 min-w-0 outline-none"/>
<Button class="px-6 py-3 uppercase" variant="secondary" title={props.buttonLabel} />
</div>
)}
</div>

View File

@ -1,255 +1,25 @@
---
import Button from "./ui/Button.astro";
import { Icon } from "astro-icon/components";
import { getCollection } from "astro:content";
import { createTranslator, routeTranslations } from "../i18n/index.ts";
const allNews = await getCollection("news");
const allEditorial = await getCollection("editorial");
const tl = createTranslator(Astro.currentLocale);
const currentLocale = Astro.currentLocale;
const currentPath = Astro.url.pathname;
const { locale } = Astro.params;
const isAdmin = currentPath.split("/").includes("admin");
const languages = [
{ code: "es", icon: "icon_flag_es", label: "Español" },
{ code: "en", icon: "icon_flag_uk", label: "English" },
{ code: "he", icon: "flagpack--il", label: "עברית" },
{ code: "pt", icon: "flagpack--br", label: "Português" },
{ code: "fr", icon: "flagpack--fr", label: "Français" },
{ code: "ru", icon: "flagpack--ru", label: "Русский" },
{ code: "rw", icon: "flagpack--rw", label: "Kinyarwanda" },
{ code: "kr", icon: "flagpack--ht", label: "Kreole" },
];
const navItems = [
{ href: "#somos", key: "nav.about" },
{ href: "#programs", key: "nav.programs" },
{ href: "#news", key: "nav.news" },
{ href: "#editorial", key: "nav.editorial" },
];
const sidebarNavItems = [
{ href: `/`, key: "nav.home" },
{ href: "#somos", key: "nav.about" },
{ href: "#programs", key: "nav.programs" },
{ href: "#news", key: "nav.news" },
{ href: "#editorial", key: "nav.editorial" },
];
function translatePath(newLocale: string) {
const segments = currentPath.split("/").filter(Boolean);
if (segments.length === 0) return `/${newLocale}`;
const remainingSegments = segments.slice(1);
const allRouteNames = Object.values(routeTranslations).flatMap(r => Object.values(r));
const translatedSegments = remainingSegments.map((segment) => {
for (const key in routeTranslations) {
const translations =
routeTranslations[key as keyof typeof routeTranslations];
if (Object.values(translations).includes(segment)) {
return (
translations[newLocale as keyof typeof translations] ||
segment
);
}
}
return segment;
});
if (segments.length >= 2 && allRouteNames.includes(segments[1])) {
if (segments.length >= 3) {
const currentId = segments[segments.length - 1];
const baseId = currentId.split("/").pop();
const existsInNews = allNews.some(
(post) =>
post.data.slug === baseId && post.data.locale === newLocale,
);
const existsInEditorial = allEditorial.some(
(post) =>
post.data.slug === baseId && post.data.locale === newLocale,
);
if (!existsInNews && !existsInEditorial) return `/${newLocale}/${translatedSegments[0]}`;
return `/${newLocale}/${translatedSegments[0]}/${baseId}`;
}
}
return `/${[newLocale, ...translatedSegments].join("/")}`;
}
---
<div>
<div class="flex justify-between px-8 md:px-0 md:py-4">
<div class="border-l-4 border-colorPrimary pl-4">
<p
class="font-secondary text-colorPrimary font-bold leading-none py-2 text-2xl md:text-lg"
>
<a href={`/${currentLocale}`}
>{tl("nav.logo_line1")}<br />{tl("nav.logo_line2")}</a
>
<div class="flex justify-between py-4">
<div class="border-l-4 border-primary pl-4">
<p class="font-secondary text-primary font-bold leading-none py-2 text-md">
Centro del Reino<br /> De Paz y Justicia
</p>
</div>
<nav
class="flex justify-end md:justify-evenly gap-4 md:gap-6 lg:gap-10 items-center uppercase text-md text-white"
class="flex justify-evenly gap-10 items-center uppercase text-md text-white"
>
<div class="hidden md:flex gap-4 lg:gap-8 font-primary font-bold">
{
navItems.map((item) => (
<a
class="hover:text-colorPrimary transition"
href={`/${currentLocale}${item.href}`}
>
{tl(item.key)}
</a>
))
}
<!-- <a class="hover:text-colorPrimary transition" href={`/${currentLocale}/archive`}>{tl("nav.archive")}</a>
<a class="hover:text-colorPrimary transition" href={`/${currentLocale}/nations`}>{tl("nav.nations")}</a> -->
<div class="flex gap-8">
<a href="#">Inicio</a>
<a href="#">Somos</a>
<a href="#">Programas</a>
<a href="#">Noticias</a>
</div>
<div class="drawer md:hidden">
<input id="my-drawer-1" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<label for="my-drawer-1" class="btn-ghost drawer-button">
<Icon
name="ph:list"
class="text-white text-4xl font-bold"
/>
</label>
</div>
<div class="drawer-side">
<label
for="my-drawer-1"
aria-label="close sidebar"
class="drawer-overlay"></label>
<ul
class="menu min-h-full w-80 p-4 bg-[url(/img/opacity-logo.png)] bg-no-repeat bg-contain bg-center bg-[#22523F] text-white"
>
<div
class="flex gap-2 justify-center items-center mb-8 mt-8"
>
<img
class="w-1/3 object-contain"
src="/img/logo-metalico.webp"
alt="Logo Centro del Reino de Paz y Justicia"
/>
<p
class="font-secondary text-colorPrimary font-bold leading-none py-2 text-lg"
>
<a href="/"
>{tl("nav.logo_line1")}<br />{
tl("nav.logo_line2")
}</a
>
</p>
</div>
<ul
class="font-primary font-bold flex flex-col gap-1 text-lg p-0"
>
{
sidebarNavItems.map((item) => (
<li>
<a
class="hover:text-colorPrimary transition"
href={`/${currentLocale}${item.href}`}
>
{tl(item.key)}
</a>
</li>
))
}
{isAdmin && (
<li>
<form method="POST" action="/api/admin/auth/logout">
<button
type="submit"
class="w-full text-left text-white hover:text-colorPrimary transition cursor-pointer"
>
Salir
</button>
</form>
</li>
)}
</ul>
<div class="w-50">
<Button
class="px-8 py-2 uppercase text-lg mt-8"
title={tl("nav.contact")}
url="#contact"
variant="primary"
/>
</div>
<div class="dropdown mt-10">
<div
tabindex="0"
role="button"
class="btn-ghost m-1 cursor-pointer"
>
<Icon name="ph:translate" class="text-2xl" />
</div>
<ul
tabindex="-1"
class="dropdown-content text-tertiary text-lg bg-colorPrimary menu z-1 w-52 p-2 shadow-sm"
>
{
languages.map((lang) => (
<li>
<a href={translatePath(lang.code)}>
{lang.icon && (
<Icon name={lang.icon} />
)}{" "}
{lang.label}
</a>
</li>
))
}
</ul>
</div>
</ul>
</div>
</div>
<div class="w-50 hidden md:block">
<Button
class="px-4 py-2 uppercase"
title={tl("nav.contact")}
url="#contact"
variant="primary"
/>
</div>
{isAdmin && (
<div class="hidden md:block">
<form method="POST" action="/api/admin/auth/logout">
<button
type="submit"
class="bg-[#22523F] text-[#EBE6D2] border-0 hover:bg-[#EBE6D2]/90 hover:text-tertiary uppercase rounded-none font-bold transition block text-center px-4 py-2 cursor-pointer"
>
Salir
</button>
</form>
</div>
)}
<div class="dropdown dropdown-end md:block hidden">
<div
tabindex="0"
role="button"
class="btn-ghost m-1 cursor-pointer"
>
<Icon name="ph:translate" class="text-2xl" />
</div>
<ul
tabindex="-1"
class="dropdown-content text-tertiary text-lg bg-colorPrimary menu z-1 w-52 p-2 shadow-sm"
>
{
languages.map((lang) => (
<li>
<a href={translatePath(lang.code)}>
{lang.icon && <Icon name={lang.icon} />}{" "}
{lang.label}
</a>
</li>
))
}
</ul>
<Button class="px-8 py-2 uppercase" title="Contacto" url="/contacto" variant="primary" />
</div>
</nav>
</div>

View File

@ -1,43 +1,38 @@
---
import Header from './Header.astro';
import { Icon } from 'astro-icon/components';
import { createTranslator } from '../i18n';
const tl = createTranslator(Astro.currentLocale);
---
<div class="font-secondary">
<div class="top-16 relative mb container mx-auto">
<div class="h-screen container bg-[url(/src/assets/hero-image.webp)] bg-no-repeat bg-contain bg-bottom mx-auto">
<Header />
</div>
<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="grid grid-cols-2 justify-center items-center h-full">
<div class="grid grid-cols-1 gap-8">
<div>
<img
src="/img/logo-metalico.webp"
src="/src/assets/logo-metalico.png"
alt="Logo Metalico"
class="w-20 sm:24 md:w-32 lg:w-58 mb-4 mt-0"
class="w-58 mb-4"
/>
<div class="text-colorPrimary font-semibold font-secondary sm:mb-24 mt-8">
<h1 class="text-3xl md:text-6xl sm:mb-8">
{tl("hero.name")}
</div>
<div class="text-primary font-bold font-primary">
<h1 class="text-3xl">
Dr. José Benjamín<br /> Pérez Matos
</h1>
<div class="flex items-center ">
<Icon name="ph:minus" class="text-xl mr-2" />
<h6 class="font-primary font-light uppercase text-white uppercase">{tl("hero.title")}</h6>
<h6 class="uppercase text-white">Lider Fundador</h6>
</div>
</div>
</div>
<div class="order-first md:order-last flex flex-col md:justify-end items-end gap-8 ">
<div class="grid grid-cols-1 gap-8">
<div
class="md:w-1/2 font-primary font-medium text-2xl text-colorPrimary gap-8 flex flex-col justify-end mb-24"
class="w-1/2 font-primary font-semibold text-2xl text-primary gap-8 flex flex-col"
>
<h2 class="text-[#EAE6D2] text-lg py-8">{tl("hero.body")}</h2>
<div class="w-16 self-end md:mt-64">
<h2>
“El sueño de mi vida es ver cumplida la visión de los
profetas: un mundo de justicia y paz para el bien de
Israel y de toda la humanidad.”
</h2>
<div class="w-32 self-end">
<img
src="/img/white-lion.png"
src="/src/assets/white-dove.webp"
alt="White Dove"
/>
</div>

View File

@ -1,181 +0,0 @@
<script setup>
import { ref} from "vue";
import { Icon } from '@iconify/vue'
const props = defineProps({
locale: String
})
import { createTranslator, t } from "../i18n";
const tl = createTranslator(props.locale);
const loading = ref(false);
const modalRef = ref(null);
const isFormPendiente = ref(true);
const openModal = () => {
modalRef.value?.showModal();
};
const closeModal = () => {
modalRef.value?.close();
};
const handleSubmit = async (e) => {
e.preventDefault();
loading.value = true;
const formData = new FormData(e.target);
try {
const response = await fetch("/api/emailPostulacion/send", {
method: "POST",
body: formData,
});
if (!response.ok) throw new Error();
} catch (error) {
console.error(error);
} finally {
e.target.reset();
loading.value = false;
closeModal();
}
};
</script>
<template>
<template v-if="isFormPendiente">
<button
@click="openModal"
class="btn rounded-none p-7 bg-[#22523F] text-[#EBE6D2] border-0 hover:bg-[#EBE6D2]/90 hover:text-tertiary uppercase text-lg"
>
{{ tl("info.register") }}
</button>
<dialog
ref="modalRef"
class="modal modal-bottom sm:modal-middle p-8"
>
<div class="modal-box bg-[#EBE6D2] text-[#22523F] rounded-none p-8">
<!-- Botón cerrar -->
<button
type="button"
@click="closeModal"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
>
<Icon icon="ph:x" class="text-2xl" />
</button>
<!-- Título -->
<h3 class="text-lg font-bold lg:text-2xl font-secondary text-center uppercase">
{{ tl("info.modal.title") }}
</h3>
<!-- Texto -->
<p class="text-lg font-primary text-center mt-10">
{{ tl("info.modal.text") }}
</p>
</div>
</dialog>
</template>
<template v-else>
<button
@click="openModal"
class="btn rounded-none p-7 bg-[#22523F] text-[#EBE6D2] border-0 hover:bg-[#EBE6D2]/90 hover:text-tertiary uppercase text-lg"
>
{{ tl("info.register") }}
</button>
<dialog ref="modalRef" id="my_modal_5" class="modal modal-bottom sm:modal-middle p-8">
<div class="modal-box bg-[#EBE6D2] text-[#22523F] rounded-none p-8">
<form method="dialog">
<button
v-on:click="closeModal"
type="button"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
>
<Icon icon="ph:x" class="w-6 h-6" />
</button>
</form>
<h3 class="text-lg font-bold lg:text-2xl font-secondary text-center">
Formulario de Registro
</h3>
<div class="form py-4 mt-6 ">
<form
id="voluteenFrom"
action="/api/emailPostulacion/send"
method="post"
@submit="handleSubmit"
class="flex flex-col gap-2 place-items-center">
<label class="input rounded-none bg-white">
<span>Nombres <span class="text-error">*</span></span>
<input name="nombres" type="text" placeholder="Nombres"
class="w-full max-w-xs rounded-none invalid:border-error focus:invalid:border-error"
required />
</label>
<label class="input rounded-none bg-white">
<span>Apellidos <span class="text-error">*</span></span>
<input name="apellidos" type="text" placeholder="Apellidos"
class="w-full max-w-xs rounded-none invalid:border-error focus:invalid:border-error"
required />
</label>
<label class="input rounded-none bg-white">
<span>País</span>
<input name="pais" type="text" placeholder="Pais"
class="w-full max-w-xs rounded-none" />
</label>
<label class="input rounded-none bg-white">
<span>Lugar</span>
<input name="lugar" type="text" placeholder="Lugar"
class="w-full max-w-xs rounded-none" />
</label>
<label class="input rounded-none bg-white">
<span>Dirección</span>
<input name="direccion" type="text" placeholder="Dirección"
class="w-full max-w-xs rounded-none" />
</label>
<label class="input rounded-none bg-white">
<span>Teléfono <span class="text-error">*</span></span>
<input name="telefono" type="text" placeholder="Teléfono"
class="w-full max-w-xs rounded-none invalid:border-error focus:invalid:border-error"
required />
</label>
<label class="input validator rounded-none bg-white">
<span>Email <span class="text-error">*</span></span>
<input name="email" type="email"
placeholder="Ingresa Correo Electrónico"
class="w-full max-w-xs rounded-none invalid:border-error focus:invalid:border-error"
required />
</label>
<button
type="submit"
:disabled="loading"
class="btn mt-4 bg-[#22523F] text-[#EBE6D2] border-0 hover:bg-[#EBE6D2]/90 hover:text-tertiary uppercase rounded-none">
{{ loading ? "Enviando..." : "Enviar" }}
</button>
</form>
</div>
</div>
</dialog>
</template>
</template>

View File

@ -1,108 +0,0 @@
<template>
<!-- DESKTOP -->
<div class="hidden md:block fixed left-0 top-1/2 -translate-y-1/2 z-[9999]">
<div
class="relative transition-all duration-300 ease-out"
:class="desktopOpen ? 'translate-x-0' : '-translate-x-[70%]'"
>
<div
class="absolute right-[-14px] top-1/2 -translate-y-1/2 bg-white shadow-md rounded-r-xl px-1 py-4 border border-gray-200 cursor-pointer"
@click="desktopOpen = !desktopOpen"
>
<div class="w-1 h-6 bg-gray-400 rounded-full"></div>
</div>
<ul class="flex flex-col gap-4 bg-white/90 backdrop-blur-md py-4 px-4 rounded-r-2xl shadow-xl border border-gray-200">
<li class="border-b pb-3">
<a :href="twitterUrl" target="_blank">
<Icon icon="ph:x-logo-thin" class="text-2xl text-black" />
</a>
</li>
<li class="border-b pb-3">
<a :href="facebookUrl" target="_blank">
<Icon icon="ph:facebook-logo-thin" class="text-2xl text-black" />
</a>
</li>
<li class="border-b pb-3">
<a :href="whatsappUrl" target="_blank">
<Icon icon="ph:whatsapp-logo-thin" class="text-2xl text-black" />
</a>
</li>
<li class="border-b pb-3">
<a :href="linkedinUrl" target="_blank">
<Icon icon="ph:linkedin-logo-thin" class="text-2xl text-black" />
</a>
</li>
<li>
<button @click="copyLink" class="cursor-pointer">
<Icon :icon="copied ? 'ph:check-thin' : 'ph:link-thin'" class="text-2xl text-black" />
</button>
</li>
</ul>
</div>
</div>
<div class="md:hidden fixed bottom-6 right-6 z-[9999]">
<div class="flex flex-col items-end gap-2 mb-3">
<transition name="fade">
<div
v-if="open"
class="flex flex-col gap-3 bg-white/90 backdrop-blur-md p-3 rounded-2xl shadow-xl border border-gray-200"
>
<a :href="twitterUrl" target="_blank" class="text-gray-600 hover:text-black transition border-b pb-3">
<Icon icon="ph:x-logo-thin" class="text-2xl" />
</a>
<a :href="facebookUrl" target="_blank" class="text-gray-600 hover:text-blue-600 transition border-b pb-3">
<Icon icon="ph:facebook-logo-thin" class="text-2xl" />
</a>
<a :href="whatsappUrl" target="_blank" class="text-gray-600 hover:text-green-600 transition border-b pb-3">
<Icon icon="ph:whatsapp-logo-thin" class="text-2xl" />
</a>
<a :href="linkedinUrl" target="_blank" class="text-gray-600 hover:text-blue-700 transition border-b pb-3">
<Icon icon="ph:linkedin-logo-thin" class="text-2xl" />
</a>
<button @click="copyLink" class="text-gray-600 hover:text-emerald-600 transition">
<Icon :icon="copied ? 'ph:check-thin' : 'ph:link-thin'" class="text-2xl" />
</button>
</div>
</transition>
</div>
<!-- FAB PRINCIPAL -->
<button
@click="toggle"
class="bg-white/90 backdrop-blur-md border border-gray-200 shadow-xl rounded-full p-3 text-gray-700 hover:text-black transition"
>
<Icon :icon="open ? 'ph:x-thin' : 'ph:share-network-thin'" class="text-2xl" />
</button>
</div>
</template>
<script setup>
import { ref, computed } from "vue"
import { Icon } from "@iconify/vue"
const props = defineProps({ url: { type: String, required: true } })
const open = ref(false)
const toggle = () => (open.value = !open.value)
const desktopOpen = ref(false) // controla el panel desktop
const copied = ref(false)
const copyLink = async () => {
try {
await navigator.clipboard.writeText(props.url)
copied.value = true
setTimeout(() => (copied.value = false), 1500)
} catch {
console.error("No se pudo copiar")
}
}
const fullUrl = computed(() => window.location.href)
const encodedUrl = computed(() => encodeURIComponent(fullUrl.value))
const twitterUrl = computed(() => `https://twitter.com/intent/tweet?url=${encodedUrl.value}`)
const facebookUrl = computed(() => `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl.value}`)
const whatsappUrl = computed(() => `https://api.whatsapp.com/send?text=${encodedUrl.value}`)
const linkedinUrl = computed(() => `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl.value}`)
</script>

View File

@ -1,210 +0,0 @@
<template>
<div class="space-y-8">
<div v-if="loading" class="flex justify-center py-16">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<div v-else-if="error" class="text-center py-10 text-error">{{ error }}</div>
<template v-else-if="stats">
<!-- Resumen -->
<section>
<h3 class="text-xs font-bold text-tertiary uppercase tracking-wide flex items-center gap-2 mb-3">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Resumen
</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="stat bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box p-4">
<div class="stat-title text-tertiary/70 text-xs font-medium">Total voluntarios</div>
<div class="stat-value text-tertiary font-primary">{{ fmt(stats.total) }}</div>
</div>
<div class="stat bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box p-4">
<div class="stat-title text-tertiary/70 text-xs font-medium">Edad promedio</div>
<div class="stat-value text-tertiary font-primary">{{ stats.edad.promedio ?? "—" }} años</div>
</div>
<div class="stat bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box p-4">
<div class="stat-title text-tertiary/70 text-xs font-medium">Femenino</div>
<div class="stat-value text-tertiary font-primary">{{ fmt(sexo.f) }}</div>
<div class="stat-desc text-tertiary/60">{{ pct(sexo.f, sexo.total) }}% del total</div>
</div>
<div class="stat bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box p-4">
<div class="stat-title text-tertiary/70 text-xs font-medium">Masculino</div>
<div class="stat-value text-tertiary font-primary">{{ fmt(sexo.m) }}</div>
<div class="stat-desc text-tertiary/60">{{ pct(sexo.m, sexo.total) }}% del total</div>
</div>
</div>
</section>
<!-- Idiomas -->
<section>
<h3 class="text-xs font-bold text-tertiary uppercase tracking-wide flex items-center gap-2 mb-3">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Idiomas
</h3>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<div
v-for="lang in stats.idiomas"
:key="lang.value"
class="stat bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box p-4"
>
<div class="stat-title text-tertiary/70 text-xs font-medium">{{ lang.label }}</div>
<div class="stat-value text-tertiary font-primary text-2xl">{{ fmt(lang.count) }}</div>
<div class="stat-desc text-tertiary/60">{{ pct(lang.count, stats.total) }}% del total</div>
</div>
</div>
</section>
<!-- Distribuciones -->
<section>
<h3 class="text-xs font-bold text-tertiary uppercase tracking-wide flex items-center gap-2 mb-3">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Distribuciones
</h3>
<div class="grid md:grid-cols-2 gap-4">
<div class="card bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box">
<div class="card-body p-4 sm:p-5">
<h4 class="card-title text-tertiary text-sm font-bold">Áreas de colaboración</h4>
<div class="space-y-2">
<div v-for="item in topAreas" :key="item.value" class="flex items-center gap-3">
<span class="w-32 sm:w-44 shrink-0 text-sm text-tertiary truncate" :title="item.label">{{ item.label }}</span>
<progress class="progress progress-secondary flex-1" :value="item.count" :max="areasMax"></progress>
<span class="w-12 shrink-0 text-right text-sm font-semibold text-tertiary">{{ fmt(item.count) }}</span>
</div>
</div>
</div>
</div>
<div class="card bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box">
<div class="card-body p-4 sm:p-5">
<h4 class="card-title text-tertiary text-sm font-bold">Días disponibles</h4>
<div class="space-y-2">
<div v-for="item in stats.dias" :key="item.value" class="flex items-center gap-3">
<span class="w-32 sm:w-44 shrink-0 text-sm text-tertiary truncate" :title="item.label">{{ item.label }}</span>
<progress class="progress progress-secondary flex-1" :value="item.count" :max="diasMax"></progress>
<span class="w-12 shrink-0 text-right text-sm font-semibold text-tertiary">{{ fmt(item.count) }}</span>
</div>
</div>
</div>
</div>
<div class="card bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box">
<div class="card-body p-4 sm:p-5">
<h4 class="card-title text-tertiary text-sm font-bold">Horario preferido</h4>
<div class="space-y-2">
<div v-for="item in stats.horarios" :key="item.value" class="flex items-center gap-3">
<span class="w-32 sm:w-44 shrink-0 text-sm text-tertiary truncate" :title="item.label">{{ item.label }}</span>
<progress class="progress progress-secondary flex-1" :value="item.count" :max="horariosMax"></progress>
<span class="w-12 shrink-0 text-right text-sm font-semibold text-tertiary">{{ fmt(item.count) }}</span>
</div>
</div>
</div>
</div>
<div class="card bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box">
<div class="card-body p-4 sm:p-5">
<h4 class="card-title text-tertiary text-sm font-bold">Países</h4>
<div class="space-y-2">
<div v-for="item in stats.paises" :key="item.value" class="flex items-center gap-3">
<span class="w-32 sm:w-44 shrink-0 text-sm text-tertiary truncate" :title="item.value">{{ item.value }}</span>
<progress class="progress progress-secondary flex-1" :value="item.count" :max="paisesMax"></progress>
<span class="w-12 shrink-0 text-right text-sm font-semibold text-tertiary">{{ fmt(item.count) }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Disponibilidad y salud -->
<section>
<h3 class="text-xs font-bold text-tertiary uppercase tracking-wide flex items-center gap-2 mb-3">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Disponibilidad y salud
</h3>
<div class="card bg-colorPrimary ring-1 ring-colorSecondary/40 rounded-box">
<div class="card-body p-4 sm:p-6 grid md:grid-cols-2 gap-x-8 gap-y-5">
<div v-for="m in condMetrics" :key="m.title" class="space-y-1.5">
<div class="flex justify-between text-sm text-tertiary">
<span class="font-medium">{{ m.title }}</span>
<span> {{ m.pct }}% · No {{ 100 - m.pct }}%</span>
</div>
<progress class="progress progress-secondary w-full" :value="m.si" :max="m.total"></progress>
<p class="text-xs text-tertiary/60">
: {{ fmt(m.si) }} · No: {{ fmt(m.no) }}
</p>
</div>
<div class="space-y-1.5">
<div class="flex justify-between text-sm text-tertiary">
<span class="font-medium">Sexo</span>
<span>F {{ pct(sexo.f, sexo.total) }}% · M {{ pct(sexo.m, sexo.total) }}%</span>
</div>
<div class="flex w-full h-3 rounded-none overflow-hidden bg-tertiary/15">
<div class="bg-tertiary h-full" :style="{ width: pct(sexo.f, sexo.total) + '%' }"></div>
<div class="bg-colorSecondary h-full" :style="{ width: pct(sexo.m, sexo.total) + '%' }"></div>
</div>
<p class="text-xs text-tertiary/60">
F: {{ fmt(sexo.f) }} · M: {{ fmt(sexo.m) }}
</p>
</div>
</div>
</div>
</section>
</template>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { cachedFetch } from "../../lib/adminCache";
const loading = ref(false);
const error = ref("");
const stats = ref(null);
const fmt = (n) => Number(n || 0).toLocaleString("es");
const pct = (n, total) => (total ? Math.round(((n || 0) / total) * 100) : 0);
const sexo = computed(() => {
const s = stats.value?.sexo || [];
const f = s.find((x) => x.value === "F")?.count || 0;
const m = s.find((x) => x.value === "M")?.count || 0;
return { f, m, total: f + m };
});
const topAreas = computed(() => (stats.value?.areas || []).slice(0, 8));
const areasMax = computed(() => topAreas.value[0]?.count || 1);
const diasMax = computed(() => stats.value?.dias[0]?.count || 1);
const horariosMax = computed(() => stats.value?.horarios[0]?.count || 1);
const paisesMax = computed(() => stats.value?.paises[0]?.count || 1);
const condMetrics = computed(() => {
const c = stats.value?.condiciones;
if (!c) return [];
const mk = (title, key) => {
const si = c[key].si || 0;
const no = c[key].no || 0;
const total = si + no;
return { title, si, no, total, pct: total ? Math.round((si / total) * 100) : 0 };
};
return [
mk("Disponible fuera de ciudad", "fuera_ciudad"),
mk("Misiones internacionales", "misiones_internacionales"),
mk("Condición médica", "condicion_medica"),
mk("Voluntariado anterior", "voluntariado_anterior"),
];
});
async function fetchStats() {
loading.value = true;
error.value = "";
try {
const res = await cachedFetch("/api/admin/voluntarios/stats", { ttl: 60 });
const json = await res.json();
if (!json.success) throw new Error(json.message || "Error al cargar estadísticas");
stats.value = json;
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
onMounted(fetchStats);
</script>

View File

@ -1,179 +0,0 @@
<template>
<div ref="rootEl" class="relative">
<div
class="combobox-input input input-sm flex flex-wrap items-center gap-1 w-full cursor-text"
@click="focusInput"
>
<template v-if="multiple">
<span
v-for="item in currentValue"
:key="item"
class="badge badge-sm badge-neutral gap-1"
>
{{ item }}
<button type="button" class="btn btn-ghost btn-xs btn-circle" @click.stop="toggle(item)"></button>
</span>
</template>
<input
ref="inputEl"
v-model="query"
type="text"
:placeholder="placeholder"
class="flex-1 min-w-[4rem]"
@input="onInput"
@focus="open = true"
/>
</div>
<ul
v-if="open"
class="absolute z-30 mt-1 w-full max-h-64 overflow-y-auto bg-base-100 rounded-box shadow ring-1 ring-base-300"
@mousedown.prevent
>
<li v-if="loading" class="px-3 py-2 text-sm text-base-content/50 flex items-center gap-2">
<span class="loading loading-spinner loading-xs"></span> Cargando
</li>
<li v-else-if="options.length === 0" class="px-3 py-2 text-sm text-base-content/50">
{{ multiple ? "Sin opciones" : "Sin sugerencias — sigue escribiendo" }}
</li>
<li v-for="opt in options" :key="opt.value">
<label
v-if="multiple"
class="flex items-center gap-2 px-3 py-1.5 hover:bg-base-200 cursor-pointer"
>
<input
type="checkbox"
class="checkbox checkbox-xs"
:checked="isSelected(opt.value)"
@change="toggle(opt.value)"
/>
<span class="flex-1 text-sm">
{{ opt.value }}
<span class="text-xs text-base-content/50">({{ opt.count }})</span>
</span>
</label>
<button
v-else
type="button"
class="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-sm hover:bg-base-200"
@click="pick(opt.value)"
>
<span class="truncate">{{ opt.value }}</span>
<span class="text-xs text-base-content/50 shrink-0">({{ opt.count }})</span>
</button>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { cachedFetch } from "../../lib/adminCache";
const props = defineProps({
field: { type: String, required: true },
placeholder: { type: String, default: "Escribir…" },
multiple: { type: Boolean, default: false },
limit: { type: Number, default: 25 },
});
const modelValue = defineModel({ type: [String, Array], default: "" });
const rootEl = ref(null);
const inputEl = ref(null);
const query = ref("");
const open = ref(false);
const loading = ref(false);
const options = ref([]);
let fetchTimer = null;
let fetchVersion = 0;
const currentValue = () => {
const v = modelValue.value;
return Array.isArray(v) ? v : typeof v === "string" ? v : "";
};
function focusInput() {
inputEl.value?.focus();
}
function isSelected(v) {
return currentValue().includes(v);
}
function toggle(v) {
const arr = [...currentValue()];
const i = arr.indexOf(v);
if (i >= 0) arr.splice(i, 1);
else arr.push(v);
modelValue.value = arr;
query.value = "";
scheduleFetch();
}
function pick(v) {
modelValue.value = v;
query.value = v;
open.value = false;
}
function onInput() {
if (!props.multiple) modelValue.value = query.value;
scheduleFetch();
}
async function fetchOptions() {
loading.value = true;
const version = ++fetchVersion;
const params = new URLSearchParams();
params.set("field", props.field);
params.set("limit", String(props.limit));
if (query.value.trim().length >= 2) params.set("q", query.value.trim());
try {
const res = await cachedFetch(`/api/admin/voluntarios/distinct?${params.toString()}`, { ttl: 300 });
const json = await res.json();
if (version !== fetchVersion) return;
options.value = json.success ? json.data : [];
} catch {
if (version === fetchVersion) options.value = [];
} finally {
if (version === fetchVersion) loading.value = false;
}
}
function scheduleFetch() {
clearTimeout(fetchTimer);
fetchTimer = setTimeout(fetchOptions, 250);
}
watch(
() => modelValue.value,
(val) => {
if (!props.multiple && query.value !== val) query.value = typeof val === "string" ? val : "";
}
);
function onDocClick(e) {
if (rootEl.value && !rootEl.value.contains(e.target)) open.value = false;
}
onMounted(() => {
document.addEventListener("mousedown", onDocClick);
scheduleFetch();
});
onBeforeUnmount(() => {
document.removeEventListener("mousedown", onDocClick);
clearTimeout(fetchTimer);
});
</script>
<style scoped>
.combobox-input {
height: auto;
min-height: 2rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
</style>

View File

@ -1,285 +0,0 @@
<template>
<div class="space-y-4">
<div v-if="loading" class="flex justify-center py-16">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<div v-else-if="error" class="text-center py-10 text-error">{{ error }}</div>
<div v-else-if="data" class="space-y-6">
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
<h3 class="text-lg sm:text-xl font-bold">
{{ data.nombre }} {{ data.segundo_nombre }} {{ data.apellido }}
</h3>
<span class="font-mono text-sm opacity-70">#{{ data.numero_voluntario }}</span>
<span class="badge badge-sm" :class="badgeClass(data.status)">{{ data.status }}</span>
</div>
<section
v-for="sec in sectionsWithData"
:key="sec.key"
class="rounded-box ring-1 ring-base-300 bg-base-100/60 overflow-hidden"
>
<h4 class="font-medium text-sm uppercase tracking-wide px-4 py-3 border-b border-base-200">
{{ sec.title }}
</h4>
<dl class="divide-y divide-base-200/70 text-sm">
<div
v-for="f in sec.fields"
:key="f.key"
class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1"
>
<dt class="text-base-content/60">{{ f.label }}</dt>
<dd class="min-w-0 break-words">
<div v-if="f.isList" :class="f.isLongList ? 'space-y-1.5' : 'flex flex-wrap gap-1.5'">
<div
v-if="f.isLongList"
v-for="(item, i) in f.displayValues"
:key="'list-' + i"
class="text-sm border-l-2 border-base-300 pl-2"
>
{{ item }}
</div>
<span
v-else
v-for="(item, i) in f.displayValues"
:key="'chip-' + i"
class="badge badge-sm badge-ghost border border-base-300 whitespace-normal text-left"
>
{{ item }}
</span>
</div>
<span v-else>{{ f.displayText }}</span>
</dd>
</div>
</dl>
</section>
<section
v-if="extraRows.length"
class="rounded-box ring-1 ring-base-300 bg-base-100/60 overflow-hidden"
>
<h4 class="font-medium text-sm uppercase tracking-wide px-4 py-3 border-b border-base-200">
Otros datos
</h4>
<dl class="divide-y divide-base-200/70 text-sm">
<div
v-for="r in extraRows"
:key="r.rawKey"
class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1"
>
<dt class="text-base-content/60">{{ r.key }}</dt>
<dd class="min-w-0 break-words">{{ r.value }}</dd>
</div>
</dl>
</section>
<section class="rounded-box ring-1 ring-base-300 bg-base-100/60 overflow-hidden">
<h4 class="font-medium text-sm uppercase tracking-wide px-4 py-3 border-b border-base-200">
Sistema
</h4>
<dl class="divide-y divide-base-200/70 text-sm">
<div class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1">
<dt class="text-base-content/60">ID</dt>
<dd class="font-mono break-all">{{ data.id }}</dd>
</div>
<div class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1">
<dt class="text-base-content/60">Versión de formulario</dt>
<dd>{{ data.form_version || "—" }}</dd>
</div>
<div class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1">
<dt class="text-base-content/60">Correo enviado</dt>
<dd>{{ formatDateTime(data.email_sent_at) }}</dd>
</div>
<div class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1">
<dt class="text-base-content/60">Registrado</dt>
<dd>{{ formatDateTime(data.submitted_at) }}</dd>
</div>
<div class="px-4 py-2.5 grid sm:grid-cols-[220px_1fr] gap-x-4 gap-y-1">
<dt class="text-base-content/60">Actualizado</dt>
<dd>{{ formatDateTime(data.updated_at) }}</dd>
</div>
</dl>
</section>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { cachedFetch } from "../../lib/adminCache";
const props = defineProps({
numero: { type: [Number, String], required: true },
});
const CORE_COLUMN_MAP = {
nombre: "nombre",
segundo_nombre: "segundo_nombre",
apellido: "apellido",
correo: "correo",
fecha_nacimiento: "fecha_nacimiento",
nacionalidad: "nacionalidad",
sexo: "sexo",
direccion_completa: "direccion",
ciudad: "ciudad",
estado: "estado",
pais: "pais",
postal: "codigo_postal",
telefono: "telefono",
whatsapp: "whatsapp",
profesion: "profesion",
lugar_trabajo_actual: "lugar_trabajo",
nivel_academico: "nivel_academico",
};
const loading = ref(false);
const error = ref("");
const data = ref(null);
const sections = ref([]);
function pretty(v) {
if (v === "si") return "Sí";
if (v === "no") return "No";
return v;
}
function fallbackLabel(key) {
if (key.startsWith("idioma_nivel_")) return `Nivel de ${key.slice("idioma_nivel_".length)}`;
if (key === "locale") return "Idioma de la solicitud";
return key;
}
const respuestasByKey = computed(() => {
const map = {};
for (const r of data.value?.respuestas || []) {
(map[r.key] ||= []).push(r.value);
}
return map;
});
function fieldTokens(field) {
let values;
if (field.key === "documentos") {
const s = [data.value.documento_tipo, data.value.documento_nro].filter(Boolean).join(" ");
values = s ? [s] : [];
} else if (CORE_COLUMN_MAP[field.key]) {
const v = data.value[CORE_COLUMN_MAP[field.key]];
values = v === null || v === undefined || v === "" ? [] : [String(v)];
} else {
values = respuestasByKey.value[field.key] || [];
}
const tokens = [];
for (const raw of values) {
for (const part of String(raw).split(",")) {
const tok = part.trim();
if (!tok) continue;
const opt = (field.options || []).find((o) => o.value === tok);
let display = (opt ? opt.label : pretty(tok)).replace(/<[^>]*>/g, "");
if (field.tipo === "date" || field.key === "fecha_nacimiento") {
const d = new Date(display);
if (!isNaN(d)) {
display = d.toLocaleDateString("es", { year: "numeric", month: "2-digit", day: "2-digit" });
}
}
tokens.push(display);
}
}
return tokens;
}
const sectionsWithData = computed(() => {
if (!data.value) return [];
return sections.value
.map((sec) => ({
...sec,
fields: sec.fields
.map((f) => {
const tokens = fieldTokens(f);
if (!tokens.length) return null;
return {
...f,
isList: f.tipo === "checkbox" && tokens.length > 1,
isLongList: f.tipo === "checkbox" && tokens.length > 1 && tokens.some((t) => t.length > 40),
displayValues: tokens,
displayText: tokens.join(", "),
};
})
.filter(Boolean),
}))
.filter((sec) => sec.fields.length);
});
const extraRows = computed(() => {
if (!data.value) return [];
const known = new Set();
for (const sec of sections.value) for (const f of sec.fields) known.add(f.key);
const rows = [];
for (const [key, vals] of Object.entries(respuestasByKey.value)) {
if (known.has(key)) continue;
const tokens = [];
for (const raw of vals) {
for (const part of String(raw).split(",")) {
const t = part.trim();
if (t) tokens.push(pretty(t));
}
}
if (!tokens.length) continue;
rows.push({ key: fallbackLabel(key), rawKey: key, value: tokens.join(", ") });
}
return rows;
});
async function fetchData() {
loading.value = true;
error.value = "";
try {
const res = await cachedFetch(`/api/admin/voluntarios/${props.numero}`, { ttl: 60 });
const json = await res.json();
if (!json.success) throw new Error(json.message || "Voluntario no encontrado");
data.value = json.data;
sections.value = json.form.sections;
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
watch(
() => props.numero,
() => {
data.value = null;
fetchData();
}
);
function formatDateTime(iso) {
if (!iso) return "—";
const d = new Date(iso);
if (isNaN(d)) return "—";
return d.toLocaleString("es", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function badgeClass(s) {
switch (s) {
case "aprobado":
return "badge-success";
case "rechazado":
return "badge-error";
case "legacy":
return "badge-warning";
default:
return "badge-neutral";
}
}
onMounted(fetchData);
</script>

View File

@ -1,605 +0,0 @@
<template>
<div class="space-y-4">
<!-- Filtros -->
<div class="bg-colorPrimary border-l-4 border-colorSecondary p-4 rounded-box space-y-3 shadow-lg shadow-tertiary/20">
<div class="flex flex-col md:flex-row gap-3 items-end">
<div class="flex-1 w-full">
<label class="label py-0 text-xs font-medium">Buscar</label>
<input
v-model="search"
type="text"
placeholder="Nombre, correo, teléfono, documento…"
class="input input-bordered input-sm w-full"
@input="onSearch"
/>
</div>
<div class="w-full md:w-52">
<label class="label py-0 text-xs font-medium">Status</label>
<details class="dropdown w-full">
<summary class="btn btn-sm btn-outline w-full justify-between">
<span class="truncate">{{ statusLabel }}</span>
<span class="opacity-60"></span>
</summary>
<ul class="menu dropdown-content bg-base-100 rounded-box shadow ring-1 ring-base-300 z-20 w-56 max-h-64 overflow-y-auto">
<li v-for="s in statuses" :key="s">
<label class="flex items-center gap-2 py-1.5">
<input
type="checkbox"
:value="s"
v-model="selectedStatuses"
class="checkbox checkbox-xs"
@change="applyFilters"
/>
<span>{{ s }}</span>
</label>
</li>
</ul>
</details>
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm text-tertiary" @click="clearFilters">Limpiar</button>
<button class="btn btn-primary btn-sm" @click="showFilters = !showFilters">
{{ showFilters ? "Ocultar filtros" : "Filtros" }}
<span v-if="activeFilterCount" class="badge badge-sm bg-tertiary text-colorPrimary border-0">{{ activeFilterCount }}</span>
</button>
</div>
</div>
<!-- Chips de filtros activos -->
<div v-if="activeChips.length" class="flex flex-wrap gap-1.5">
<span
v-for="chip in activeChips"
:key="chip.key"
class="badge badge-sm bg-white gap-1 border border-colorSecondary/60 text-tertiary"
>
{{ chip.label }}
<button type="button" class="opacity-60 hover:opacity-100" @click="chip.remove()"></button>
</span>
</div>
<!-- Panel de filtros -->
<div v-if="showFilters" class="border-t border-colorSecondary/40 pt-3 space-y-4">
<div>
<p class="text-xs font-bold text-tertiary mb-2 uppercase tracking-wide flex items-center gap-2">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Fecha y edad
</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div>
<label class="label py-0 text-xs font-medium">Desde</label>
<input v-model="desde" type="date" class="input input-bordered input-sm w-full" @change="applyFilters" />
</div>
<div>
<label class="label py-0 text-xs font-medium">Hasta</label>
<input v-model="hasta" type="date" class="input input-bordered input-sm w-full" @change="applyFilters" />
</div>
<div>
<label class="label py-0 text-xs font-medium">Edad mín</label>
<input v-model="edadMin" type="number" min="0" max="120" class="input input-bordered input-sm w-full" @change="applyFilters" />
</div>
<div>
<label class="label py-0 text-xs font-medium">Edad máx</label>
<input v-model="edadMax" type="number" min="0" max="120" class="input input-bordered input-sm w-full" @change="applyFilters" />
</div>
</div>
</div>
<div>
<p class="text-xs font-bold text-tertiary mb-2 uppercase tracking-wide flex items-center gap-2">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Ubicación
</p>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div>
<label class="label py-0 text-xs font-medium">País</label>
<SearchableCombobox
v-model="selectedPaises"
field="pais"
multiple
placeholder="Buscar país…"
@update:modelValue="applyFilters"
/>
</div>
<div>
<label class="label py-0 text-xs font-medium">Nacionalidad</label>
<SearchableCombobox
v-model="nacionalidad"
field="nacionalidad"
placeholder="Buscar…"
@update:modelValue="onSearch"
/>
</div>
<div>
<label class="label py-0 text-xs font-medium">Sexo</label>
<select v-model="sexo" class="select select-bordered select-sm w-full" @change="applyFilters">
<option value="">Todos</option>
<option value="M">M</option>
<option value="F">F</option>
</select>
</div>
<div>
<label class="label py-0 text-xs font-medium">Ciudad</label>
<SearchableCombobox
v-model="coreText.ciudad"
field="ciudad"
placeholder="Buscar…"
@update:modelValue="onSearch"
/>
</div>
<div>
<label class="label py-0 text-xs font-medium">Estado</label>
<SearchableCombobox
v-model="coreText.estado"
field="estado"
placeholder="Buscar…"
@update:modelValue="onSearch"
/>
</div>
</div>
</div>
<div>
<p class="text-xs font-bold text-tertiary mb-2 uppercase tracking-wide flex items-center gap-2">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Perfil
</p>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div>
<label class="label py-0 text-xs font-medium">Profesión</label>
<SearchableCombobox
v-model="coreText.profesion"
field="profesion"
placeholder="Buscar…"
@update:modelValue="onSearch"
/>
</div>
<div>
<label class="label py-0 text-xs font-medium">Nivel académico</label>
<SearchableCombobox
v-model="coreText.nivel_academico"
field="nivel_academico"
placeholder="Buscar…"
@update:modelValue="onSearch"
/>
</div>
<div>
<label class="label py-0 text-xs font-medium">Documento</label>
<input v-model="coreText.documento_nro" type="text" class="input input-bordered input-sm w-full" @input="onSearch" />
</div>
<div>
<label class="label py-0 text-xs font-medium">Versión de formulario</label>
<select v-model="formVersion" class="select select-bordered select-sm w-full" @change="applyFilters">
<option value="">Todas</option>
<option v-for="f in formVersions" :key="f" :value="f">{{ f }}</option>
</select>
</div>
</div>
</div>
<div>
<p class="text-xs font-bold text-tertiary mb-2 uppercase tracking-wide flex items-center gap-2">
<span class="inline-block w-1 h-3.5 bg-colorSecondary"></span>Preferencias y condiciones
</p>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div v-for="f in responseFields" :key="f">
<label class="label py-0 text-xs font-medium">{{ respLabels[f] }}</label>
<select v-model="respFilters[f]" class="select select-bordered select-sm w-full" @change="applyFilters">
<option value="">Todos</option>
<option v-for="o in respFilterOptions[f]" :key="o" :value="o">{{ o }}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla -->
<div class="overflow-x-auto bg-base-100 rounded-box shadow ring-1 ring-base-300">
<table class="table table-sm">
<thead>
<tr class="text-xs uppercase">
<th class="cursor-pointer select-none" @click="toggleSort('numero_voluntario')">#</th>
<th class="cursor-pointer select-none" @click="toggleSort('nombre')">Nombre completo</th>
<th>Correo</th>
<th>Teléfono</th>
<th class="cursor-pointer select-none" @click="toggleSort('pais')">País</th>
<th>Ciudad</th>
<th>Idiomas</th>
<th>Áreas</th>
<th>Status</th>
<th class="cursor-pointer select-none" @click="toggleSort('submitted_at')">Fecha</th>
<th class="w-20 text-right">Acción</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="11" class="text-center py-10">
<span class="loading loading-spinner loading-md text-primary"></span>
</td>
</tr>
<tr v-else-if="error">
<td colspan="11" class="text-center py-10 text-error">{{ error }}</td>
</tr>
<tr v-else-if="rows.length === 0">
<td colspan="11" class="text-center py-10 text-base-content/40">Sin resultados</td>
</tr>
<tr v-for="row in rows" :key="row.id" class="hover:bg-base-200/50 cursor-pointer" @click="openDetail(row)">
<td class="font-mono">{{ row.numero_voluntario }}</td>
<td class="font-medium whitespace-nowrap">{{ row.nombre_completo }}</td>
<td class="max-w-[220px] truncate">{{ row.correo }}</td>
<td class="whitespace-nowrap">{{ row.telefono || "—" }}</td>
<td class="whitespace-nowrap">{{ row.pais || "—" }}</td>
<td class="whitespace-nowrap">{{ row.ciudad || "—" }}</td>
<td class="max-w-[200px] truncate" :title="row.idioma">{{ row.idioma || "—" }}</td>
<td class="max-w-[200px] truncate" :title="row.areas_colaborar">{{ row.areas_colaborar || "—" }}</td>
<td>
<span class="badge badge-sm" :class="badgeClass(row.status)">{{ row.status || "pendiente" }}</span>
</td>
<td class="whitespace-nowrap">{{ formatDate(row.submitted_at) }}</td>
<td class="text-right">
<button class="btn btn-ghost btn-xs" title="Ver detalle" @click.stop="openDetail(row)">Ver</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Paginación -->
<div class="flex items-center justify-between flex-wrap gap-3">
<p class="text-sm text-amber-200">
{{ total }} registros · página {{ page }} de {{ totalPages || 1 }}
</p>
<div class="join">
<button class="join-item btn btn-sm" :disabled="page <= 1" @click="goPage(page - 1)">«</button>
<button
v-for="p in pageNumbers"
:key="p"
class="join-item btn btn-sm"
:class="p === page ? 'btn-primary' : ''"
@click="goPage(p)"
>
{{ p }}
</button>
<button class="join-item btn btn-sm" :disabled="page >= totalPages" @click="goPage(page + 1)">»</button>
</div>
</div>
<!-- Modal de detalle -->
<Teleport v-if="detailNumero !== null" to="body">
<div class="fixed inset-0 z-[60] overflow-y-auto">
<div class="fixed inset-0 bg-black/60" @click="closeDetail"></div>
<div class="relative min-h-full flex items-center justify-center p-4 sm:p-6">
<div class="w-full max-w-3xl max-h-[85vh] overflow-y-auto rounded-box bg-base-100 shadow-2xl ring-1 ring-base-300">
<div class="sticky top-0 z-10 flex items-center justify-between px-4 py-3 bg-base-100 border-b border-base-200">
<span class="text-sm font-semibold">Detalle del voluntario</span>
<button class="btn btn-ghost btn-sm" @click="closeDetail"></button>
</div>
<div class="p-4">
<VoluntarioDetail :numero="detailNumero" />
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, onUnmounted } from "vue";
import SearchableCombobox from "./SearchableCombobox.vue";
import VoluntarioDetail from "./VoluntarioDetail.vue";
import { cachedFetch } from "../../lib/adminCache";
const rows = ref([]);
const total = ref(0);
const page = ref(1);
const totalPages = ref(1);
const limit = 25;
const loading = ref(false);
const error = ref("");
const detailNumero = ref(null);
let returnPath = "";
function slugUrl(numero) {
const m = window.location.pathname.match(/^\/([a-z]{2})\/admin/);
const locale = m ? m[1] : "es";
return `/${locale}/admin/voluntario/${numero}`;
}
function openDetail(row) {
returnPath = window.location.pathname;
detailNumero.value = row.numero_voluntario;
history.pushState({ voluntario: true }, "", slugUrl(row.numero_voluntario));
}
function closeDetail() {
detailNumero.value = null;
history.replaceState(null, "", returnPath || window.location.pathname);
}
function onPopstate() {
if (detailNumero.value !== null) detailNumero.value = null;
}
function onKeydown(e) {
if (e.key === "Escape" && detailNumero.value !== null) closeDetail();
}
watch(detailNumero, (v) => {
document.body.style.overflow = v === null ? "" : "hidden";
});
const search = ref("");
const selectedStatuses = ref([]);
const selectedPaises = ref([]);
const sort = ref("submitted_at");
const order = ref("desc");
const showFilters = ref(false);
const desde = ref("");
const hasta = ref("");
const edadMin = ref("");
const edadMax = ref("");
const coreText = reactive({
ciudad: "",
estado: "",
profesion: "",
nivel_academico: "",
documento_nro: "",
});
const sexo = ref("");
const nacionalidad = ref("");
const formVersion = ref("");
const responseFields = [
"idioma",
"areas_colaborar",
"dias_disponibles",
"horario_preferido",
"fuera_ciudad",
"misiones_internacionales",
"condicion_medica",
"voluntariado_anterior",
];
const respFilters = reactive(Object.fromEntries(responseFields.map((f) => [f, ""])));
const respLabels = {
idioma: "Idioma",
areas_colaborar: "Áreas de colaboración",
dias_disponibles: "Días disponibles",
horario_preferido: "Horario preferido",
fuera_ciudad: "Disponible fuera de ciudad",
misiones_internacionales: "Misiones internacionales",
condicion_medica: "Condición médica",
voluntariado_anterior: "Voluntariado anterior",
};
const respFilterOptions = {
idioma: ["Espanol", "Ingles", "Hebreo", "Portugues", "Frances", "Otro"],
areas_colaborar: [
"Ayuda Humanitaria", "Educacion", "Desarrollo Comunitario", "Liderazgo",
"Logistica", "Organizacion de Eventos", "Comunicacion Institucional",
"Fotografia y Medios", "Recaudacion de Fondos", "Gestion de Proyectos",
"Traduccion e Interpretacion", "Asesoria Juridica", "Servicios Medicos",
"Diplomacia Publica", "Administracion", "Tecnologia e Innovacion", "Otra",
],
dias_disponibles: ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"],
horario_preferido: ["Manana", "Tarde", "Noche", "Segun necesidades"],
fuera_ciudad: ["si", "no"],
misiones_internacionales: ["si", "no"],
condicion_medica: ["si", "no"],
voluntariado_anterior: ["si", "no"],
};
const statuses = ref([]);
const formVersions = ref([]);
let searchTimer = null;
const statusLabel = computed(() =>
selectedStatuses.value.length ? selectedStatuses.value.join(", ") : "Todos"
);
const activeFilterCount = computed(() => activeChips.value.length);
const activeChips = computed(() => {
const chips = [];
const add = (key, label, remove) => chips.push({ key, label, remove });
if (search.value.trim()) add("q", `Búsqueda: ${search.value.trim()}`, () => {
search.value = "";
onSearch();
});
for (const s of selectedStatuses.value) add(`status-${s}`, `Status: ${s}`, () => {
selectedStatuses.value = selectedStatuses.value.filter((x) => x !== s);
applyFilters();
});
for (const p of selectedPaises.value) add(`pais-${p}`, `País: ${p}`, () => {
selectedPaises.value = selectedPaises.value.filter((x) => x !== p);
applyFilters();
});
if (desde.value) add("desde", `Desde: ${desde.value}`, () => {
desde.value = "";
applyFilters();
});
if (hasta.value) add("hasta", `Hasta: ${hasta.value}`, () => {
hasta.value = "";
applyFilters();
});
if (edadMin.value) add("edad_min", `Edad mín: ${edadMin.value}`, () => {
edadMin.value = "";
applyFilters();
});
if (edadMax.value) add("edad_max", `Edad máx: ${edadMax.value}`, () => {
edadMax.value = "";
applyFilters();
});
for (const [k, v] of Object.entries(coreText)) {
if (v) {
const label = { ciudad: "Ciudad", estado: "Estado", profesion: "Profesión", nivel_academico: "Nivel académico", documento_nro: "Documento" }[k];
add(`core-${k}`, `${label}: ${v}`, () => {
coreText[k] = "";
applyFilters();
});
}
}
if (sexo.value) add("sexo", `Sexo: ${sexo.value}`, () => {
sexo.value = "";
applyFilters();
});
if (nacionalidad.value) add("nacionalidad", `Nacionalidad: ${nacionalidad.value}`, () => {
nacionalidad.value = "";
applyFilters();
});
if (formVersion.value) add("form_version", `Versión: ${formVersion.value}`, () => {
formVersion.value = "";
applyFilters();
});
for (const f of responseFields) {
if (respFilters[f]) add(`resp-${f}`, `${respLabels[f]}: ${respFilters[f]}`, () => {
respFilters[f] = "";
applyFilters();
});
}
return chips;
});
async function fetchData() {
loading.value = true;
error.value = "";
const params = new URLSearchParams();
params.set("page", String(page.value));
params.set("limit", String(limit));
params.set("sort", sort.value);
params.set("order", order.value);
if (search.value.trim()) params.set("q", search.value.trim());
if (selectedStatuses.value.length) params.set("status", selectedStatuses.value.join(","));
if (selectedPaises.value.length) params.set("pais", selectedPaises.value.join(","));
if (desde.value) params.set("desde", desde.value);
if (hasta.value) params.set("hasta", hasta.value);
if (edadMin.value) params.set("edad_min", edadMin.value);
if (edadMax.value) params.set("edad_max", edadMax.value);
for (const [f, v] of Object.entries(coreText)) if (v) params.set(f, v);
if (sexo.value) params.set("sexo", sexo.value);
if (nacionalidad.value) params.set("nacionalidad", nacionalidad.value);
if (formVersion.value) params.set("form_version", formVersion.value);
for (const f of responseFields) if (respFilters[f]) params.set(f, respFilters[f]);
try {
const res = await cachedFetch(`/api/admin/voluntarios?${params.toString()}`, { ttl: 30 });
const json = await res.json();
if (!json.success) throw new Error(json.message || "Error al cargar");
rows.value = json.data;
total.value = json.total;
totalPages.value = json.totalPages;
page.value = json.page;
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
async function fetchOptions() {
try {
const res = await cachedFetch("/api/admin/voluntarios/options", { ttl: 300 });
const json = await res.json();
if (json.success) {
statuses.value = json.statuses;
formVersions.value = json.form_versions;
}
} catch {
// opciones no críticas
}
}
function onSearch() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
page.value = 1;
fetchData();
}, 400);
}
function applyFilters() {
page.value = 1;
fetchData();
}
function clearFilters() {
search.value = "";
selectedStatuses.value = [];
selectedPaises.value = [];
desde.value = "";
hasta.value = "";
edadMin.value = "";
edadMax.value = "";
Object.keys(coreText).forEach((k) => (coreText[k] = ""));
sexo.value = "";
nacionalidad.value = "";
formVersion.value = "";
Object.keys(respFilters).forEach((k) => (respFilters[k] = ""));
page.value = 1;
fetchData();
}
function toggleSort(field) {
if (sort.value === field) {
order.value = order.value === "asc" ? "desc" : "asc";
} else {
sort.value = field;
order.value = "asc";
}
page.value = 1;
fetchData();
}
function goPage(p) {
if (p < 1 || p > totalPages.value || p === page.value) return;
page.value = p;
fetchData();
}
const pageNumbers = computed(() => {
const total = totalPages.value;
const current = page.value;
const start = Math.max(1, current - 2);
const end = Math.min(total, current + 2);
const pages = [];
for (let i = start; i <= end; i++) pages.push(i);
return pages;
});
function formatDate(iso) {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("es", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
}
function badgeClass(s) {
switch (s) {
case "aprobado":
return "badge-success";
case "rechazado":
return "badge-error";
case "legacy":
return "badge-warning";
default:
return "badge-neutral";
}
}
onMounted(() => {
fetchOptions();
fetchData();
window.addEventListener("popstate", onPopstate);
window.addEventListener("keydown", onKeydown);
});
onUnmounted(() => {
window.removeEventListener("popstate", onPopstate);
window.removeEventListener("keydown", onKeydown);
});
</script>

View File

@ -1,41 +1,15 @@
---
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
const { props } = Astro.props;
const textColor = props.textColor || ''
const imageUrl = props.image || ''
//const bgClass = props.type === 'text'? `bg-[${props.bgColor}]` : `bg-[url(${props.image})] bg-cover`
const bgClass = props.type === 'text'? `bg-[${props.bgColor}]` : `bg-[url(${props.image})] bg-cover`
---
{props.type !== 'imgText' && (
<div class="aspect-square overflow-hidden">
<div class={`flex flex-col justify-center h-full bg-[${props.bgColor}]`}>
<div class="aspect-square">
<div class={`flex flex-col justify-between h-full p-16 ${bgClass}`}>
{ props.type === 'text' && (
<div class="p-8 sm:p-16 md:p-8 xl:p-16 flex flex-col justify-between h-full">
<Icon name={props.icon} class={`text-8xl text-[${textColor}] hidden lg:block`} />
<p class={`text-[${textColor}]`}>{props.text}</p>
</div>
)}
{ props.type === 'image' && (
<div class="object-cover aspect-square flex">
<Image class="w-full object-cover" src={props.image} alt={props.text||''} width="400" height="400" />
</div>
<Icon name={props.icon} class="text-8xl" />
<p>{props.text}</p>
)}
</div>
</div>
)}
{ props.type === 'imgText' && (
<div class={`flex flex-col justify-between h-full bg-[${props.bgColor}]`}>
<div class="grid grid-cols-1 gap-0 p-8 font-bold">
<div class="md:px-10 md:py-10">
<Icon name={props.icon} class="text-3xl" />
<p class={`font-primary text-xl text-[${textColor}]`}>{props.text}</p>
</div>
<div class="object-cover aspect-square flex">
<img class="w-full h-auto object-cover" src={props.image} alt="" />
</div>
</div>
</div>
)}

View File

@ -1,62 +1,24 @@
---
import { Image } from "astro:assets"
import Image from "astro/components/Image.astro";
import { Icon } from "astro-icon/components";
import "dayjs/locale/es";
import "dayjs/locale/fr";
import "dayjs/locale/he";
import "dayjs/locale/uk";
import "dayjs/locale/pt";
import "dayjs/locale/ru";
import "dayjs/locale/rw";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { getLocalizedRoute, createTranslator } from "../../i18n";
const locale = Astro.currentLocale || "es";
const tl = createTranslator(locale);
const regionNames = new Intl.DisplayNames([locale], { type: 'region' });
dayjs.extend(utc);
dayjs.locale(locale);
const { data, routeKey = "news" } = Astro.props;
const nicedate = dayjs.utc(data.data.date).format("D MMMM YYYY");
const countryName = data?.data?.country ? regionNames.of(data.data.country) : "";
const locationArray = [data.data.city,countryName]
locationArray.filter(Boolean).join(', ');
const { data } = Astro.props;
const nicedate = dayjs(data.data.date).format("D MMMM YYYY");
---
<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}):
<div class="bg-[#EBE5D0] p-10">
<Icon name="ph:arrow-circle-down-thin" class="text-8xl mb-8" />
<p class="font-light text-2xl mb-8">
{data.data.city}, {data.data.country}<br />({nicedate}):
</p>
) : (
<p class="font-light md:text-2xl text-lg md:mb-8 mb-3">
{locationArray.filter(Boolean).join(', ')}:
</p>
)}
</div>
<h3 class="md:text-2xl text-lg mb-4 font-bold md:mb-8 hover:underline"><a href={`/${locale}/${getLocalizedRoute(routeKey, locale)}/${data.data.slug}`}>{data.data.title}</a></h3>
<h3 class="text-2xl font-bold mb-8"><a href={`/news/${data.id}`}>{data.data.title}</a></h3>
<div class="overflow-hidden">
<a href={`/${locale}/${getLocalizedRoute(routeKey, locale)}/${data.data.slug}`}>
<div>
<Image
src={data.data.thumbnail}
alt={data.data.title}
class="aspect-square object-cover transition-transform duration-300 hover:scale-110"
class="aspect-square object-cover"
/>
</a>
</div>
<div class="mt-8">
<a href={`/${locale}/${getLocalizedRoute(routeKey, locale)}/${data.data.slug}`} class="inline-flex items-center gap-2 px-6 py-3 bg-white text-[#22523F] hover:bg-[#22523F] hover:text-[#EBE6D2] hover:underline font-bold transition text-sm rounded-none uppercase">
{tl(routeKey + ".fullnew")}
<Icon name="ph:arrow-right" class="transform group-hover:translate-x-1 transition-transform" />
</a>
</div>
</div>

View File

@ -1,68 +0,0 @@
---
import { Image } from "astro:assets";
import { Icon } from "astro-icon/components";
import "dayjs/locale/es";
import "dayjs/locale/fr";
import "dayjs/locale/he";
import "dayjs/locale/uk";
import "dayjs/locale/pt";
import "dayjs/locale/ru";
import "dayjs/locale/rw";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
const locale = Astro.currentLocale || "es";
const regionNames = new Intl.DisplayNames([locale], { type: "region" });
import { createTranslator, getLocalizedRoute } from "@/i18n";
const tl = createTranslator(locale);
dayjs.extend(utc);
dayjs.locale(locale);
const { data, content, routeKey = "news" } = Astro.props;
const nicedate = dayjs.utc(data.data.date).format("D MMMM YYYY");
const countryName = data?.data?.country ? regionNames.of(data.data.country) : "";
const location = [data.data.city, data.data.state, countryName].filter(Boolean).join(", ");
const newsUrl = `/${locale}/${getLocalizedRoute(routeKey, locale)}/${data.data.slug}`;
const rawContent = content?.body || "";
const plainText = rawContent.replace(/^#.*$/gm, "").replace(/^###.*$/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^>.*$/gm, "").replace(/`[^`]+`/g, "").replace(/^[-*]\s+/gm, "").trim();
const words = plainText.split(/\s+/).filter((w) => w.length > 0).slice(0, 40);
const excerpt = words.join(" ") + (words.length === 40 ? "..." : "");
---
<a href={newsUrl} class="block group">
<article class="flex flex-col md:flex-row gap-6 md:gap-8 p-4 md:p-6 border-b border-tertiary/20 hover:bg-tertiary/5 transition-colors">
<div class="md:w-1/3 flex-shrink-0 overflow-hidden justify-center items-center flex">
<Image src={data.data.thumbnail_square || data.data.thumbnail} alt={data.data.title} width={480} class="w-full h-auto object-contain transform group-hover:scale-105 transition-transform duration-300" />
</div>
<div class="md:w-2/3 flex flex-col">
<div class="flex items-center gap-2 mb-2">
<span class="font-normal font-primary text-sm text-tertiary/70">{nicedate}</span>
{location && <><span class="text-tertiary/40">|</span><span class="font-normal font-primary text-sm text-tertiary/70">{location}</span></>}
</div>
<h3 class="text-xl md:text-2xl font-bold font-secondary text-tertiary group-hover:text-tertiary/80 transition-colors mb-2">{data.data.title}</h3>
<p class="font-primary text-base text-tertiary/80 mb-4 line-clamp-3">{excerpt}</p>
{data.data.tags && data.data.tags.length > 0 && (
<div class="flex flex-nowrap md:flex-wrap gap-2 overflow-x-auto md:overflow-visible pb-2 md:pb-0 mb-4">
{data.data.tags.map((tag: string) => (
<span class="badge rounded-none bg-[#EBE6D2] border-none text-[#003421] whitespace-nowrap">{tag}</span>
))}
</div>
)}
<div class="mt-auto">
<span class="inline-flex items-center gap-1 text-sm font-primary text-tertiary font-semibold group-hover:underline">
{tl(routeKey + ".seemore")}
<Icon name="ph:arrow-right" class="transform group-hover:translate-x-1 transition-transform" />
</span>
</div>
</div>
</article>
</a>

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
---
import { createTranslator } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
---
<div class="container mx-auto bg-[#003421]">
<div class="grid grid-cols-3 relative">
<div class="col-span-3 sm:col-span-2 p-12 lg:p-24 lg:h-82 text-[#EBE6D2]">
<h2 class="text-colorSecondary text-2xl" set:html={tl("authority.title")}></h2>
<p set:html={tl("authority.body")} />
</div>
<div class="hidden sm:flex col-span-1 bg-[#CBA16A] items-end self-end justify-center h-82 overflow-visible">
<img src="/img/DRJBP-1.webp" class="w-48 sm:w-64 lg:w-76 object-contain absolute bottom-0" alt="">
</div>
</div>
</div>

View File

@ -1,69 +1,34 @@
---
import Image from "astro/components/Image.astro";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import { Icon } from "astro-icon/components";
const isHebrew = Astro.currentLocale === "he";
const { images, class: className, imgClass } = Astro.props;
const { images } = Astro.props;
---
<div class={`bg-white ${className || ''}`}>
<div class="bg-white">
<div class="mx-auto">
<div class="swiper">
<div class="swiper-wrapper">
{
images.map((image) => (
<div class="swiper-slide">
<div class="relative">
{image.text && (
<div class="uppercase lg:text-9xl md:text-5xl text-4xl text-white text-shadow-lg absolute inset-0 flex items-center justify-center font-secondary z-10">
{image.text}
</div>
)}
<img
class={`w-full ${imgClass || ''}`}
src={image.image}
alt={image.text}
/>
{image.text_alt && (
<div class="text-sm text-white px-4 py-2 italic bg-black/50 absolute bottom-2 right-2 rounded z-10">
{image.text_alt}
</div>
)}
</div>
<Image src={image} alt="" />
</div>
))
}
</div>
<!-- If we need pagination -->
<div class="swiper-pagination z-50"></div>
<div class="swiper-pagination"></div>
<!-- If we need navigation buttons -->
<div class="swiper-button-prev">
{
isHebrew && (
<Icon name="ph:arrow-circle-right-thin" class="text-white" />
)
}
{
!isHebrew && (
<Icon name="ph:arrow-circle-left-thin" class="text-white" />
)
}
</div>
<div class="swiper-button-next">
{
isHebrew && (
<Icon name="ph:arrow-circle-left-thin" class="text-white" />
)
}
{
!isHebrew && (
<Icon name="ph:arrow-circle-right-thin" class="text-white" />
)
}
</div>
</div>
</div>
@ -71,15 +36,8 @@ const { images, class: className, imgClass } = Astro.props;
<script>
import Swiper from "swiper";
import { Navigation, Pagination, Scrollbar } from "swiper/modules";
// Re-initialize after every page swap via View Transitions
document.addEventListener("astro:after-swap", init);
function init() {
const swiper = new Swiper(".swiper", {
// Optional parameters
modules: [Navigation, Pagination, Scrollbar],
loop: true,
// If we need pagination
@ -98,8 +56,4 @@ const { images, class: className, imgClass } = Astro.props;
el: ".swiper-scrollbar",
},
});
}
// Initialize on the first page load
init();
</script>

View File

@ -1,9 +1,11 @@
---
const { bgColor, titleColor, textColor, title, text, id } = Astro.props;
const { bgColor, titleColor, textColor, title, text } = Astro.props;
console.log( 'Props', bgColor )
---
<div id={id} class={`bg-[${bgColor}] py-20`}>
<div class="container mx-auto w-3/4">
<h4 class={`uppercase text-lg ${titleColor} font-primary md:text-xl text-center mb-4 font-bold`}>{title}</h4>
<p class={`text-lg md:text-3xl font-secondary ${textColor} text-center`}>{text}</p>
<div class={`bg-[${bgColor}] py-20`}>
<div class="container mx-auto">
<h4 class={`uppercase text-lg ${titleColor} font-primary text-center mb-4 font-bold`}>{title}</h4>
<p class={`text-3xl font-secondary ${textColor} text-center`}>{text}</p>
</div>
</div>

View File

@ -1,44 +0,0 @@
---
import { Icon } from "astro-icon/components";
import { createTranslator } from '../../i18n';
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 min-w-0">
<h2 class="text-3xl lg:text-2xl xl:text-3xl 2xl:text-5xl font-secondary text-tertiary font-bold wrap-break-word">{tl("values.justice.title")}</h2>
<p class="text-lg text-tertiary break-words">{tl("values.justice.text")}</p>
</div>
<div class="flex items-end justify-end min-h-full min-w-0 max-w-full">
<Icon name="icon_justice_1" class="text-6xl md:text-9xl text-tertiary max-w-full h-auto" />
</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-3xl lg:text-2xl xl:text-3xl 2xl:text-5xl font-secondary font-bold text-[#CBA16A] wrap-break-word">{tl("values.integrity.title")}</h2>
<p class="text-[#CBA16A] text-lg break-words">{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>
</div>
</div>
<div class="grid lg:grid-cols-3">
<div class="bg-tertiary p-12 xl:p-20 2xl:p-24 flex justify-between flex-col min-h-full lg:aspect-square min-w-0">
<h2 class="text-3xl 2xl:text-5xl font-secondary font-bold text-white wrap-break-word">{tl("values.service.title")}</h2>
<p class="text-xl text-[#EBE5D0] wrap-break-word">{tl("values.service.text")}</p>
</div>
<div class="bg-[#BEA48D] p-12 xl:p-20 2xl:p-24 flex justify-between flex-col min-h-full lg:aspect-square min-w-0">
<h2 class="text-3xl 2xl:text-5xl font-secondary font-bold text-tertiary wrap-break-word">{tl("values.excellence.title")}</h2>
<p class="text-xl text-tertiary wrap-break-word">{tl("values.excellence.text")}</p>
</div>
<div class="bg-[#22523F] p-12 xl:p-20 2xl:p-24 flex justify-between flex-col min-h-full lg:aspect-square min-w-0">
<h2 class="text-3xl 2xl:text-5xl font-secondary font-bold text-white wrap-break-word">{tl("values.dialogue.title")}</h2>
<p class="text-white text-xl wrap-break-word">{tl("values.dialogue.text")}</p>
</div>
</div>
</div>

View File

@ -1,63 +0,0 @@
---
import { getCollection } from "astro:content";
import { Image } from "@unpic/astro";
import Button from "../ui/Button.astro";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { createTranslator, getLocalizedRoute } from '../../i18n';
const currentLocale = Astro.currentLocale;
const tl = createTranslator(currentLocale);
const items = await getCollection("editorial", (post)=>{
return post.data.locale == currentLocale
});
const sorted = [...items]
.sort((a, b) => {
const dateDiff = new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
if (dateDiff !== 0) return dateDiff
return (a.data.order ?? 0) - (b.data.order ?? 0)
})
.slice(0, 6);
dayjs.extend(utc);
dayjs.locale(currentLocale || "es");
---
<div id="editorial" class="bg-[#EBE5D0] py-10 lg:py-14">
<div class="container mx-auto px-4 md:px-0 max-w-4xl">
<div class="flex flex-col items-center mb-8">
<h4 class="text-[#003421] text-2xl uppercase font-bold text-center font-primary">{tl("editorial.title")}</h4>
<h2 class="text-[#003421] text-3xl md:text-4xl font-bold text-center font-secondary mt-1">{tl("editorial.text")}</h2>
</div>
<div class="divide-y divide-[#003421]/10">
{
sorted.map((item) => {
const date = dayjs.utc(item.data.date).format("D MMMM YYYY");
const url = `/${currentLocale}/${getLocalizedRoute("editorial", currentLocale)}/${item.data.slug}`;
const thumb = item.data.thumbnail_square || item.data.thumbnail;
return (
<a href={url} class="flex gap-5 py-5 group hover:bg-[#003421]/5 transition-colors -mx-3 px-3 rounded-sm">
<div class="w-20 h-20 md:w-24 md:h-24 shrink-0 overflow-hidden rounded-sm bg-[#003421]/10">
{thumb && (
<Image src={thumb} alt={item.data.title} class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
)}
</div>
<div class="min-w-0 flex-1">
<p class="text-sm md:text-base font-primary font-semibold text-[#22523F]">Dr. José Benjamín Pérez Matos</p>
<p class="text-sm text-[#003421]/50 font-primary mt-0.5">{date}</p>
<h3 class="text-base md:text-lg font-secondary font-bold text-[#003421] group-hover:text-[#003421]/60 transition-colors mt-1 leading-snug line-clamp-2">{item.data.title}</h3>
</div>
</a>
)
})
}
</div>
<div class="flex justify-center mt-8">
<Button class="px-6 py-2.5 uppercase text-sm" url={`/${currentLocale}/${getLocalizedRoute('editorial', currentLocale)}`} variant="primary" title={tl("editorial.buttonLable")} />
</div>
</div>
</div>

View File

@ -1,87 +0,0 @@
---
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
import jbp from "../../assets/DRJBP-1.webp";
import FormContact from "./FormContact.vue";
import { createTranslator, t } from "../../i18n";
const tl = createTranslator(Astro.currentLocale);
const isHebrew = Astro.currentLocale === "he";
const { hideContact } = Astro.props;
---
<div id="contact" class="bg-[#22523F]">
<div class="container mx-auto py-20 relative text-[#EBE5D0]">
<div class="absolute bottom-0 right-0 w-80 hidden lg:block">
<Image src={jbp} alt="Centro del Reino de Paz y Justicia" />
</div>
<h4
class="text-lg text-center font-bold uppercase"
set:html={tl("footer.title")}
>
</h4>
<h2
class="text-2xl lg:text-5xl text-center font-bold font-secondary my-8"
set:html={tl("footer.subtitle")}
>
</h2>
<div class="px-12 lg:px-4 lg:w-2/5 mx-auto">
<p class="text-lg font-light mb-10" set:html={tl("footer.text")}></p>
<p class="text-lg font-regular">
{tl("footer.email")}
<!--email_off-->
<a
class="font-light hover:text-[#D4C7A1] transition-all hover:underline hover:underline-offset-4 hidden lg:block"
href="mailto:joseperez@centrodelreinodepazyjusticia.com"
>joseperez@centrodelreinodepazyjusticia.com</a
>
<!--/email_off-->
</p>
<!--email_off-->
<a
class="font-light hover:text-[#D4C7A1] transition-all hover:underline hover:underline-offset-4 block lg:hidden overflow-hidden overflow-ellipsis"
href="mailto:joseperez@centrodelreinodepazyjusticia.com"
>joseperez@centrodelreinodepazyjusticia.com</a
>
<!--/email_off-->
<p
class="text-lg font-light mb-10 break-words"
set:html={tl("footer.text2")}
>
</p>
{!hideContact && <FormContact client:load locale={Astro.currentLocale} />}
</div>
<div
class="flex lg:justify-between mt-10 lg:mt-0 align-center justify-center"
>
{
!isHebrew && (
<p class="px-4 order-1 text-sm font-normal text-white lg:self-end text-center lg:text-left">
{tl("footer.reserved")}
</p>
)
}
<div class="flex items-center gap-3 lg:order-1 z-10 pl-6">
<img
class="w-24 object-contain"
src="/img/logo-metalico.webp"
alt="Logo Metalico"
/>
{
isHebrew && (
<p class="text-sm font-normal text-white text-center lg:text-left">
{tl("footer.reserved")}
</p>
)
}
</div>
</div>
</div>
</div>

View File

@ -1,188 +0,0 @@
<script setup>
import { Icon } from "@iconify/vue";
import { createTranslator } from "../../i18n";
import { ref, computed } from "vue";
const props = defineProps({
locale: String
});
const tl = createTranslator(props.locale);
const isLoading = ref(false);
const isSuccess = ref(false);
const isError = ref(false);
const dashOffset = ref(453);
const dashStyle = computed(() => ({
stroke: "#1ecd97",
fill: "white",
strokeDasharray: 453,
strokeDashoffset: dashOffset.value,
transition: "stroke-dashoffset 1.2s ease"
}));
const handleSubmit = async (e) => {
const formData = new FormData(e.target);
isLoading.value = true;
isSuccess.value = false;
isError.value = false;
try {
const response = await fetch("/api/emailInfo/send", {
method: "POST",
body: formData,
});
if (!response.ok) throw new Error();
e.target.reset();
isSuccess.value = true;
setTimeout(() => {
isSuccess.value = false;
}, 3500);
} catch (error) {
console.error(error);
isError.value = true;
setTimeout(() => {
isError.value = false;
}, 3500);
} finally {
setTimeout(() => {
isLoading.value = false;
}, 2000);
}
};
</script>
<template>
<form
@submit.prevent="handleSubmit"
class="flex flex-col gap-4"
>
<fieldset>
<input
name="nombre"
type="text"
:placeholder="tl('footer.form.name')"
required
class="bg-[#EBE5D0] w-full py-2 px-4 mb-2 text-[#303335] placeholder:text-[#303335] focus:outline-none"
/>
<input
name="email"
type="email"
placeholder="E-Mail"
required
class="bg-[#EBE5D0] w-full py-2 px-4 mb-2 text-[#303335] placeholder:text-[#303335] focus:outline-none"
/>
<textarea
name="mensaje"
:placeholder="tl('footer.form.mesagge')"
rows="5"
required
class="bg-[#EBE5D0] w-full py-2 px-4 mb-2 text-[#303335] placeholder:text-[#303335] focus:outline-none resize-none"
></textarea>
</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 -->
<ul class="flex flex-row gap-2">
<li class="border-r pr-2">
<a href="https://x.com/CRPazYJusticia" target="_blank">
<Icon icon="ph:x-logo-thin" class="text-3xl hover:text-[#D4C7A1] transition-all hover:transform hover:scale-115" />
</a>
</li>
<li class="border-r pr-2">
<a href="https://www.instagram.com/centrodelreinodepazyjusticia/" target="_blank">
<Icon icon="ph:instagram-logo-thin" class="text-3xl hover:text-[#D4C7A1] transition-all hover:transform hover:scale-115" />
</a>
</li>
<li class="border-r pr-2">
<a href="https://www.facebook.com/Centrodelreinodepazyjusticia" target="_blank">
<Icon icon="ph:facebook-logo-thin" class="text-3xl hover:text-[#D4C7A1] transition-all hover:transform hover:scale-115" />
</a>
</li>
<li class="border-r pr-2">
<a href="https://www.youtube.com/@CentrodelReinodePazyJusticia" target="_blank">
<Icon icon="ph:youtube-logo-thin" class="text-3xl hover:text-[#D4C7A1] transition-all hover:transform hover:scale-115" />
</a>
</li>
<li>
<a href="https://t.me/CentroRPJ" target="_blank">
<Icon icon="ph:telegram-logo-thin" class="text-3xl hover:text-[#D4C7A1] transition-all hover:transform hover:scale-115" />
</a>
</li>
</ul>
</div>
</form>
</template>
<style scoped>
.kk-submit {
background: transparent;
border: none;
cursor: pointer;
}
.btn-bg {
stroke: #c8c8c8;
fill: white;
}
.btn-color {
stroke-width: 4;
}
.textNode {
fill: #48727F;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
}
.checkNode {
fill: #1ecd97;
font-size: 22px;
}
</style>

View File

@ -1,48 +0,0 @@
---
import { Icon } from "astro-icon/components";
import { createTranslator, t } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
---
<div id="programs" class="bg-[#BEA48D] py-16 lg:py-32">
<div class="container mx-auto">
<h4 class="text-lg text-center uppercase text-[#003421]">{tl("formation.subtitle")}</h4>
<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">
<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>
</div>
<div class="bg-[#EBE5D0] p-12 relative pb-32 md:pb-52">
<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>
</div>
<div class="bg-[#EBE5D0] p-12 relative pb-32 md:pb-52">
<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>
</div>
</div>
</div>
</div>

View File

@ -1,64 +1,65 @@
---
import GridCard from "../cards/GridCard.astro"
import { createTranslator } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
const cards = [
{
type: 'text',
icon: 'ph:arrow-circle-down-thin',
text: 'Dirección de La Gran Carpa Catedral. Puerto Rico',
bgColor: '#EBE5D0',
textColor: '#003421'
bgColor: '#EBE5D0'
},
{
type: 'image',
image: 'https://ik.imagekit.io/crpy/grid_image_1.webp',
image: 'https://picsum.photos/600/600?random=1&grayscale',
},
{
type: 'text',
icon: 'ph:arrow-circle-right-thin',
text: 'Participación activa en escenarios internacionales.',
textColor: '#003421',
bgColor: '#BEA48D'
},
{
type: 'image',
image: 'https://ik.imagekit.io/crpy/grid_image_2.webp',
image: 'https://picsum.photos/600/600?random=1&grayscale',
},
{
type: 'image',
image: 'https://ik.imagekit.io/crpy/grid_image_3.webp',
image: 'https://picsum.photos/600/600?random=1&grayscale',
},
{
type: 'text',
icon: 'ph:arrow-circle-up-thin',
text: 'Enseñanza de las escrituras aplicada al análisis del mundo contemporáneo.',
textColor: '#EBE5D0',
bgColor: '#003421'
text: 'Dirección de La Gran Carpa Catedral. Puerto Rico',
bgColor: '#EBE5D0'
},
{
type: 'image',
image: 'https://ik.imagekit.io/crpy/grid_image_4.webp',
image: 'https://picsum.photos/600/600?random=1&grayscale',
},
{
type: 'text',
icon: 'ph:arrow-circle-left-thin',
text: 'Compromiso público en la defensa de Israel, la justicia y la paz.',
textColor: '#003421',
bgColor: '#EBE5D0'
text: 'Participación activa en escenarios internacionales.',
bgColor: '#BEA48D'
},
];
---
<div>
<div class="container mx-auto">
<div class="grid grid-cols-2 md:grid-cols-4">
<div class="grid grid-cols-4">
{
tl("grid.cards").map((card) => (
cards.map((card)=>(
<GridCard props={card} />
))
}
<!-- <div class="aspect-square bg-neutral-200">1</div>
<div class="aspect-square">2</div>
<div class="aspect-square bg-neutral-200">3</div>
<div class="aspect-square">4</div>
<div class="aspect-square">5</div>
<div class="aspect-square bg-neutral-200">6</div>
<div class="aspect-square">7</div>
<div class="aspect-square bg-neutral-200">8</div> -->
</div>
</div>
</div>

View File

@ -1,23 +1,16 @@
---
const { colorText, bgColor } = Astro.props;
import { createTranslator } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
const { title, text, initTitle, colorText, bgColor } = Astro.props;
---
<div id="somos" class="container mx-auto">
<div class="container mx-auto">
<div class={`grid grid-cols-2 ${bgColor}`}>
<div>IMAGEN</div>
<div class={`flex flex-col ${colorText} gap-8 py-24 px-16`}>
<h2 class="font-primary text-3xl font-bold">{initTitle}</h2>
<h2 class="font-secondary text-5xl">{title}</h2>
<p class="text-lg font-primary leading-relaxed">{text}</p>
<div class="grid grid-cols-1 md:grid-cols-3 bg-white">
<div class="bg-[#22523F] col-span-1 flex items-center justify-center bg-[url('/img/opacity-logo.png')] bg-cover bg-no-repeat bg-right md:bg-left">
<img src="/img/logo-metalico.webp" alt="" class="w-1/4 my-8 md:w-1/2" />
</div>
<div class={`flex flex-col text-tertiary col-span-2 gap-8 py-8 md:py-16 lg:py-24 px-8 sm:px-16 lg:px-32`}>
<h2 class="font-primary text-xl md:text-3xl font-bold">{tl("identity.initTitle")}</h2>
<h2 class="font-secondary text-3xl md:text-5xl font-bold">{tl("identity.title")}</h2>
<div class="prose-p:text-lg prose-p:font-primary prose-p:leading-relaxed prose-p:text-justify prose-p:mb-4">
<p set:html={tl("identity.body")} />
</div>
</div>
</div>

View File

@ -1,34 +1,43 @@
---
import { infoboxes } from "../../data/content/infosection.js";
import BoxContainer from "../BoxContainer.astro";
import { Icon } from "astro-icon/components";
import { createTranslator } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
const { bgColor, titleColor, textColor, title, text } = Astro.props;
---
<div class="container mx-auto">
<div class="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-3">
{ tl("info.boxes").map((box) => (
<div class="grid grid-cols-3">
{ infoboxes.map((box) => (
<BoxContainer props={box} />
))}
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 h-auto bg-white w-full">
<div class="lg:col-span-2 row-span-2 p-8 lg:p-24 text-tertiary grid gap-8 ">
<div class="grid grid-cols-3 h-auto bg-white w-full">
<div class="col-span-2 row-span-2 p-24 text-tertiary grid gap-8 ">
<div class="grid gap-8">
<h2 class="font-secondary text-2xl lg:text-5xl font-bold">{tl('info.title')}</h2>
<h2 class="font-secondary text-5xl font-bold">{title}</h2>
<Icon name="ph:minus" class="text-tertiary text-4xl" />
<p class="text-lg text-justify" set:html={tl("info.copy1")} />
<p class="text-lg">El <b>Centro del Reino de Paz y Justicia (CPyJ)</b> es una organización de alcance <b>internacional dedicada a la formación, el diálogo estratégico y la acción pública, orientada a promover la justicia y la paz</b> conforme a los valores eternos proclamados por los profetas, con un compromiso explícito y permanente con Israel y su lugar central en la historia y el destino del mundo.</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 text-lg leading-relaxed text-justify">
<p set:html={tl("info.copy_column1")} />
<p set:html={tl("info.copy_column2")} />
<div class="grid grid-cols-2 gap-8 text-lg leading-relaxed">
<p>El CPyJ desarrolla iniciativas educativas, espacios de reflexión profunda y acciones concretas en el ámbito público, integrando principios espirituales, responsabilidad institucional y liderazgo ético. Su labor se inscribe en el campo de la diplomacia pública, entendida como una herramienta legítima para influir en la conversación global, fortalecer vínculos entre naciones y defender valores fundamentales frente a los desafíos del presente y del futuro.</p>
<p>La paz no es concebida como una consigna abstracta ni como un ideal ingenuo, sino como el resultado de decisiones firmes, liderazgo con valores y compromiso sostenido con propósitos claros, que reconocen el rol insustituible de Israel en la construcción de un orden justo y estable para toda la humanidad.</p>
</div>
</div>
<div class="col-span-1 h-full bg-[#CBA16A]">
<BoxContainer props={tl("info.endbox")} />
<BoxContainer props={{
title: 'Misión, Visión y Valores Institucionales.',
buttonLabel: 'Leer Mas',
hasButton: true,
bgImage: '../src/assets/dove-bg.webp',
hasIcon: true,
bgColor: '#CBA16A',
titleColor: 'text-tertiary',
sizeTitle: 'text-2xl',
}} />
</div>
<div class="col-span-1 py-10 lg:px-24 bg-[#21523F]">
<img src="/img/logo-new-white.png" alt="Logo del Centro del Reino de Paz y Justicia (CRPJ)" class="w-1/3 lg:w-100 mx-auto">
<div class="col-span-1 py-38 px-24 bg-tertiary">
<img src="/src/assets/logo-ligth.webp" alt="Logo del CPyJ">
</div>
</div>
</div>

View File

@ -1,41 +1,17 @@
---
import { getCollection } from "astro:content";
import { getCollection, getEntry } from "astro:content";
import NewsCard from "../cards/NewsCard.astro";
import Button from "../ui/Button.astro";
const { routeKey = "news", anchorId = "news", titlePrefix = "news" } = Astro.props;
const currentLocale = Astro.currentLocale;
const items = await getCollection(routeKey, (post)=>{
return post.data.locale == currentLocale
});
import { createTranslator, getLocalizedRoute } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
const newsItems = await getCollection("news");
---
<div id={anchorId} class="bg-[#22523F] py-12 lg:py-20">
<div id="news" class="bg-[#22523F] py-20">
<div class="container mx-auto">
<div class="flex flex-col lg:w-1/2 items-center mx-auto py-8">
<h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">{tl(titlePrefix + ".title")}</h4>
<h2 class="text-white text-3xl lg:text-5xl font-bold text-center font-secondary mb-4">{tl(titlePrefix + ".text")}</h2>
<p class="text-white text-xl text-center">{tl(titlePrefix + ".text2")}</p>
<Button class="px-6 py-3 uppercase mt-4" url={`/${currentLocale}/${getLocalizedRoute(routeKey, currentLocale)}`} variant="primary" title={tl(titlePrefix + ".buttonLable")} />
</div>
<h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">Noticias</h4>
<h2 class="text-white text-6xl font-bold text-center font-secondary mb-4">Actualidad institucional y proyección internacional</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 md:gap-10 gap-20">
<div class="grid grid-cols-3 gap-20">
{
[...items]
.sort((a, b) => {
const dateDiff =
new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
if (dateDiff !== 0) return dateDiff
return (a.data.order ?? 0) - (b.data.order ?? 0)
})
.slice(0,6)
.map((item) => (
<NewsCard data={item} routeKey={routeKey} />
newsItems.map((item) => (
<NewsCard data={item} />
))
}
</div>

View File

@ -1,33 +1,29 @@
---
import { createTranslator, t } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
---
<div class="bg-[#CBA16A] py-12 lg:py-20">
<div class="bg-[#CBA16A] py-20">
<div class="container mx-auto">
<h4 class="text-tertiary text-2xl uppercase font-bold text-center mb-8 font-primary">{tl("participate.title")}</h4>
<h2 class="text-tertiary text-3xl lg:text-5xl font-bold lg:text-center font-secondary mb-4 text-center">{tl("participate.text")}</h2>
<h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">Participa | Colabora</h4>
<h2 class="text-white text-6xl font-bold text-center font-secondary mb-4">Sumarse es asumir un compromiso con propósito</h2>
<p class="text-tertiary font-normal pt-10 text-2xl px-12 text-center" set:html={tl("participate.text2")} />
<p>La labor del CPyJ se fortalece mediante la participación de personas e instituciones alineadas con sus valores y objetivos generales. Formas de participar:</p>
<div class="grid md:grid-cols-2 w-3/4 justify-center mx-auto gap-20 my-20">
<div class="border-1 border-white text-lg p-8 text-tertiary">
<h5 class="font-bold mb-6">{tl("participate.box1.title")}</h5>
<p class="font-normal">{tl("participate.box1.text")}</p>
<div class="grid grid-cols-3 gap-20 my-20">
<div class="border-1 border-white p-8">
<h5>Voluntariado:</h5>
<p>colaboración en proyectos formativos, institucionales o internacionales.</p>
</div>
<!-- <div class="border-1 border-white p-8 text-tertiary">
<h5 class="font-bold text-lg mb-6">Aportes y donaciones:</h5>
<div class="border-1 border-white p-8">
<h5>Aportes y donaciones:</h5>
<p>sostenimiento de programas y actividades.</p>
</div> -->
</div>
<div class="border-1 border-white p-8 text-tertiary">
<h5 class="font-bold text-lg mb-6">{tl("participate.box2.title")}</h5>
<p class="font-normal">{tl("participate.box2.text")}</p>
<div class="border-1 border-white p-8">
<h5>Difusión institucional:</h5>
<p>amplificación de la misión y acciones del CPyJ en espacios públicos.</p>
</div>
</div>
<div class="flex justify-center">
<p class="text-tertiary text-xl px-12 font-bold mx-auto">{tl("participate.text3")}</p>
</div>
<p>La paz se construye mediante decisiones responsables, liderazgo comprometido y acción sostenida.</p>
</div>
</div>

View File

@ -1,53 +0,0 @@
---
import GridCard from "../cards/GridCard.astro";
import BoxContainer from "../BoxContainer.astro";
import { Icon } from "astro-icon/components";
import { createTranslator, t } from '../../i18n';
const tl = createTranslator(Astro.currentLocale);
---
<div class="container mx-auto">
<div id="projection" class="grid lg:grid-cols-3 bg-white w-full">
<div class="row-span-2 lg:col-span-2 p-12 lg:p-24">
<div class="flex flex-col gap-5 md:text-lg text-tertiary text-justify prose-p:mb-4 prose-ul:text-tertiary prose-p:text-tertiary prose-strong:text-tertiary prose-strong:font-bold ">
<h2 class="font-secondary text-3xl lg:text-5xl font-bold">{tl('projection.title')}</h2>
<Icon name="ph:minus" class="text-tertiary text-4xl" />
<div class="prose" set:html={tl('projection.text')}></div>
</div>
<div class="grid xl:grid-cols-2 gap-16 mt-12">
<GridCard props={{
type: 'imgText',
bgColor: '#BEA48D',
icon: 'ph:minus',
image: 'https://ik.imagekit.io/crpy/JBP-MBM.webp',
text: tl("projection.card1")
}} />
<GridCard props={{
bgColor: '#BEA48D',
type: 'imgText',
image: 'https://ik.imagekit.io/crpy/pueblo-judio.webp',
icon: 'ph:minus',
text: tl("projection.card2")
}} />
</div>
</div>
<div class="hidden lg:block row-span-2 col-span-full lg:col-span-1 over bg-[#CBA16A] relative">
<div class="relative">
<BoxContainer props={{
bgImage: 'https://ik.imagekit.io/crpy/tr:o-20/white-lion.png',
bgColor: '#CBA16A',
}} />
<div class="absolute -bottom-[8rem] z-10 w-full">
<img src="/img/logo-new-white.png" class="w-1/2 mx-auto" alt="Logo del Centro del Reino de Paz y Justicia (CRPJ)">
</div>
</div>
<div class="hidden lg:flex bg-[#22523F] object-cover pt-64 justify-end ">
<img src="/img/DRJBP-1.webp" class="h-full object-cover" />
</div>
</div>
</div>
</div>

View File

@ -3,11 +3,11 @@ import { Icon } from 'astro-icon/components'
const { title } = Astro.props;
---
<div class="md:py-16 p-4 bg-white">
<div class="py-20 bg-white">
<div class="container mx-auto">
<div class="flex justify-between px-4 align-center items-center">
<h2 id="article-title" class="text-tertiary font-secondary text-xl sm:text-2xl md:text-3xl lg:text-5xl font-bold">{title}</h2>
<img class="md:w-20 md:h-20 w-10 h-10" src="/img/lion.svg" alt="Leon">
<div class="flex justify-between">
<h2 class="text-tertiary font-secondary text-6xl font-bold">{title}</h2>
<Icon name="logo" class="text-6xl" />
</div>
</div>
</div>

View File

@ -6,8 +6,8 @@ const { variant = "primary", title = "Botón", url = "#", class: className } = A
class?: string;
};
const variants = {
primary: "bg-[#EBE6D2] text-[#22523F] hover:bg-[#22523F]/90 hover:text-[#EBE6D2] rounded-none",
secondary: "bg-[#22523F] text-[#EBE6D2] border-0 hover:bg-[#EBE6D2]/90 hover:text-tertiary uppercase rounded-none",
primary: "bg-[#EBE6D2] text-[#22523F] hover:bg-[#EBE6D2]/90 rounded-none",
secondary: "bg-[#22523F] text-[#EBE6D2] hover:bg-[#22523F]/90 uppercase rounded-none",
light: "bg-green-100",
} as const;
@ -15,4 +15,6 @@ type Variant = keyof typeof variants;
const styles = variants[variant];
---
<a class={`${styles} font-bold transition block text-center ${className || ''}`} href={url} class="inline-block w-full h-full">{title}</a>
<button class={`${styles} font-bold transition block text-center ${className || ''}`}>
<a href={url} class="inline-block w-full h-full">{title}</a>
</button>

View File

@ -3,57 +3,18 @@ import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const news = defineCollection({
loader: glob({
pattern: "**/*.md",
base: "./src/content/news",
generateId: ({ data, entry }) => `${data.locale}/${data.slug ?? entry.replace(/\.md$/, "")}`,
}),
schema: ({ image }) => z.object({
locale: z.string().describe("News main language"),
loader: glob({ pattern: "**/*.md", base: "./src/data/news" }),
schema: z.object({
title: z.string(),
slug: z.string(),
date: z.date(),
draft: z.boolean().optional(),
place: z.string().optional(),
order: z.number().optional(),
city: z.string().optional(),
state: z.string().optional(),
country: z.string().optional(),
thumbnail: image().optional().describe("Main news thumbnail image"),
thumbnail_square: image().optional().describe("Main news thumbnail square image"),
thumbnail: z.string().optional(),
youtube: z.string().optional(),
tags: z.array(z.string()).optional().describe("News tags"),
gallery: z.array(z.object({
image: image().optional(),
text: z.string().optional(),
text_alt: z.string().optional(),
})).optional().nullable()
gallery: z.string().array().optional().nullable()
}),
});
const editorial = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/editorial" }),
schema: ({ image }) => z.object({
locale: z.string().describe("Editorial main language"),
title: z.string(),
slug: z.string(),
date: z.date(),
draft: z.boolean().optional(),
place: z.string().optional(),
order: z.number().optional(),
city: z.string().optional(),
state: z.string().optional(),
country: z.string().optional(),
thumbnail: image().optional().describe("Main editorial thumbnail image"),
thumbnail_square: image().optional().describe("Main editorial thumbnail square image"),
youtube: z.string().optional(),
tags: z.array(z.string()).optional().describe("Editorial tags"),
gallery: z.array(z.object({
image: image().optional(),
text: z.string().optional(),
text_alt: z.string().optional(),
})).optional().nullable()
}),
});
export const collections = { news, editorial };
export const collections = { news };

View File

@ -1,69 +0,0 @@
import { defineCollection } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const news = defineCollection({
//loader: glob({ pattern: "**/*.md", base: "./src/content/news" }),
loader: async () => {
const response = await fetch("http://localhost:3000/api/news?depth=1&draft=false&locale=es&trash=false");
const rawData = await response.json();
return rawData.docs.map((item: any) => {
item.id = item.id.toString()
return item
});
},
schema: ({ image }) => z.object({
locale: z.string().describe("News main language"),
title: z.string(),
date: z.string(),
slug: z.string(),
draft: z.boolean().optional().nullable(),
place: z.string().optional().nullable(),
order: z.number().optional().nullable(),
city: z.string().optional().nullable(),
state: z.string().optional().nullable(),
country: z.string().optional().nullable(),
thumbnail: z.object({
image: image().optional().nullable(),
url: z.string().optional().nullable()
}).optional().nullable().describe("Main news thumbnail image"),
//thumbnail_square: image().optional().describe("Main news thumbnail square image"),
youtube: z.string().optional(),
tags: z.array(z.string()).optional().describe("News tags"),
gallery: z.array(z.object({
image: z.object({
url: z.string().optional().nullable()
}).optional().nullable()
})).optional().nullable(),
body: z.string().nullable().optional()
}),
});
const editorial = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/editorial" }),
schema: ({ image }) => z.object({
locale: z.string().describe("Editorial main language"),
title: z.string(),
date: z.date(),
slug: z.string(),
draft: z.boolean().optional(),
place: z.string().optional(),
order: z.number().optional(),
city: z.string().optional(),
state: z.string().optional(),
country: z.string().optional(),
thumbnail: image().optional().describe("Main editorial thumbnail image"),
thumbnail_square: image().optional().describe("Main editorial thumbnail square image"),
youtube: z.string().optional(),
tags: z.array(z.string()).optional().describe("Editorial tags"),
gallery: z.array(z.object({
image: image().optional(),
text: z.string().optional(),
text_alt: z.string().optional(),
})).optional().nullable()
}),
});
export const collections = { news, editorial };

View File

@ -1,42 +0,0 @@
---
locale: en
title: 'The True Nature of Ideas'
date: 2026-06-29
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-06-29-the-true-nature-of-ideas
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# The True Nature of Ideas
Election campaigns are often filled with promises, slogans, and carefully crafted speeches designed to win the citizens vote. However, once the political race is over, the moment comes when ideas leave the theoretical realm and begin to manifest themselves in reality. I firmly believe that the true nature of an ideology can be discerned precisely at that moment: not by what it promises, but by the behavior it inspires in its leaders, its supporters, and those who act on its behalf.
Ideologies should not be judged by the beauty of their rhetoric, but by their concrete results.
From this perspective, one of the clearest indicators is to observe how different political sectors react when they lose power or when the outcome of an election is unfavorable to them. In various Latin American countries—such as Chile, Bolivia, Honduras, and Ecuador—a frequently recurring phenomenon can be observed: when a left-wing government is replaced by a right-wing one, the response is not institutional acceptance, but rather unrest, riots, and actions aimed at disrupting public order. In contrast, when election results favor the left, normality prevails. The vast majority of the right accepts the results and respects order without resorting to violence as a tool of political pressure; the few isolated incidents of unrest stem from reckless individuals or provocateurs infiltrated to sow destabilization.
This profound difference and moral asymmetry is clearly evident in the security measures surrounding leaders during political races.
I remember perfectly well the harsh criticism directed at the Colombian president for attending his rallies surrounded by strict security measures and armored protection, which was used to portray him as someone who was afraid. However, that leader had to protect his life because there were specific death threats against him. In contrast, the left-wing candidate walked freely and without fear. The explanation is purely logical: his supporters would never attack their own leader, and those of us on the right are not murderers, nor do we use the physical elimination of our adversaries as a tool of political action. It is in this need for protection that it becomes clear which side harbors a propensity for harm and evil.
This is not about making absolute generalizations; I recognize that not everyone who identifies with the left engages in such behavior. Many citizens support these movements in good faith, convinced that they represent a path toward a more just society. However, they are misled by ideological positions that promise social justice, but they are completely unaware of their real consequences.
The examples in the region are painful, obvious, and indisputable.
Cuba, which was once one of the most prosperous and advanced islands in the continent, is now sinking into deep economic and social decline, lacking even basic services such as electricity. Nicaragua and Venezuela have followed the same path of institutional and economic destruction.
This latter nation is currently undergoing a crucial transition; and it is my fervent hope that this process may come to full completion so that the Venezuelan people may break their chains, restore their democratic institutions, and once again live in absolute and total freedom.
Recent history provides ample examples with which to evaluate each model; therefore, I reaffirm my conviction that the defense of freedom, the Rule of Law, and peaceful coexistence are the indispensable pillars for building truly prosperous societies.
**Dr. José Benjamín Pérez Matos**<br>
President<br>

View File

@ -1,112 +0,0 @@
---
locale: en
title: 'Temporary Peace or the Beginning of a Global Realignment in the Middle East?'
date: 2026-07-26
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-07-26-temporary-peace-or-the-beginning-of-a-global-realignment-in-the-middle-east
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# Temporary Peace or the Beginning of a Global Realignment in the Middle East?
The events of recent weeks in the Middle East have once again placed the region at the center of international attention. The recent pause in U.S. bombings on Iranian territory, after nearly two weeks of continuous hostilities, has opened a waiting period that many analysts are presenting as a possible step toward an instance of strategic diplomacy.
The technical talks held in Oman and the efforts aimed at stabilizing transit through the Strait of Hormuz, appear to reflect the intention to reduce tension and avoid an escalation that would further jeopardize regional stability and international trade.
However, for **Dr. José Benjamín Pérez Matos**, behind these diplomatic efforts lies a much deeper and more transcendent reality that cannot be understood solely from the perspective of international politics.
Referring to the moment the region is going through, he stated:
**"Now, these days, the United States is still at war with Iran; and the latest thing I heard: it seems they are going to leave it at that, when they should completely exterminate the problem at its root! If they keep dropping a couple of little bombs and then withdraw, later on they will do the same thing. See? These are seemingly temporary peace processes, but evil remains there."**
These words invite us to distinguish between a circumstantial peace and the definitive elimination of the causes that give rise to the conflict. From this perspective, a temporary interruption of military operations does not necessarily constitute the resolution of the problem, but merely a pause within a process that still remains open.
The very evolution of international events confirms the fragility of the current scenario.
While the United States military suspended its airstrikes after 13 consecutive nights of bombings, uncertainty continues to prevail in the international community. The outlook remains open, and the major powers continue to evaluate their next steps in a region whose stability remains decisive for the global geopolitical balance.
In this context, the President of the United States, Donald Trump, has maintained a position that combines diplomatic pressure with the possibility of reaching an understanding with Tehran.
On the one hand, he has issued severe warnings to the Iranian regime; on the other, he has publicly fueled expectations of a negotiation; stating, before the media gathered in the Oval Office:
“We can negotiate with them, which were also doing right now.”
However, the president himself also tempered those expectations by stating that he does not believe Iran is “ready” yet to reach a final agreement.
At the same time, he acknowledged:
“I think theyre getting more and more serious as the days go by, for maybe the obvious reason.”
The statements of the U.S. president reflect a scenario in which military pressure, diplomatic negotiation, and uncertainty over the definitive outcome of the conflict continue to coexist.
## The Prophetic Background of the Crisis
For **Dr. José Benjamín Pérez Matos**, fully understanding what is taking place between Washington and Tehran requires looking beyond a simple journalistic account and analyzing events from a broader perspective.
In this regard, he maintained that it is necessary to observe the geopolitical map through the lens of the biblical prophecies contained in chapter 2 of the book of Daniel.
In that context, he stated:
**“That kingdom of Persia, that kingdom of Iran, must be completely removed, so that only the kingdom of Rome remains; that is, the kingdom of the feet of iron and clay.”**
From this theological perspective, the final world order does not contemplate the coexistence of different geopolitical blocs developing simultaneously as parallel structures of power.
As **Dr. José Benjamín Pérez Matos** explained:
**"There can't also be a kingdom running parallel to that last kingdom of the legs of iron and clay; only that one must remain. Therefore, those remnants that remain of that Persian empire: Iran, must be removed. And that will be a very great sign when it happens. Which will mark a definitive prophetic milestone in these days."**
From this interpretation, current events transcend the strictly political and military realm, to become part of a broader historical process whose development, according to **Dr. José Benjamín Pérez Matos**, forms part of the progressive fulfillment of biblical prophecies.
## High-Pressure Diplomacy at the White House
It is precisely in this scenario of maximum tension that the visit of the Prime Minister of the State of Israel, Benjamin Netanyahu, to the United States takes on particular relevance.
Although the official reason for the trip is to participate in the funeral services of the late Senator Lindsey Graham —described by the Prime Ministers Office as “a friend of Israel”— the scheduled meetings between the highest authorities of both countries will focus on the immediate future of the Middle East and the strategic challenges facing the region.
Referring to these meetings, **Dr. José Benjamín Pérez Matos** stated:
**"Now, we hope that... They have a meeting these days in Washington, or in New York. The Prime Minister of Israel, Benjamin Netanyahu, is going to be there, and they are going to talk about all of that as well."**
The importance of these conversations lies in the fact that Israel has cautiously observed the development of the U.S. attacks, fully aware that this crisis involves control of a strategic maritime route through which approximately twenty percent of the oil traded in the world passes.
Consequently, the reactivation of bilateral contacts between Washington and Jerusalem seeks to align stances against a common enemy and coordinate the next strategic steps in a scenario marked by the ongoing volatility of the Persian Gulf.
## The Race Against the Electoral Clock
In addition to the military and diplomatic components, the time factor appears as one of the most sensitive elements of the current situation.
The proximity of upcoming electoral processes introduces a political variable that could significantly modify the geopolitical scenario.
Domestically in the United States, President Donald Trump has downplayed speculation regarding the potential electoral impact of rising fuel prices.
Before the press, he stated:
“Despite what everyone says about the election, Im not in a hurry… Elections take care of themselves.”
However, **Dr. José Benjamín Pérez Matos** drew attention to another electoral process that, from his perspective, is of even greater importance for the immediate future of the Middle East: the elections scheduled in Israel.
On this point, he said:
**“But look at what is happening in Israel: now there are going to be elections in October. They have to hurry. Because if a prime minister comes along who is not in favor (and aligned with that line of thought) of the religious world, of the Orthodox, and someone else comes along and wins the elections in October, things are going to get difficult for Israel. In other words, they should instead take advantage of the time remaining in his term, just in case there is a surprise in October.”**
These words reflect the importance that **Dr. José Benjamín Pérez Matos** attributes to the Israeli political calendar within the regional context. From this perspective, the time available to consolidate certain strategic decisions is limited and could be conditioned by the outcome of the upcoming elections.
As **Dr. José Benjamín Pérez Matos** himself noted: “The window to consolidate positions of strength is extremely narrow. Political actors must act swiftly in the face of the possibility of an electoral reversal or a surprise at the polls.”
This warning constitutes the natural conclusion of the geopolitical analysis developed throughout this reflection. However, for **Dr. José Benjamín Pérez Matos**, international events cannot be understood solely from the perspective of human decisions, diplomacy, or military strategy.
Above all those factors, there exists a higher purpose whose fulfillment, he states, continues to unfold according to the prophetic program revealed in the Scriptures.
For this reason, he concluded his statement with a reminder:
**“But remember that everything is, and everything will be fulfilled, according to the prophecies. None of this rests in human hands.”**
From the perspective presented by **Dr. José Benjamín Pérez Matos**, the events currently unfolding in the Middle East transcend the international situation and constitute part of a broader historical process. Beyond diplomatic negotiations, military decisions, or political changes that may occur in the various countries involved, the definitive outcome will be determined by the fulfillment of the prophetic purpose which, according to the Scriptures, will lead to the establishment of the final order announced in the book of Daniel.

View File

@ -1,43 +0,0 @@
---
locale: es
title: 'La verdadera naturaleza de las ideas'
date: 2026-06-29
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-06-29-la-verdadera-naturaleza-de-las-ideas
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# La verdadera naturaleza de las ideas
Las campañas electorales suelen estar cargadas de promesas, consignas y discursos cuidadosamente elaborados para conquistar el voto ciudadano. Sin embargo, una vez finalizada la contienda política, llega el momento en que las ideas abandonan el plano teórico y comienzan a manifestarse en la realidad. Sostengo firmemente que es precisamente en ese momento cuando resulta posible conocer la verdadera naturaleza de una ideología: no por lo que promete, sino por la conducta que inspira en sus dirigentes, en sus militantes y en quienes actúan en su nombre.
Las ideologías no deben ser juzgadas por la belleza de sus discursos, sino por sus resultados concretos.
Desde esta perspectiva, uno de los indicadores más claros consiste en observar cómo reaccionan los distintos sectores políticos cuando pierden el poder o cuando el resultado de una elección les resulta adverso. En diversos países de América Latina —como Chile, Bolivia, Honduras y Ecuador— se advierte un fenómeno que se repite con frecuencia: cuando un gobierno de izquierda es reemplazado por uno de derecha, la respuesta no es la aceptación institucional, sino la agitación, los disturbios y las acciones destinadas a alterar el orden público. En contraste, cuando los resultados electorales favorecen a la izquierda, la normalidad prevalece. La derecha, en su gran mayoría, asume los resultados y respeta el orden sin recurrir a la violencia como instrumento de presión política; los pocos desmanes aislados provienen de individuos torpes o de provocadores infiltrados para generar desestabilización.
Esta profunda diferencia y asimetría moral se manifiesta con total claridad en la seguridad de los líderes durante las contiendas políticas.
Recuerdo perfectamente las duras críticas hacia el presidente de Colombia por asistir a sus mítines rodeado de estrictas medidas de protección y blindajes, utilizándolo para presentarlo como alguien temeroso. Sin embargo, ese dirigente debía proteger su vida porque existían amenazas concretas de muerte en su contra. Por el contrario, el candidato de izquierda caminaba libremente y sin temores. La explicación es de pura lógica: sus seguidores jamás atentarían contra su propio líder, y los sectores de derecha no somos asesinos ni utilizamos la eliminación física del adversario como herramienta de acción política. Es en esa necesidad de protección donde se evidencia de qué lado se encuentra la inclinación hacia el daño y el mal.
No se trata de generalizar de forma absoluta; reconozco que no todos quienes se identifican con la izquierda participan de esas conductas. Muchos ciudadanos apoyan estos movimientos de buena fe, convencidos de que representan un camino hacia una sociedad más justa. Sin embargo, marchan engañados por planteamientos ideológicos que prometen justicia social, pero cuyas consecuencias reales ignoran por completo.
Los espejos de la región son dolorosos, evidentes y no admiten discusión.
Cuba, que en su momento fue una de las islas más prósperas y adelantadas del continente, hoy se hunde en un profundo deterioro económico y social, careciendo incluso de servicios básicos, como el suministro de energía eléctrica.
El mismo libreto de destrucción institucional y económica lo han seguido Nicaragua y Venezuela.
Esta última nación se encuentra hoy en una transición crucial; y mi deseo ferviente es que este proceso culmine plenamente para que el pueblo venezolano rompa sus cadenas, recupere sus instituciones democráticas y vuelva a vivir en absoluta y total libertad.
La historia reciente ofrece ejemplos suficientes para evaluar cada modelo; por ello, reafirmo mi convicción de que la defensa de la libertad, el Estado de Derecho y la convivencia pacífica son los pilares indispensables para construir sociedades verdaderamente prósperas.
**Dr. José Benjamín Pérez Matos**<br>
Presidente<br>

View File

@ -1,114 +0,0 @@
---
locale: es
title: '¿Paz temporal o el inicio de un reordenamiento global en Oriente Medio?'
date: 2026-07-26
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-07-26-paz-temporal-o-el-inicio-de-un-reordenamiento-global-en-oriente-medio
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# ¿Paz temporal o el inicio de un reordenamiento global en Oriente Medio?
Los acontecimientos de las últimas semanas en Oriente Medio han vuelto a colocar a la región en el centro de la atención internacional. La reciente pausa en los bombardeos estadounidenses sobre territorio iraní, tras casi dos semanas de hostilidades continuas, ha abierto un compás de espera que numerosos analistas presentan como un posible avance hacia una instancia de diplomacia estratégica.
Las conversaciones técnicas desarrolladas en Omán, y los esfuerzos orientados a estabilizar el tránsito por el estrecho de Ormuz, parecen reflejar la intención de reducir la tensión y evitar una escalada que comprometa aún más la estabilidad regional y el comercio internacional.
Sin embargo, para el **Dr. José Benjamín Pérez Matos**, detrás de esos movimientos diplomáticos existe una realidad mucho más profunda y trascendente que no puede ser comprendida únicamente desde la perspectiva de la política internacional.
Al referirse al momento que atraviesa la región, expresó:
**«Ahora, en estos días tiene Estados Unidos esa guerra ahí con Irán todavía; y lo último que escuché: que parece que hasta ahí van a dejarlo, ¡cuando deben exterminar por completo, de raíz, el problema! Si siguen tirando dos bombitas y se retiran, van ellos, más adelante, a hacer lo mismo. ¿Ven? Son procesos aparentemente de paz temporal, pero sigue el mal allí»**.
Estas palabras invitan a distinguir entre una paz circunstancial y la eliminación definitiva de las causas que originan el conflicto. Desde esta perspectiva, una interrupción temporal de las operaciones militares no constituye necesariamente la resolución del problema, sino apenas una pausa dentro de un proceso que todavía permanece abierto.
La propia evolución de los acontecimientos internacionales confirma la fragilidad del escenario actual.
Mientras el ejército de los Estados Unidos suspendió sus ataques aéreos tras trece noches consecutivas de bombardeos, la incertidumbre continúa predominando en la comunidad internacional. El panorama permanece abierto, y las principales potencias siguen evaluando los próximos pasos, en una región cuya estabilidad continúa siendo determinante para el equilibrio geopolítico mundial.
En este contexto, el presidente de los Estados Unidos, Donald Trump, ha mantenido una postura que combina la presión diplomática con la posibilidad de alcanzar un entendimiento con Teherán.
Por un lado, ha formulado severas advertencias al régimen iraní; por el otro, ha alimentado públicamente las expectativas de una negociación; al afirmar, ante los medios reunidos en la Oficina Oval:
«Podemos negociar con ellos, que es lo que también estamos haciendo ahora mismo».
Sin embargo, el propio mandatario también moderó esas expectativas al declarar que no considera que Irán esté “ready (o listo) todavía” para alcanzar un acuerdo definitivo.
Al mismo tiempo, reconoció que:
«Creo que se están poniendo cada vez más serios a medida que pasan los días; quizá por la razón obvia».
Las declaraciones del presidente estadounidense reflejan un escenario en el que continúan coexistiendo la presión militar, la negociación diplomática y la incertidumbre sobre el desenlace definitivo del conflicto.
## El trasfondo profético de la crisis
Para el **Dr. José Benjamín Pérez Matos**, comprender plenamente lo que ocurre entre Washington y Teherán exige levantar la mirada por encima de la simple crónica periodística y analizar los acontecimientos desde una perspectiva más amplia.
En ese sentido sostuvo que resulta necesario observar el mapa geopolítico a través del lente de las profecías bíblicas contenidas en el capítulo 2 del libro de Daniel.
En ese contexto afirmó:
**«Ese reino de Persia, ese reino de Irán, tiene que ser quitado completamente, para que solamente quede el reino de Roma; o sea, el reino de los pies de hierro y barro cocido»**.
Bajo esta perspectiva teológica, el orden mundial final no contempla la coexistencia de distintos bloques geopolíticos desarrollándose simultáneamente como estructuras paralelas de poder.
Como explicó el **Dr. José Benjamín Pérez Matos**:
**«No puede haber un reino también, corriendo paralelo a ese reino último de las piernas de hierro y barro cocido; solamente tiene que quedar ese. Por lo tanto, esos residuos que están quedando de ese imperio de Persia: Irán, tiene que ser quitado. Y eso es una señal muy grande, cuando eso suceda. Lo cual marcará un hito profético definitivo en estos días»**.
Desde esta interpretación, los acontecimientos actuales trascienden el ámbito estrictamente político y militar, para insertarse dentro de un proceso histórico de mayor alcance, cuyo desarrollo, según el **Dr. José Benjamín Pérez Matos**, forma parte del cumplimiento progresivo de las profecías bíblicas.
## Diplomacia de alta presión en la Casa Blanca
Es precisamente en este escenario de máxima tensión donde adquiere una relevancia particular la visita del primer ministro del Estado de Israel, Benjamín Netanyahu, a los Estados Unidos.
Aunque el motivo oficial del viaje es participar de las honras fúnebres del fallecido senador Lindsey Graham —descrito por la Oficina del Primer Ministro como “un amigo de Israel”—, las reuniones previstas entre las máximas autoridades de ambos países tendrán como eje el futuro inmediato de Oriente Medio y los desafíos estratégicos que enfrenta la región.
Al referirse a esos encuentros, el **Dr. José Benjamín Pérez Matos** manifestó:
**«Ahora, esperemos que… tienen una reunión en estos días en Washington, o en Nueva York. El primer ministro de Israel, Benjamín Netanyahu, va a estar allá; y van a hablar sobre todo eso también»**.
La importancia de estas conversaciones radica en que Israel ha observado con cautela el desarrollo de los ataques estadounidenses, plenamente consciente de que en esta crisis se encuentra comprometido el control de una vía marítima estratégica por la que circula aproximadamente el veinte por ciento del petróleo que se comercializa en el mundo.
En consecuencia, la reactivación de los contactos bilaterales entre Washington y Jerusalén busca unificar criterios frente a un enemigo común y coordinar los próximos pasos estratégicos en un escenario marcado por la permanente volatilidad del golfo Pérsico.
## La carrera contra el reloj electoral
Además del componente militar y diplomático, el factor tiempo aparece como uno de los elementos más sensibles de la coyuntura actual.
La proximidad de los próximos procesos electorales introduce una variable política que puede modificar significativamente el escenario geopolítico.
En el plano interno de los Estados Unidos, el presidente Donald Trump ha restado importancia a las especulaciones sobre el eventual impacto electoral del aumento de los precios de los combustibles.
Ante la prensa afirmó:
«A pesar de lo que todo el mundo dice sobre las elecciones, no tengo prisa... Las elecciones se resuelven solas».
Sin embargo, el **Dr. José Benjamín Pérez Matos** llamó la atención sobre otro proceso electoral que, desde su perspectiva, reviste una importancia todavía mayor para el futuro inmediato de Oriente Medio: las elecciones previstas en Israel.
Sobre este punto expresó:
**«Pero miren lo que pasa en Israel: ahora van a haber elecciones, en octubre. Tienen que apurarse. Porque si les cae allí un primer ministro que no esté en favor (y en esa línea de pensamiento) del mundo religioso, de los ortodoxos, y cae otro y gana las elecciones en octubre, se le van a poner las cosas difíciles a Israel. O sea, más bien debe de aprovechar este término que queda, por si acaso pasa una sorpresa ahí en octubre»**.
Estas palabras reflejan la importancia que el **Dr. José Benjamín Pérez Matos** atribuye al calendario político israelí dentro del contexto regional. Desde esta perspectiva, el tiempo disponible para consolidar determinadas decisiones estratégicas resulta limitado y podría verse condicionado por el resultado de los próximos comicios.
Como señaló el propio **Dr. José Benjamín Pérez Matos**:
**«el margen de maniobra para consolidar las posiciones de fuerza es sumamente estrecho. Los actores políticos deben actuar con presteza ante la posibilidad de un vuelco electoral o una sorpresa en las urnas»**.
Esta advertencia constituye el cierre natural del análisis geopolítico desarrollado a lo largo de esta reflexión. Sin embargo, para el **Dr. José Benjamín Pérez Matos**, los acontecimientos internacionales no pueden comprenderse únicamente desde la perspectiva de las decisiones humanas, de la diplomacia o de la estrategia militar.
Por encima de todos esos factores existe un propósito superior cuyo cumplimiento, afirma, continúa desarrollándose conforme al programa profético revelado en las Escrituras.
Por ello concluyó su exposición recordando:
**«Pero recuerden que todo es, y todo se cumplirá, conforme a las profecías. Nada de esto queda en manos humanas»**.
Desde la perspectiva presentada por el **Dr. José Benjamín Pérez Matos**, los acontecimientos que hoy se desarrollan en Oriente Medio trascienden la coyuntura internacional y constituyen parte de un proceso histórico de mayor alcance. Más allá de las negociaciones diplomáticas, de las decisiones militares o de los cambios políticos que puedan producirse en los distintos países involucrados, el desenlace definitivo responderá al cumplimiento del propósito profético que, según las Escrituras, habrá de conducir al establecimiento del orden final anunciado en el libro de Daniel.

View File

@ -1,115 +0,0 @@
---
locale: es
title: 'Un mundo en transición: guerras, estremecimientos y el cambio de reino'
date: 2026-08-16
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-08-16-un-mundo-en-transicion-guerras-estremecimientos-y-el-cambio-de-reino
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# Un mundo en transición: guerras, estremecimientos y el cambio de reino
### Las guerras que continúan abiertas, la situación de Irán y los grandes fenómenos naturales que estremecen distintas regiones del planeta, forman parte, desde la perspectiva profética expuesta por el Dr. José Benjamín Pérez Matos, de un escenario de transición que la humanidad debe observar con atención. En medio de ese proceso, Israel y Jerusalén ocupan un lugar central en el cumplimiento de las profecías y en la futura instauración del Reino del Mesías.
El mundo atraviesa una etapa marcada simultáneamente por conflictos geopolíticos, guerras y fenómenos naturales de una magnitud que obliga a mirar con atención lo que está sucediendo.
La guerra entre Rusia y Ucrania continúa; persisten las tensiones y el conflicto en torno a Irán; y, paralelamente, terremotos, huracanes, ciclones, tormentas, tsunamis, maremotos y erupciones volcánicas, parecen recordarnos constantemente la fragilidad del orden humano.
El **Dr. José Benjamín Pérez Matos** ha llamado a observar estos acontecimientos desde una perspectiva profética, entendiendo que no se trata simplemente de hechos aislados, sino de manifestaciones que acompañan un proceso mucho mayor: el cambio de los reinos gentiles al Reino del Mesías.
En relación con la situación de Irán, su planteamiento ha sido categórico:
**«Miren las situaciones del mundo, las guerras que hay: Todavía Rusia está en guerra con Ucrania. Ves la guerra que hay con Irán; la cual deben de terminar ya, de una vez y por todas, ese reino, para que puedan entrar esas otras fases y etapas físicas como nación, pueda entrar el próximo paso».**
Según esta interpretación, existe una secuencia profética que debe cumplirse. Por ello, el **Dr. Pérez Matos** sostiene:
**«No se puede pasar a los próximos pasos si no se termina ese paso de eliminar ese reino (que todavía está dando pataletas allí), para que quede solamente el reino de los pies de hierro y barro cocido. Ya eso lo hemos venido hablando».**
Esta referencia remite directamente a la imagen profética presentada en el capítulo 2 del libro de Daniel y a la sucesión de reinos representada en ella. Desde esa perspectiva, los acontecimientos contemporáneos no pueden ser considerados únicamente desde los cálculos políticos, diplomáticos o militares del presente: forman parte de un proceso profético cuyo desarrollo conduce finalmente al establecimiento del Reino del Mesías.
## El llamado a Israel y a sus dirigentes
Dentro de este escenario, Israel ocupa un lugar determinante. El **Dr. José Benjamín Pérez Matos** dirigió particularmente sus palabras al primer ministro israelí, Benjamín Netanyahu:
**«Y los líderes…, tanto el de Israel: Benjamín Netanyahu, primer ministro de Israel, debe de tomar en serio estas palabras, y que termine por completo de sacar ese reino de Persia, para que así puedan entrar esas fases y esas etapas para esta introducción del establecimiento del Reino del Mesías».**
Las decisiones políticas pueden estar acompañadas por alianzas, asesoramiento internacional y cálculos estratégicos. Sin embargo, desde la perspectiva planteada por el **Dr. Pérez Matos**, existe un criterio superior que debe orientar esas decisiones:
**«Y puede haber naciones que le den apoyo y que estén dándole consejos, pero el consejo mejor que pueden ellos recibir es el Consejo de Dios. Y tienen que recibirlo bajo las profecías que fueron habladas. Y tiene que estar basado, todo lo que hagan, en las profecías; no en lo que digan los demás. ¡Y todo le irá bien! Porque todo es un Programa que se está desarrollando, el cual es ese cambio de reino; e Israel tiene una parte muy importante».**
Esa importancia de Israel no se limita a su protagonismo actual en Medio Oriente. Dentro de la visión profética expuesta, su territorio posee una función futura fundamental, con Jerusalén como centro de gobierno:
**«La parte de Israel es, vean, el territorio; y desde allí es que se estará gobernando todo el planeta Tierra completo y el universo completo. Galaxias, todos los planetas…, todo va a ser gobernado, miren, desde ese puntito allí, en donde será la capital del planeta Tierra completo; y de las galaxias y de todo el sistema sería la Tierra; pero en la Tierra estaría la capital, que es Jerusalén».**
Por eso, hablar de Israel y Jerusalén desde esta perspectiva, implica hablar no solamente de la geopolítica contemporánea, sino también del futuro orden anunciado en las profecías.
## El cambio de reino y los estremecimientos de la Tierra
Los grandes cambios históricos nunca han estado exentos de conmociones. Guerras, conflictos y transformaciones profundas acompañaron anteriores cambios de reinos. La pregunta, entonces, es qué ocurrirá en el tránsito definitivo desde los reinos gentiles hacia el Reino del Mesías.
El **Dr. José Benjamín Pérez Matos** lo expresa de esta manera:
**«Ahora, para esos cambios de reino, vean, hubo situaciones, guerras y todo eso. Y dijimos: ¿Cómo será para este cambio de reino ahora, de los gentiles al Reino del Mesías? Pues miren, ahí va a estar todo siendo cumplido. Lo que fue en parte en esos cambios de reino, ahora estaremos viéndolo en todas esas manifestaciones. Tanto la Tierra gimiendo…».**
Esta imagen de una creación que gime encuentra su referencia en Romanos, capítulo 8:
**«Romanos, capítulo 8, nos habla de eso también: “Esperando la manifestación de los hijos de Dios”».**
Desde esta lectura, los acontecimientos de la naturaleza adquieren también una dimensión profética. No deben ser observados únicamente como sucesos físicos separados unos de otros, sino como parte de un escenario que llama a la humanidad —y especialmente al pueblo de Dios— a estar atento.
**«Y vemos volcanes, vemos terremotos, maremotos, vemos tsunamis, tormentas, huracanes, ciclones y todas esas cosas, que van siendo manifestados en este tiempo cada vez peores. Huracanes que nunca se han visto, de una fuerza sobrenatural…, que nunca jamás ha pasado un huracán así, y ahora lo estamos viendo».**
## Venezuela, Colombia y un mundo que no está preparado
Los recientes terremotos ocurridos en Venezuela y Colombia fueron señalados por el **Dr. José Benjamín Pérez Matos** como ejemplos concretos de esos estremecimientos:
**«Terremotos…, miren el de Venezuela, lo que ocurrió allí. Ahora con el que ocurrió en Colombia, en donde no se esperaban eso. Y no tiene que ver con el de Venezuela, fueron dos fallas distintas, totalmente distintas».**
Sobre el terremoto de Colombia, agregó:
**«Y una…, algo curioso: dicen que eso fue de arriba a abajo; cubrió toda esa parte izquierda (mirando el mapa de frente), de arriba hacia abajo: desde Medellín, bajó por ahí, todo, Cali, hasta abajo. Y vemos cómo el mundo, pues… como que no está preparado para eso».**
Más allá de la dimensión profética con la cual se interpreten estos acontecimientos, existe una dimensión profundamente humana que nunca debe quedar relegada. Los terremotos destruyen viviendas, alteran comunidades y, sobre todo, cobran vidas.
**«Y en esos terremotos, pues hay muchas pérdidas también, humanas. Ya van… unos ciento y pico vi anoche, por ahí».**
Ante ese sufrimiento, las palabras deben convertirse también en solidaridad. El **Dr. José Benjamín Pérez Matos** expresó su acompañamiento al pueblo colombiano y, especialmente, a las familias que han perdido seres queridos:
**«Y nos unimos al pueblo colombiano. Y deseamos que Dios conforte los corazones de aquellos que han perdido sus seres queridos; y que Dios les dé la fortaleza; y que escuchen esa Voz del Ángel Fuerte, la Voz del Ángel con el Evangelio Eterno, diciendo: “Temed a Dios, y dadle gloria, porque la hora de Su juicio ha llegado”. Apocalipsis, capítulo 14».**
## Un llamado a prepararse
La lectura profética de estos acontecimientos no conduce únicamente a interpretar lo que sucede. Implica también preparación.
El **Dr. José Benjamín Pérez Matos** advierte:
**«Porque la ira de Dios estará siendo manifestada en este planeta Tierra ya durante la gran tribulación, pero antes hay unos estremecimientos, en donde el pueblo de Dios tiene que estar listo para recibir las bendiciones».**
En ese marco aparece también la Fiesta de las Trompetas y el entrelace profético con aquello que habrá de manifestarse durante el Reino Milenial:
**«Todo se va a pintar en ese entrelace de la Fiesta de las Trompetas. Se va a estar viendo un entrelace de cómo será el Reino Milenial, donde lo que se van a ver, en su mayoría o totalmente, son carpas».**
La referencia a las carpas posee, además, una dimensión práctica frente a los grandes movimientos de la tierra:
**«Porque no se va a meter usted debajo de un edificio que esté todo agrietado. Y lo más cómodo es una carpa: caseta de campaña, una carpa, cosas prácticas; que si hay un movimiento, pues eso no se le cae encima».**
La conclusión, por tanto, no es solamente contemplativa. Los acontecimientos atmosféricos y naturales constituyen, dentro de esta perspectiva, un aviso:
**«O sea que Dios nos está dando un aviso con todos esos eventos atmosféricos y de la naturaleza que están ocurriendo, para que el pueblo de Dios se prepare».**
Vivimos en un mundo sometido a profundas tensiones: guerras que no terminan, estructuras de poder que se enfrentan, sociedades expuestas a fenómenos naturales devastadores, y una humanidad que —muchas veces— descubre que sus sistemas y ciudades no están preparados para responder a acontecimientos de gran magnitud.
Pero, desde la perspectiva profética, estos estremecimientos no anuncian solamente destrucción; también señalan transición. Los reinos humanos son temporales; el Programa de Dios continúa desarrollándose; Israel conserva su lugar dentro de ese Programa; Jerusalén tiene reservado un papel central; y el pueblo de Dios está llamado a comprender el tiempo, escuchar el aviso y prepararse.
Porque, como concluye el **Dr. José Benjamín Pérez Matos**:
**«Y cuando se habla de terremotos, se habla de liberación».**

View File

@ -1,42 +0,0 @@
---
locale: fr
title: 'La véritable nature des idées'
date: 2026-06-29
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-06-29-la-veritable-nature-des-idees
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# La véritable nature des idées
Les campagnes électorales sont souvent chargées de promesses, de slogans et de discours soigneusement élaborés pour conquérir le vote des citoyens. Cependant, une fois la lutte politique terminée, vient le moment où les idées quittent le plan théorique et commencent à se manifester dans la réalité. Je soutiens fermement que c'est précisément à ce moment-là qu'il devient possible de connaître la véritable nature d'une idéologie : non pas par ce qu'elle promet, mais par la conduite qu'elle inspire chez ses dirigeants, ses militants et ceux qui agissent en son nom.
Les idéologies ne doivent pas être jugées par la beauté de leurs discours, mais par leurs résultats concrets.
Dans cette perspective, l'un des indicateurs les plus clairs consiste à observer comment réagissent les différents secteurs politiques lorsqu'ils perdent le pouvoir ou lorsque le résultat d'une élection leur est défavorable. Dans divers pays d'Amérique latine —comme le Chili, la Bolivie, le Honduras et l'Équateur— on observe un phénomène qui se répète fréquemment : lorsquun gouvernement de gauche est remplacé par un gouvernement de droite, la réponse n'est pas l'acceptation institutionnelle, mais l'agitation, les troubles et les actions destinées à altérer l'ordre public. En revanche, lorsque les résultats électoraux favorisent la gauche, la normalité prévaut. La droite, dans sa grande majorité, accepte les résultats et respecte l'ordre sans recourir à la violence comme instrument de pression politique ; les rares débordements isolés proviennent d'individus malavisés ou de provocateurs infiltrés pour générer de la déstabilisation.
Cette profonde différence et asymétrie morale se manifeste avec une clarté totale dans la sécurité des dirigeants pendant les campagnes politiques.
Je me souviens parfaitement des vives critiques envers le président de la Colombie pour avoir assisté à ses réunions publiques entouré dun important dispositif de sécurité, l'utilisant pour le présenter comme quelqu'un de craintif. Cependant, ce dirigeant devait protéger sa vie car il existait des menaces concrètes de mort à son encontre. Au contraire, le candidat de gauche marchait librement et sans crainte. L'explication est purement logique : ses partisans n'attaqueraient jamais leur propre leader, et nous, dans les secteurs de droite, ne sommes pas des assassins et n'avons pas recours à l'élimination physique de l'adversaire comme outil d'action politique. C'est dans ce besoin de protection que se révèle de quel côté se trouve l'inclination vers le mal et la violence.
Il ne s'agit pas de généraliser de manière absolue ; je reconnais que tous ceux qui s'identifient à la gauche ne participent pas à ces comportements. Beaucoup de citoyens soutiennent ces mouvements de bonne foi, convaincus qu'ils représentent un chemin vers une société plus juste. Cependant, ils sont guides par des idées trompeuses qui promettent la justice sociale, mais dont ils ignorent complètement les conséquences réelles.
Les exemples de la région son éloquents, douloureux et ne souffrent aucune discussion.
Cuba, qui à l'époque était l'une des îles les plus prospères et avancées du continent, sombre aujourd'hui dans un profond déclin économique et social, manquant même de services de base, comme l'approvisionnement en électricité. Le même scénario de destruction institutionnelle et économique a été suivi par le Nicaragua et le Venezuela.
Cette dernière nation se trouve aujourd'hui dans une transition cruciale ; et mon ardent désir est que ce processus aboutisse pleinement afin que le peuple vénézuélien rompe ses chaînes, récupère ses institutions démocratiques et vive à nouveau en liberté totale.
L'histoire récente offre des exemples suffisants pour évaluer chaque modèle ; c'est pourquoi je réaffirme ma conviction que la défense de la liberté, l'État de droit et la coexistence pacifique sont les piliers indispensables pour construire des sociétés véritablement prospères.
**Dr. José Benjamín Pérez Matos**<br>
Président<br>

View File

@ -1,111 +0,0 @@
---
locale: fr
title: Paix temporaire ou le début d'un réaménagement global au Moyen-Orient ?
date: 2026-07-26
order: 1
# city: 'Palmira'
# country: 'CO'
# state: 'Valle del Cauca'
slug: 2026-07-26-paix-temporaire-ou-le-debut-dun-reamenagement-global-au-moyen-orient
thumbnail: 'https://ik.imagekit.io/crpy/tr:w-1280,q-100/comunicado-1.webp'
# tags: [Comunicado, Venezuela, Estados Unidos]
gallery: [
{
image: 'https://ik.imagekit.io/crpy/tr:w-1280,h-900,cm-pad_resize,bg-blurred,q-70/comunicado-1.webp',
},
]
---
# Paix temporaire ou le début d'un réaménagement global au Moyen-Orient ?
Les événements des dernières semaines au Moyen-Orient ont remis la région au centre de l'attention internationale. La récente pause dans les bombardements américains sur le territoire iranien, après près de deux semaines d'hostilités continues, a ouvert une période d'attente que de nombreux analystes présentent comme une possible avancée diplomatique.
Les conversations techniques menées à Oman, et les efforts visant à stabiliser le transit par le détroit d'Ormuz, semblent refléter l'intention de réduire les tensions régionales et d'éviter une escalade qui compromettrait encore plus la stabilité régionale et le commerce international.
Cependant, pour le **Dr. José Benjamín Pérez Matos**, derrière ces mouvements diplomatiques se cache une réalité bien plus profonde et transcendante qui ne peut être comprise uniquement depuis la perspective de la politique internationale.
En se référant au moment que traverse la région, il a exprimé :
**«Maintenant, ces jours-ci, les États-Unis ont encore cette guerre avec l'Iran ; et la dernière chose que j'ai entendue : il semble qu'ils vont s'arrêter là, alors qu'ils devraient éradiquer le problème à la racine, le problème ! S'ils continuent à lancer deux bombes et à se retirer, eux, plus tard, ils vont faire la même chose. Vous voyez ? Ce sont des processus apparemment de paix temporaire, mais le mal démure ».**
Ces mots invitent à distinguer entre une paix circonstancielle et l'élimination définitive les causes à l'origine du conflit. Dans cette perspective, une interruption temporaire des opérations militaires ne constitue pas nécessairement la résolution du problème, mais seulement une pause dans un processus qui reste ouvert.
L'évolution même des événements internationaux confirme la fragilité du scénario actuel.
Alors que larmée américaine a suspendu ses frappes aériennes après treize nuits consécutives de bombardements, l'incertitude continue de prédominer dans la communauté internationale. Le panorama reste ouvert, et les principales puissances continuent d'évaluer les prochaines étapes, dans une région dont la stabilité demeure déterminante pour l'équilibre géopolitique mondial.
Dans ce contexte, le président des États-Unis, Donald Trump, a maintenu une position qui combine la pression diplomatique avec la possibilité d'atteindre un accord avec Téhéran.
D'une part, il a formulé de sévères avertissements au régime iranien ; d'autre part, il a alimenté publiquement les attentes d'une négociation ; en affirmant, devant les médias réunis dans le Bureau ovale :
«Nous pouvons négocier avec eux, ce que nous faisons en ce moment même.»
Cependant, le président lui-même a également tempéré ces attentes en déclarant qu'il ne considère pas que l'Iran soit « prêt » (ready) pour parvenir à un accord définitif.
En même temps, il a reconnu que :
«Je pense qu'ils deviennent de plus en plus sérieux au fil des jours; peut-être pour la raison évidente.»
Les déclarations du président américain reflètent un scénario dans lequel continuent de coexister la pression militaire, la négociation diplomatique et l'incertitude quant à l'issue définitive du conflit.
## Le contexte prophétique de la crise
Pour le **Dr. José Benjamín Pérez Matos**, comprendre pleinement ce qui se passe entre Washington et Téhéran exige d'aller au-delà de la simple chronique journalistique et d'analyser les événements d'une perspective plus large.
En ce sens, il a soutenu qu'il est nécessaire d'observer la carte géopolitique à travers le prisme des prophéties bibliques contenues dans le chapitre 2 du livre de Daniel. Dans ce contexte, il a affirmé :
**«Ce royaume de Perse, ce royaume d'Iran, doit être complètement supprimé, afin qu'il ne reste que le royaume de Rome ; c'est-à-dire, le royaume des pieds de fer et d'argile cuite».**
Dans cette perspective théologique, l'ordre mondial final ne prévoit pas la coexistence de différents blocs géopolitiques se développant simultanément comme des structures parallèles de pouvoir.
Comme l'a expliqué le **Dr. José Benjamín Pérez Matos** :
**«Il ne peut pas y avoir un royaume parallèle à ce dernier royaume des jambes de fer et d'argile cuite ; il ne doit rester que celui-là. Par conséquent, ces résidus qui restent de cet empire de Perse : L'Iran doit être éliminé. Et c'est un très grand signe, quand cela arrivera. Ce qui marquera un jalon prophétique définitif en ces jours».**
Selon cette interprétation, les événements actuels transcendent le domaine strictement politique et militaire, pour s'inscrire dans un processus historique de plus grande envergure, dont le développement, selon le **Dr. José Benjamín Pérez Matos**, fait partie de l'accomplissement progressif des prophéties bibliques.
## Diplomatie de haute pression à la Maison Blanche
C'est précisément dans ce contexte de tension maximale que la visite du Premier ministre de l'État d'Israël, Benjamin Netanyahu, aux États-Unis revêt une importance particulière.
Bien que le motif officiel du voyage soit de participer aux funérailles du sénateur décédé Lindsey Graham —décrit par le Bureau du Premier ministre comme « un ami d'Israël »—, les réunions prévues entre les plus hautes autorités des deux pays auront pour principal sujet l'avenir immédiat du Moyen-Orient et les défis stratégiques auxquels la région est confrontée.
En se référant à ces rencontres, le **Dr. José Benjamín Pérez Matos** a déclaré :
**«Maintenant, espérons que… ils ont une réunion ces jours-ci à Washington, ou à New York. Le Premier ministre israélien, Benyamin Netanyahou, va être là-bas ; et ils vont parler de tout cela aussi».**
L'importance de ces conversations réside dans le fait qu'Israël a observé avec prudence le développement des attaques américaines, pleinement conscient que dans cette crise est engagé le contrôle d'une voie maritime stratégique par laquelle circule environ vingt pour cent du pétrole commercialisé dans le monde.
En conséquence, la réactivation des contacts bilatéraux entre Washington et Jérusalem vise à harmoniser les positions face à un ennemi commun et à coordonner les prochaines étapes stratégiques dans un scénario marqué par la volatilité permanente du golfe Persique.
## La course contre la montre électorale
En plus du composant militaire et diplomatique, le facteur temps apparaît comme l'un des éléments les plus sensibles de la conjoncture actuelle.
La proximité des prochaines élections introduit une variable politique qui peut modifier significativement le paysage géopolitique.
Sur le plan interne des États-Unis, le président Donald Trump a minimisé les spéculations sur l'éventuel impact électoral de l'augmentation des prix des combustibles.
Devant la presse, il a affirmé :
«Malgré ce que tout le monde dit sur les élections, je ne suis pas pressé… Les élections se résolvent d'elles-mêmes.
Cependant, le **Dr. José Benjamín Pérez Matos** a attiré l'attention sur un autre processus électoral qui, de son point de vue, revêt une importance encore plus grande pour l'avenir immédiat du Moyen-Orient : les élections prévues en Israël.
À ce sujet, il a exprimé :
**«Mais regardez ce qui se passe en Israël : il va y avoir des élections, en octobre. Ils doivent se dépêcher. Parce que si un premier ministre qui ne soit pas favorable (et dans cette ligne de pensée) au monde religieux, des orthodoxes, arrive là-bas, et qu'un autre arrive au pouvoir et gagne les élections en octobre, les choses vont devenir difficiles pour Israël. C'est-à-dire, il doit plutôt profiter du temps restant, au cas où une surprise se produirait en octobre».**
Ces mots reflètent l'importance que le **Dr. José Benjamín Pérez Matos** attribue au calendrier politique israélien dans le contexte régional. Dans cette perspective, le temps disponible pour consolider certaines décisions stratégiques est limité et pourrait être conditionné par le résultat des prochaines élections.
Comme l'a souligné le **Dr. José Benjamín Pérez Matos** lui-même : « la marge de manœuvre pour consolider les positions de force est extrêmement étroite. » Les acteurs politiques doivent agir avec promptitude face à la possibilité d'un retournement électoral ou d'une surprise dans les urnes.
Cet avertissement constitue la conclusion naturelle de l'analyse géopolitique développée tout au long de cette réflexion. Cependant, pour le **Dr. José Benjamín Pérez Matos**, les événements internationaux ne peuvent pas être compris uniquement sous langle des décisions humaines, de la diplomatie ou de la stratégie militaire.
Au-delà de tous ces facteurs, il existe un dessein supérieur dont l'accomplissement, affirme-t-il, continue de saccomplir conformément au programme prophétique révélé dans les Écritures.
C'est pourquoi il a conclu son exposé en rappelant :
**«Mais rappelez-vous que tout est, et tout s'accomplira, conformément aux prophéties. Rien de tout cela ne reste entre les mains des hommes».**
Selon la perspective présentée par le **Dr. José Benjamín Pérez Matos**, les événements qui se déroulent aujourd'hui au Moyen-Orient transcendent la conjoncture internationale et constituent une part d'un processus historique de plus grande envergure. Au-delà des négociations diplomatiques, des décisions militaires ou des changements politiques qui peuvent se produire dans les différents pays impliqués, le dénouement définitif sera déterminé par l'accomplissement du dessein prophétique qui, selon les Écritures, conduira à l'établissement de l'ordre final annoncé dans le livre de Daniel.

Some files were not shown because too many files have changed in this diff Show More