modified: src/app/[slug]/page.tsx
modified: src/app/clasificados/page.tsx modified: src/app/dashboard/[entity]/[id]/page.tsx modified: src/app/dashboard/[entity]/new/page.tsx modified: src/app/dashboard/[entity]/page.tsx modified: src/app/dashboard/entity/page.tsx modified: src/app/dashboard/profile/page.tsx modified: src/app/dashboard/settings/page.tsx modified: src/app/eventos/page.tsx modified: src/app/globals.css modified: src/app/page.tsx modified: src/components/NewsTicker.tsx modified: src/components/ProgrammingSchedule.tsx
This commit is contained in:
parent
80a6f77b74
commit
e3024af88b
@ -35,8 +35,9 @@ async function fetchPostData(slugOrId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export default async function NoticiaDetail({ params }: { params: { slug: string } }) {
|
||||
const post = await fetchPostData(params.slug);
|
||||
export default async function NoticiaDetail({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const post = await fetchPostData(slug);
|
||||
const hasSidebar = await hasSidebarContent();
|
||||
|
||||
if (!post) {
|
||||
|
||||
@ -13,10 +13,10 @@ async function fetchClasificadosData() {
|
||||
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
|
||||
const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {};
|
||||
|
||||
const postsCount = settings.classifieds_count || 9;
|
||||
const postsCount = settings.feed_news_count || 12;
|
||||
const cardSettings = {
|
||||
showCategory: settings.news_card_show_category ?? false,
|
||||
showFooterDate: settings.news_card_show_footer_date ?? true,
|
||||
showFooterDate: settings.show_news_date ?? false,
|
||||
showFooterButton: settings.news_card_show_footer_button ?? true,
|
||||
footerText: settings.news_card_footer_text || ""
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import Link from 'next/link';
|
||||
import { updateDocumentServer } from '@/app/actions/appwrite';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function EditEntityPage({ params }: { params: { entity: string, id: string } }) {
|
||||
export default async function EditEntityPage({ params }: { params: Promise<{ entity: string, id: string }> }) {
|
||||
const { entity, id } = await params;
|
||||
|
||||
const collectionId = collectionMap[entity];
|
||||
|
||||
@ -5,7 +5,7 @@ import Link from 'next/link';
|
||||
import { createDocumentServer } from '@/app/actions/appwrite';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function NewEntityPage({ params }: { params: { entity: string } }) {
|
||||
export default async function NewEntityPage({ params }: { params: Promise<{ entity: string }> }) {
|
||||
const { entity } = await params;
|
||||
|
||||
const collectionId = collectionMap[entity];
|
||||
|
||||
@ -11,8 +11,8 @@ export default async function EntityPage({
|
||||
params,
|
||||
searchParams
|
||||
}: {
|
||||
params: { entity: string },
|
||||
searchParams: { q?: string, page?: string }
|
||||
params: Promise<{ entity: string }>,
|
||||
searchParams: Promise<{ q?: string, page?: string }>
|
||||
}) {
|
||||
const { entity } = await params;
|
||||
const sParams = await searchParams;
|
||||
|
||||
@ -1,13 +1,26 @@
|
||||
import { getEntity, updateEntity } from '@/app/actions/entity';
|
||||
import { Building, Image as ImageIcon, Link as LinkIcon, Share2, Tv, RadioTower } from 'lucide-react';
|
||||
import { Building, Image as ImageIcon, Link as LinkIcon, Share2, Tv, RadioTower, Facebook, Instagram, Twitter, Youtube, Send, Mail, Globe, MapPin, Phone, MessageSquare } from 'lucide-react';
|
||||
import { createSessionClient } from '@/lib/appwrite.server';
|
||||
import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
|
||||
|
||||
export default async function EntityPage() {
|
||||
let entity: any = null;
|
||||
let cities: any[] = [];
|
||||
|
||||
try {
|
||||
entity = await getEntity();
|
||||
|
||||
const sessionClient = await createSessionClient();
|
||||
if (sessionClient) {
|
||||
const citiesRes = await sessionClient.databases.listDocuments(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
'cities',
|
||||
[]
|
||||
);
|
||||
cities = citiesRes.documents;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("No se pudo cargar config de entidad:", e);
|
||||
console.error("No se pudo cargar la información de la entidad o ciudades:", e);
|
||||
}
|
||||
|
||||
async function updateEntityClient(formData: FormData) {
|
||||
@ -17,7 +30,7 @@ export default async function EntityPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6 animate-fade-in pb-12">
|
||||
<div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl">
|
||||
<div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl shadow-cyan-500/5">
|
||||
<div className="p-3 bg-blue-500/10 rounded-xl text-blue-400">
|
||||
<Building size={32} />
|
||||
</div>
|
||||
@ -25,7 +38,7 @@ export default async function EntityPage() {
|
||||
<h1 className="text-3xl font-black font-outfit uppercase tracking-tighter text-white">
|
||||
Perfil de Entidad
|
||||
</h1>
|
||||
<p className="text-sm font-medium text-slate-400">Identidad Corporativa, Contacto y Canales de Streaming</p>
|
||||
<p className="text-sm font-medium text-slate-400 font-bold uppercase tracking-wider">Identidad Corporativa, Contacto y Canales</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -36,7 +49,7 @@ export default async function EntityPage() {
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><ImageIcon size={18} className="text-cyan-400" /> Identidad Visual</h3>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Razón Social / Nombre Entidad *</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Razón Social / Nombre Entidad *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="razon_social"
|
||||
@ -47,7 +60,7 @@ export default async function EntityPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Sube el Logotipo (Dejar vacío para conservar el actual)</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Logotipo Corporativo</label>
|
||||
{entity?.logotipo && (
|
||||
<div className="mb-3 p-3 bg-slate-950 rounded-xl inline-block border border-slate-800">
|
||||
<img src={entity.logotipo} alt="Logo actual" className="h-16 object-contain" />
|
||||
@ -57,7 +70,7 @@ export default async function EntityPage() {
|
||||
type="file"
|
||||
name="logotipo"
|
||||
accept="image/*"
|
||||
className="w-full bg-slate-950/50 border border-slate-800 rounded-xl file:mr-4 file:py-3 file:px-4 file:rounded-l-xl file:border-0 file:text-sm file:font-semibold file:bg-cyan-500/10 file:text-cyan-400 hover:file:bg-cyan-500/20 text-slate-300 text-sm transition-all"
|
||||
className="w-full bg-slate-950/50 border border-slate-800 rounded-xl file:mr-4 file:py-3 file:px-4 file:rounded-l-xl file:border-0 file:text-[10px] file:font-black file:uppercase file:bg-cyan-500/10 file:text-cyan-400 hover:file:bg-cyan-500/20 text-slate-300 text-sm transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -71,36 +84,55 @@ export default async function EntityPage() {
|
||||
|
||||
{/* Contacto Físico */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><LinkIcon size={18} className="text-green-400" /> Contacto Oficial</h3>
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><MapPin size={18} className="text-green-400" /> Contacto Oficial</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Ciudad</label>
|
||||
<input type="text" name="ciudad" defaultValue={entity?.ciudad || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Ciudad</label>
|
||||
<select
|
||||
name="ciudad"
|
||||
defaultValue={entity?.ciudad || ''}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all"
|
||||
>
|
||||
<option value="">Seleccionar Ciudad...</option>
|
||||
{cities.map(city => (
|
||||
<option key={city.$id} value={city.descripcion}>{city.descripcion}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Dirección Físico</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Dirección Física</label>
|
||||
<input type="text" name="direccion" defaultValue={entity?.direccion || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Teléfono</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Teléfono</label>
|
||||
<input type="text" name="numero_contacto" defaultValue={entity?.numero_contacto || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">WhatsApp</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">WhatsApp</label>
|
||||
<input type="text" name="numero_whatsapp" defaultValue={entity?.numero_whatsapp || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Email</label>
|
||||
<input type="email" name="email" defaultValue={entity?.email || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
|
||||
|
||||
{/* Email y Sitio Web en líneas separadas como solicitó el usuario */}
|
||||
<div>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Email Corporativo</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-600 group-focus-within:text-cyan-500 transition-colors">
|
||||
<Mail size={18} />
|
||||
</div>
|
||||
<input type="email" name="email" defaultValue={entity?.email || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-3 text-white focus:border-cyan-500 transition-all font-medium" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Sitio Web URL</label>
|
||||
<input type="url" name="sitio_web" defaultValue={entity?.sitio_web || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Sitio Web URL</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-600 group-focus-within:text-cyan-500 transition-colors">
|
||||
<Globe size={18} />
|
||||
</div>
|
||||
<input type="url" name="sitio_web" defaultValue={entity?.sitio_web || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-3 text-white focus:border-cyan-500 transition-all font-medium" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -111,7 +143,7 @@ export default async function EntityPage() {
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><Tv size={18} className="text-rose-400" /> Canales en Vivo (Streaming)</h3>
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-cyan-400">URL Streaming TV (YouTube/HLS/iFrame Source)</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2 text-cyan-400">URL Streaming TV</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tv_url"
|
||||
@ -119,10 +151,9 @@ export default async function EntityPage() {
|
||||
placeholder="https://www.youtube.com/embed/..."
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 transition-all font-mono text-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 mt-2">Esta URL se usará para el reproductor principal de la sección TV.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-rose-400">URL Streaming Radio (Audio Stream / Icecast)</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2 text-rose-400">URL Streaming Radio (Audio Stream)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="radio_url"
|
||||
@ -130,43 +161,52 @@ export default async function EntityPage() {
|
||||
placeholder="https://stream.server.com:8000/live"
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-rose-500 transition-all font-mono text-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 mt-2">Enlace directo al flujo de audio para la sección Radio.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Redes Sociales */}
|
||||
{/* Redes Sociales con Iconos */}
|
||||
<div className="space-y-6 pt-4 border-t border-slate-800">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><Share2 size={18} className="text-purple-400" /> Vínculos Sociales</h3>
|
||||
<div className="grid md:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-blue-500">Facebook</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-blue-500">
|
||||
<Facebook size={12} /> Facebook
|
||||
</label>
|
||||
<input type="url" name="facebook_url" defaultValue={entity?.facebook_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-blue-500 text-sm transition-all" placeholder="https://..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-pink-500">Instagram</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-pink-500">
|
||||
<Instagram size={12} /> Instagram
|
||||
</label>
|
||||
<input type="url" name="instagram_url" defaultValue={entity?.instagram_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-pink-500 text-sm transition-all" placeholder="https://..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-slate-400">X (Twitter)</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-slate-200">
|
||||
<Twitter size={12} /> X (Twitter)
|
||||
</label>
|
||||
<input type="url" name="x_url" defaultValue={entity?.x_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-slate-500 text-sm transition-all" placeholder="https://..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-red-500">YouTube</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-red-500">
|
||||
<Youtube size={12} /> YouTube
|
||||
</label>
|
||||
<input type="url" name="youtube_url" defaultValue={entity?.youtube_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-red-500 text-sm transition-all" placeholder="https://..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-cyan-500">Telegram</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-cyan-500">
|
||||
<Send size={12} /> Telegram
|
||||
</label>
|
||||
<input type="url" name="telegram_url" defaultValue={entity?.telegram_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-cyan-500 text-sm transition-all" placeholder="https://..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-slate-800">
|
||||
<button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-8 rounded-xl transition-all border border-blue-500/50 hover:border-blue-400 ml-auto shadow-[0_0_20px_rgba(59,130,246,0.3)]">
|
||||
<button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-12 rounded-xl transition-all border border-blue-500/50 hover:border-blue-400 ml-auto shadow-[0_0_20px_rgba(59,130,246,0.3)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-indigo-500 opacity-90 group-hover:opacity-100 transition-opacity"></div>
|
||||
<Building size={18} className="relative z-10" />
|
||||
<span className="relative z-10 text-sm">Guardar Perfil de Entidad</span>
|
||||
<span className="relative z-10 text-sm">Guardar</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -140,7 +140,7 @@ export default function ProfilePage() {
|
||||
<button disabled={loading} type="submit" className="w-full relative group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-4 px-6 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 shadow-[0_0_20px_rgba(6,182,212,0.1)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-600 to-blue-600 opacity-80 group-hover:opacity-100 transition-opacity"></div>
|
||||
<span className="relative z-10 flex items-center justify-center gap-2">
|
||||
{loading ? <Loader className="animate-spin" size={18} /> : <>Guardar Cambios <ArrowRight size={18} /></>}
|
||||
{loading ? <Loader className="animate-spin" size={18} /> : <>Guardar <ArrowRight size={18} /></>}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@ -35,15 +35,26 @@ export default async function SettingsPage() {
|
||||
|
||||
const updateData = {
|
||||
ticker_count: parseInt(formData.get('ticker_count') as string),
|
||||
rotation_seconds: parseInt(formData.get('rotation_seconds') as string)
|
||||
feed_news_count: parseInt(formData.get('feed_news_count') as string),
|
||||
rotation_seconds: parseInt(formData.get('rotation_seconds') as string),
|
||||
show_news_date: formData.get('show_news_date') === 'true'
|
||||
};
|
||||
|
||||
await sessionClient.databases.updateDocument(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
'settings',
|
||||
formData.get('docId') as string,
|
||||
updateData
|
||||
);
|
||||
if (formData.get('docId')) {
|
||||
await sessionClient.databases.updateDocument(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
'settings',
|
||||
formData.get('docId') as string,
|
||||
updateData
|
||||
);
|
||||
} else {
|
||||
await sessionClient.databases.createDocument(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
'settings',
|
||||
'unique()',
|
||||
updateData
|
||||
);
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard/settings');
|
||||
revalidatePath('/'); // Revalidate frontpage
|
||||
@ -56,7 +67,7 @@ export default async function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6 animate-fade-in pb-12">
|
||||
<div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl">
|
||||
<div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl shadow-cyan-500/5">
|
||||
<div className="p-3 bg-cyan-500/10 rounded-xl text-cyan-400">
|
||||
<Settings size={32} />
|
||||
</div>
|
||||
@ -75,58 +86,83 @@ export default async function SettingsPage() {
|
||||
<input type="hidden" name="docId" value={docId} />
|
||||
|
||||
{/* Configurador del Ticker */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Menu className="text-pink-500" />
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-white">Ticker de Noticias</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 border-b border-slate-800 pb-2">
|
||||
<Menu className="text-cyan-500" size={20} />
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Noticias y Feed</h2>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm h-12">
|
||||
Gobierna el cintillo de 'Últimas Noticias' que rota bajo el encabezado superior estableciendo su límite histórico.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Cantidad a Mostrar</label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Ticker (Cintillo)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="ticker_count"
|
||||
defaultValue={settings.ticker_count || 10}
|
||||
min="1" max="50"
|
||||
required
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 text-xl font-black font-mono transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Feed (Inicio)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="feed_news_count"
|
||||
defaultValue={(settings as any).feed_news_count || 12}
|
||||
min="4" max="100"
|
||||
required
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-400 text-xl font-black font-mono transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 bg-slate-950 p-4 border border-slate-800 rounded-xl hover:border-cyan-500/30 transition-colors">
|
||||
<input
|
||||
type="number"
|
||||
name="ticker_count"
|
||||
defaultValue={settings.ticker_count}
|
||||
min="1" max="50"
|
||||
required
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 text-xl font-black font-mono transition-all"
|
||||
type="checkbox"
|
||||
name="show_news_date"
|
||||
value="true"
|
||||
defaultChecked={(settings as any).show_news_date === true}
|
||||
id="show_news_date"
|
||||
className="w-5 h-5 accent-cyan-500 cursor-pointer"
|
||||
/>
|
||||
<label htmlFor="show_news_date" className="text-sm font-bold text-slate-300 cursor-pointer select-none">
|
||||
Mostrar fecha de publicación en las tarjetas del feed
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configurador de Rotación (Banners) */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Clock className="text-purple-500" />
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-white">Velocidad Carrusel</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 border-b border-slate-800 pb-2">
|
||||
<Clock className="text-purple-500" size={20} />
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Visualización</h2>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm h-12">
|
||||
Regula el tiempo en pantalla antes de que cambie la lámina (Afecta Banner Principal y Sliders Patrocinados).
|
||||
</p>
|
||||
|
||||
<div className="relative">
|
||||
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Segundos en Pantalla</label>
|
||||
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Segundos en Pantalla (Banners)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="rotation_seconds"
|
||||
defaultValue={settings.rotation_seconds}
|
||||
defaultValue={settings.rotation_seconds || 5}
|
||||
min="1" max="60"
|
||||
required
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-purple-500 text-xl font-black font-mono transition-all"
|
||||
/>
|
||||
<span className="absolute right-4 bottom-3 text-slate-500 font-bold tracking-widest uppercase text-sm">Segundos</span>
|
||||
<span className="absolute right-4 bottom-3 text-slate-600 font-bold tracking-widest uppercase text-[10px]">Segundos</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-slate-500 italic leading-relaxed">
|
||||
Estas configuraciones afectan el comportamiento dinámico del sitio web principal y los tiempos de carga de recursos del lado del servidor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-slate-800">
|
||||
<button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-8 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 ml-auto">
|
||||
<button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-10 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 ml-auto shadow-[0_0_20px_rgba(6,182,212,0.2)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-600 to-blue-500 opacity-90 group-hover:opacity-100 transition-opacity"></div>
|
||||
<Save size={18} className="relative z-10" />
|
||||
<span className="relative z-10 text-sm">Guardar Motor Web</span>
|
||||
<span className="relative z-10 text-sm">Guardar</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -13,10 +13,10 @@ async function fetchEventosData() {
|
||||
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
|
||||
const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {};
|
||||
|
||||
const postsCount = settings.events_count || 9;
|
||||
const postsCount = settings.feed_news_count || 12;
|
||||
const cardSettings = {
|
||||
showCategory: settings.news_card_show_category ?? false,
|
||||
showFooterDate: settings.news_card_show_footer_date ?? true,
|
||||
showFooterDate: settings.show_news_date ?? false,
|
||||
showFooterButton: settings.news_card_show_footer_button ?? true,
|
||||
footerText: settings.news_card_footer_text || ""
|
||||
};
|
||||
|
||||
@ -86,14 +86,17 @@
|
||||
/* Scrollbar futurista */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #020617;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #1e293b;
|
||||
border-radius: 4px;
|
||||
background: #06b6d4; /* Cyan */
|
||||
background: linear-gradient(180deg, #06b6d4 0%, #3b82f6 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #020617;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #334155;
|
||||
background: #22d3ee;
|
||||
}
|
||||
|
||||
@ -13,10 +13,10 @@ async function fetchHomeData() {
|
||||
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
|
||||
const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {};
|
||||
|
||||
const postsCount = settings.feed_count || settings.post_count || 9;
|
||||
const postsCount = settings.feed_news_count || 12;
|
||||
const cardSettings = {
|
||||
showCategory: settings.news_card_show_category ?? false,
|
||||
showFooterDate: settings.news_card_show_footer_date ?? true,
|
||||
showFooterDate: settings.show_news_date ?? false,
|
||||
showFooterButton: settings.news_card_show_footer_button ?? true,
|
||||
footerText: settings.news_card_footer_text || ""
|
||||
};
|
||||
|
||||
@ -42,16 +42,16 @@ export default async function NewsTicker() {
|
||||
]);
|
||||
|
||||
// Resolve Media
|
||||
const mediaIds = postsRes.documents.map(p => p.imagen_destacada_id).filter(id => id && typeof id === 'string');
|
||||
const mediaIds = postsRes.documents.map((p: any) => p.imagen_destacada_id).filter((id: any) => id && typeof id === 'string');
|
||||
const mediaMap = new Map();
|
||||
if (mediaIds.length > 0) {
|
||||
const mediaRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [
|
||||
Query.equal('$id', mediaIds)
|
||||
]);
|
||||
mediaRes.documents.forEach(m => mediaMap.set(m.$id, m));
|
||||
mediaRes.documents.forEach((m: any) => mediaMap.set(m.$id, m));
|
||||
}
|
||||
|
||||
newsItems = postsRes.documents.map(post => {
|
||||
newsItems = postsRes.documents.map((post: any) => {
|
||||
const media = mediaMap.get(post.imagen_destacada_id);
|
||||
let imagenUrl = post.og_image_url || null;
|
||||
if (media) {
|
||||
@ -92,7 +92,7 @@ export default async function NewsTicker() {
|
||||
>
|
||||
{noticia.imagenUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={noticia.imagenUrl} alt={noticia.titulo} className="w-10 h-10 object-cover rounded-lg shadow-[0_0_10px_rgba(6,182,212,0.3)] group-hover:scale-110 group-hover:shadow-[0_0_15px_rgba(6,182,212,0.5)] transition-all duration-300 border border-white/10" />
|
||||
<img src={noticia.imagenUrl} alt={noticia.titulo} className="w-16 h-9 aspect-video object-cover rounded shadow-[0_0_10px_rgba(6,182,212,0.3)] group-hover:scale-105 group-hover:shadow-[0_0_15px_rgba(6,182,212,0.5)] transition-all duration-300 border border-white/10" />
|
||||
)}
|
||||
<span className="text-sm font-bold text-slate-300 group-hover:text-cyan-400 transition-colors uppercase tracking-widest flex items-center gap-2">
|
||||
{!noticia.imagenUrl && <span className="w-2 h-2 rounded-full bg-rose-500 animate-pulse"></span>}
|
||||
|
||||
@ -4,15 +4,16 @@ import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
export default function ProgrammingSchedule({ matrixData }: { matrixData: Record<string, string> }) {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [viewMode, setViewMode] = useState<'day' | 'week'>('day');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Actualizar cada minuto
|
||||
// Actualizar cada minuto cada que cambie la hora
|
||||
const interval = setInterval(() => setCurrentTime(new Date()), 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const days = [
|
||||
const allDays = [
|
||||
{ key: 'mon', label: 'Lunes', index: 1 },
|
||||
{ key: 'tue', label: 'Martes', index: 2 },
|
||||
{ key: 'wed', label: 'Miércoles', index: 3 },
|
||||
@ -22,20 +23,26 @@ export default function ProgrammingSchedule({ matrixData }: { matrixData: Record
|
||||
{ key: 'sun', label: 'Domingo', index: 0 }
|
||||
];
|
||||
|
||||
const currentDayIndex = currentTime.getDay();
|
||||
|
||||
// Si estamos en modo día, solo filtramos el día actual
|
||||
const days = viewMode === 'day'
|
||||
? allDays.filter(d => d.index === currentDayIndex)
|
||||
: allDays;
|
||||
|
||||
const slots = Array.from({length: 48}).map((_, i) => {
|
||||
const h = Math.floor(i/2).toString().padStart(2, '0');
|
||||
const m = i % 2 === 0 ? '00' : '30';
|
||||
return `${h}:${m}`;
|
||||
});
|
||||
|
||||
const currentDayIndex = currentTime.getDay();
|
||||
const currentHour = currentTime.getHours();
|
||||
const currentMinute = currentTime.getMinutes();
|
||||
const currentSlotIndex = currentHour * 2 + (currentMinute >= 30 ? 1 : 0);
|
||||
|
||||
// Calc rowSpans to unify cells visually
|
||||
const spans: Record<string, number[]> = {};
|
||||
days.forEach(d => {
|
||||
allDays.forEach(d => {
|
||||
const list = slots.map(time => matrixData[`${time}_${d.key}`] || '');
|
||||
const spanList = new Array(48).fill(1);
|
||||
for(let i=0; i<48; i++){
|
||||
@ -58,30 +65,46 @@ export default function ProgrammingSchedule({ matrixData }: { matrixData: Record
|
||||
const scrollAmnt = Math.max(0, (currentSlotIndex - 3) * rowHeight);
|
||||
containerRef.current.scrollTop = scrollAmnt;
|
||||
}
|
||||
}, [currentSlotIndex]);
|
||||
}, [currentSlotIndex, viewMode]);
|
||||
|
||||
return (
|
||||
<div className="w-full bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden shadow-2xl relative">
|
||||
<div className="p-4 bg-slate-950 border-b border-slate-800 flex justify-between items-center">
|
||||
<div className="w-full bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden shadow-2xl relative group/matrix">
|
||||
<div className="p-4 bg-slate-950 border-b border-slate-800 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<h3 className="text-xl font-black font-outfit uppercase tracking-tighter text-white flex items-center gap-3">
|
||||
<span className="w-3 h-3 rounded-full bg-rose-500 animate-pulse shadow-[0_0_10px_#f43f5e]"></span>
|
||||
Parrilla de Programación
|
||||
</h3>
|
||||
|
||||
{/* Switch Futurista Día / Semana */}
|
||||
<div className="flex items-center gap-2 bg-slate-900 p-1 rounded-xl border border-slate-800">
|
||||
<button
|
||||
onClick={() => setViewMode('day')}
|
||||
className={`px-4 py-1.5 rounded-lg text-xs font-black uppercase tracking-widest transition-all ${viewMode === 'day' ? 'bg-cyan-500 text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.4)]' : 'text-slate-500 hover:text-slate-300'}`}
|
||||
>
|
||||
Hoy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('week')}
|
||||
className={`px-4 py-1.5 rounded-lg text-xs font-black uppercase tracking-widest transition-all ${viewMode === 'week' ? 'bg-cyan-500 text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.4)]' : 'text-slate-500 hover:text-slate-300'}`}
|
||||
>
|
||||
Semana
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={containerRef} className="overflow-x-auto overflow-y-auto max-h-[600px] relative custom-scrollbar scroll-smooth">
|
||||
<table className="w-full text-left border-collapse min-w-[900px] relative">
|
||||
<table className="w-full text-left border-collapse min-w-[300px] md:min-w-[900px] relative">
|
||||
<thead className="sticky top-0 z-30 ring-1 ring-slate-800">
|
||||
<tr>
|
||||
<th className="p-3 w-20 text-center bg-slate-950/90 backdrop-blur-md">
|
||||
<span className="text-xs font-black tracking-widest text-slate-500 uppercase">Hora</span>
|
||||
<th className="p-3 w-16 md:w-20 text-center bg-slate-950/95 backdrop-blur-md">
|
||||
<span className="text-[10px] font-black tracking-widest text-slate-500 uppercase">Hora</span>
|
||||
</th>
|
||||
{days.map(d => {
|
||||
const isToday = d.index === currentDayIndex;
|
||||
return (
|
||||
<th key={d.key} className={`p-4 text-center border-b border-slate-800 bg-slate-900/90 backdrop-blur-md ${isToday ? 'text-cyan-400' : 'text-slate-300'}`}>
|
||||
<th key={d.key} className={`p-4 text-center border-b border-slate-800 bg-slate-900/95 backdrop-blur-md ${isToday ? 'text-cyan-400' : 'text-slate-300'}`}>
|
||||
<span className={`text-sm tracking-widest uppercase font-black`}>{d.label}</span>
|
||||
{isToday && <div className="text-[10px] bg-cyan-500/20 text-cyan-300 rounded-full px-2 py-0.5 mt-1 inline-block animate-pulse">HOY</div>}
|
||||
{isToday && viewMode === 'week' && <div className="text-[9px] bg-cyan-500/20 text-cyan-300 rounded-full px-2 py-0.5 mt-1 inline-block animate-pulse">HOY</div>}
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
@ -95,7 +118,7 @@ export default function ProgrammingSchedule({ matrixData }: { matrixData: Record
|
||||
return (
|
||||
<tr key={idx} className={`group transition-colors relative ${isCurrentSlot ? 'bg-rose-950/10' : ''}`}>
|
||||
|
||||
{/* Indicador de Línea de Tiempo Rojo (Midiéndose internamente en sus 30 mins) */}
|
||||
{/* Indicador de Línea de Tiempo Roja */}
|
||||
{isCurrentSlot && (
|
||||
<td className="absolute left-0 right-0 h-0.5 bg-rose-500 z-20 pointer-events-none shadow-[0_0_10px_rgba(244,63,94,0.8)] transition-all ease-linear"
|
||||
style={{ top: `${((currentMinute % 30) / 30) * 100}%` }}></td>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user