diff --git a/package.json b/package.json index 20b0529..71f2028 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,14 @@ "@tiptap/extension-youtube": "^3.21.0", "@types/dompurify": "^3.0.5", "@types/video.js": "^7.3.58", - "appwrite": "^23.0.0", + "appwrite": "^1.7.0", "axios": "^1.13.6", "clsx": "^2.1.1", "dompurify": "^3.3.3", "isomorphic-dompurify": "^2.20.0", "lucide-react": "^0.577.0", "next": "16.1.6", - "node-appwrite": "^22.1.3", + "node-appwrite": "^1.7.0", "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.5.0", diff --git a/src/app/api/webhooks/publications/route.ts b/src/app/api/webhooks/publications/route.ts new file mode 100644 index 0000000..5eb042c --- /dev/null +++ b/src/app/api/webhooks/publications/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ID } from 'node-appwrite'; +import { createAdminClient } from '@/lib/appwrite.server'; +import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; +import { PostSchema, generateSlug } from '@/lib/validators/publications'; +import type { ApiResponse } from '@/types/publications'; + +const DB = APPWRITE_ALMEIRA_CONFIG.databaseId; +const COLLECTION = 'posts'; + +/** + * @file app/api/webhooks/publications/route.ts + * @description Secure Bridge API for n8n/Automation. + * + * Features: + * 1. X-Webhook-Token Authentication. + * 2. Automatic slug generation from title. + * 3. Default values for automated posts. + * 4. Full Zod validation. + */ + +export async function POST(request: NextRequest) { + // 1. Authentication (Shared Secret) + const authHeader = request.headers.get('X-Webhook-Token'); + const WEBHOOK_TOKEN = process.env.WEBHOOK_TOKEN || 'almeira_n8n_secret_2024'; // FALLBACK FOR DEV + + if (!authHeader || authHeader !== WEBHOOK_TOKEN) { + return NextResponse.json( + { success: false, error: 'Unauthorized. Invalid Webhook Token.' }, + { status: 401 } + ); + } + + try { + // 2. Parse Body + const body = await request.json(); + + // 3. Pre-process Data (Slug & Defaults) + const processedData = { + ...body, + // Default to "Borrador" if not specified + estado: body.estado || 'Borrador', + // Auto-generate slug if missing + slug: body.slug || (body.titulo ? generateSlug(body.titulo) : undefined), + // Current ISO date for publication + fecha_publicacion: body.fecha_publicacion || new Date().toISOString(), + }; + + // 4. Validate with Zod + const parseResult = PostSchema.safeParse(processedData); + if (!parseResult.success) { + const validationErrors: Record = {}; + parseResult.error.issues.forEach((err) => { + const key = String(err.path[0]); + if (!validationErrors[key]) validationErrors[key] = []; + validationErrors[key].push(err.message); + }); + return NextResponse.json( + { success: false, error: 'Invalid publication data.', validationErrors }, + { status: 422 } + ); + } + + // 5. Create Document in Appwrite + const admin = await createAdminClient(); + const document = await admin.databases.createDocument( + DB, + COLLECTION, + ID.unique(), + parseResult.data + ); + + return NextResponse.json( + { success: true, data: document }, + { status: 201 } + ); + + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Error processing webhook.'; + console.error('[WEBHOOK_ERROR]', error); + return NextResponse.json( + { success: false, error: message }, + { status: 500 } + ); + } +} diff --git a/src/components/media/LivePlayer.tsx b/src/components/media/LivePlayer.tsx index d4317b2..38016f7 100644 --- a/src/components/media/LivePlayer.tsx +++ b/src/components/media/LivePlayer.tsx @@ -1,8 +1,10 @@ 'use client'; import { Tv, PlayCircle, RadioTower, WifiOff, PauseCircle, Volume2 } from 'lucide-react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { usePlayerStore } from '@/store/usePlayerStore'; +import videojs from 'video.js'; +import 'video.js/dist/video-js.css'; interface LivePlayerProps { url?: string; @@ -52,6 +54,40 @@ export function LivePlayer({ url, type, title }: LivePlayerProps) { } }, [url, type, setAudioUrl, setVideoUrl, setVideoType]); + // Handle inner Video Player Initialization + const innerVideoRef = useRef(null); + const innerPlayerRef = useRef(null); + + useEffect(() => { + if (type === 'tv' && !isYouTube && processedUrl && innerVideoRef.current) { + if (innerPlayerRef.current) { + innerPlayerRef.current.dispose(); + innerPlayerRef.current = null; + } + const videoElement = document.createElement('video-js'); + videoElement.classList.add('vjs-fill', 'vjs-almeira'); + innerVideoRef.current.innerHTML = ''; + innerVideoRef.current.appendChild(videoElement); + + innerPlayerRef.current = videojs(videoElement, { + autoplay: 'muted', + controls: false, + fluid: true, + responsive: true, + sources: [{ + src: processedUrl, + type: 'application/x-mpegURL' + }] + }); + } + return () => { + if (innerPlayerRef.current) { + innerPlayerRef.current.dispose(); + innerPlayerRef.current = null; + } + }; + }, [type, isYouTube, processedUrl]); + if (!processedUrl) { return (
@@ -78,18 +114,26 @@ export function LivePlayer({ url, type, title }: LivePlayerProps) {
{type === 'tv' ? (
-