feat(ui): optimización de UI/UX en CMS y GlobalPlayer, correcciones backend Appwrite
This commit is contained in:
parent
d48d7bc094
commit
58b28378fb
@ -89,3 +89,43 @@ export async function uploadMediaServer(formData: FormData) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadMediaDocumentServer(formData: FormData) {
|
||||
try {
|
||||
const client = getAdminClient();
|
||||
const storage = new Storage(client);
|
||||
const databases = new Databases(client);
|
||||
const files = formData.getAll('files') as File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('No se encontraron archivos para subir');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const file of files) {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const uploadedFile = await storage.createFile('media', ID.unique(), {
|
||||
buffer: buffer,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type
|
||||
} as any);
|
||||
|
||||
const doc = await databases.createDocument(DATABASE_ID, 'media', ID.unique(), {
|
||||
file_id: uploadedFile.$id,
|
||||
bucket_id: 'media',
|
||||
titulo: file.name,
|
||||
alt_text: file.name.split('.')[0],
|
||||
tipo: file.type.startsWith('image/') ? 'imagen' : 'video'
|
||||
});
|
||||
results.push(doc);
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard/media');
|
||||
revalidatePath('/');
|
||||
return { success: true, count: results.length, data: results };
|
||||
} catch (error: any) {
|
||||
console.error('Error uploading media and creating document:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,14 +44,32 @@ export async function updateEntityConfig(data: any) {
|
||||
const databases = await getAdminClient();
|
||||
const existing = await getEntityConfig();
|
||||
|
||||
let parsedData = data;
|
||||
let parsedData: any = data;
|
||||
if (typeof FormData !== 'undefined' && data instanceof FormData) {
|
||||
parsedData = {};
|
||||
data.forEach((value, key) => {
|
||||
for (const [key, value] of Array.from(data.entries())) {
|
||||
if (key !== '$id' && key !== '$collectionId' && key !== '$databaseId') {
|
||||
parsedData[key] = value;
|
||||
if (typeof value === 'object' && 'size' in value && 'name' in value) {
|
||||
const file = value as File;
|
||||
if (file.size > 0) {
|
||||
try {
|
||||
const { uploadMediaServer } = await import('@/app/actions/appwrite');
|
||||
const tempForm = new FormData();
|
||||
tempForm.append('files', file);
|
||||
const uploadRes = await uploadMediaServer(tempForm);
|
||||
if (uploadRes.success && uploadRes.data && uploadRes.data[0]) {
|
||||
const m = uploadRes.data[0];
|
||||
parsedData[key] = `${ENDPOINT}/storage/buckets/${m.bucketId || 'media'}/files/${m.$id}/view?project=${PROJECT_ID}`;
|
||||
}
|
||||
} catch (uploadErr) {
|
||||
console.error('Error uploading file for entity config:', uploadErr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parsedData[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedData || Object.keys(parsedData).length === 0) {
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ImageIcon, X, Search, CheckCircle2, UploadCloud, Loader2 } from 'lucide-react';
|
||||
import { databases, storage, APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
|
||||
import { Query, ID } from 'appwrite';
|
||||
import { databases, APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
|
||||
import { Query } from 'appwrite';
|
||||
import { uploadMediaDocumentServer } from '@/app/actions/appwrite';
|
||||
|
||||
/** Utility to generate frontend Appwrite image previews */
|
||||
export function getAppwriteImageUrl(fileId: string, width = 400, height = 250) {
|
||||
@ -79,26 +80,22 @@ export const MediaGridSelector = ({
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
// 1. Upload to storage bucket
|
||||
const uploadedFile = await storage.createFile('media', ID.unique(), file);
|
||||
const formData = new FormData();
|
||||
formData.append('files', file);
|
||||
const res = await uploadMediaDocumentServer(formData);
|
||||
|
||||
// 2. Create document in media collection
|
||||
const doc = await databases.createDocument(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', ID.unique(), {
|
||||
file_id: uploadedFile.$id,
|
||||
bucket_id: 'media',
|
||||
titulo: file.name,
|
||||
alt_text: file.name.split('.')[0],
|
||||
tipo: file.type.startsWith('image/') ? 'imagen' : 'video' // mostly images in this bucket
|
||||
});
|
||||
if (!res.success || !res.data || res.data.length === 0) {
|
||||
throw new Error(res.error || "Error from server edge while uploading media.");
|
||||
}
|
||||
|
||||
// 3. Update state
|
||||
const doc = res.data[0];
|
||||
setMediaItems(prev => [doc, ...prev]);
|
||||
const url = getAppwriteImageUrl(uploadedFile.$id, 1200, 800);
|
||||
const url = getAppwriteImageUrl(doc.file_id, 1200, 800);
|
||||
onChange(doc.$id, url);
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error("Error upload:", error);
|
||||
alert("Error al subir el archivo.");
|
||||
alert("Error al subir el archivo. Revisa que el perfil tenga el scope files.write.");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ export const GlobalPlayer = () => {
|
||||
const playerRef = useRef<any>(null);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [lastVolume, setLastVolume] = useState(audioVolume);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Sync Audio State
|
||||
useEffect(() => {
|
||||
@ -110,7 +111,24 @@ export const GlobalPlayer = () => {
|
||||
|
||||
{/* Floating Global Controller */}
|
||||
<div className={`fixed bottom-6 left-6 z-[60] flex items-center gap-3 transition-opacity duration-500 ${isVideoActive ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}>
|
||||
<div className="bg-slate-900/40 backdrop-blur-2xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-4 pr-6 group hover:bg-slate-900/60 transition-all">
|
||||
|
||||
{/* ESTADO COLAPSADO */}
|
||||
{!isExpanded && (audioUrl || videoUrl) && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="group relative h-14 px-5 bg-red-600 hover:bg-red-500 text-white rounded-full flex items-center justify-center gap-3 shadow-[0_0_20px_rgba(220,38,38,0.4)] transition-all animate-in zoom-in slide-in-from-bottom-5"
|
||||
>
|
||||
<div className="absolute inset-0 border-2 border-red-500 rounded-full animate-[ping_2s_ease-out_infinite] opacity-50 pointer-events-none"></div>
|
||||
<Radio size={20} className="relative z-10" />
|
||||
<span className="font-black tracking-widest uppercase text-xs whitespace-nowrap">En Vivo</span>
|
||||
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse relative z-10 shadow-[0_0_8px_rgba(255,255,255,0.8)]"></div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* ESTADO EXPANDIDO */}
|
||||
{isExpanded && (
|
||||
<>
|
||||
<div className="bg-slate-900/40 backdrop-blur-2xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-4 pr-3 group hover:bg-slate-900/60 transition-all animate-in fade-in slide-in-from-left-4">
|
||||
{/* Play/Pause Main Button */}
|
||||
<button
|
||||
onClick={() => toggleAudio()}
|
||||
@ -132,14 +150,14 @@ export const GlobalPlayer = () => {
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col pr-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[10px] font-black uppercase tracking-[0.2em] transition-colors ${isAudioPlaying ? 'text-cyan-400' : 'text-slate-500'}`}>
|
||||
{isAudioPlaying ? 'En Vivo' : 'Radio Offline'}
|
||||
{isAudioPlaying ? 'Señal Activa' : 'Radio Offline'}
|
||||
</span>
|
||||
{isAudioPlaying && <div className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(239,68,68,0.5)]"></div>}
|
||||
</div>
|
||||
<h4 className="text-xs font-bold text-white tracking-wider font-outfit truncate max-w-[120px]">ALMEIRA RADIO</h4>
|
||||
<h4 className="text-xs font-bold text-white tracking-wider font-outfit truncate max-w-[100px]">ALMEIRA RADIO</h4>
|
||||
</div>
|
||||
|
||||
{/* Volume Control (Desktop Only) */}
|
||||
@ -160,6 +178,15 @@ export const GlobalPlayer = () => {
|
||||
}}
|
||||
className="w-20 h-1 bg-slate-800 rounded-full appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400 transition-all"
|
||||
/>
|
||||
|
||||
{/* Botón para colapsar */}
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
title="Ocultar Player"
|
||||
className="ml-2 w-8 h-8 flex items-center justify-center bg-white/5 hover:bg-white/10 border border-white/5 rounded-xl text-slate-400 hover:text-white transition-all"
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -177,6 +204,8 @@ export const GlobalPlayer = () => {
|
||||
<div className="absolute inset-x-0 bottom-0 h-1 bg-white/30 animate-pulse"></div>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Music Bar Animation to globals.css */}
|
||||
|
||||
@ -389,14 +389,14 @@ export const PublicationLayout: React.FC<PublicationLayoutProps> = ({
|
||||
<FieldWrapper label="Estado" required error={validationErrors.estado}>
|
||||
<select
|
||||
name="estado"
|
||||
defaultValue={initialData?.estado || 'Borrador'}
|
||||
defaultValue={initialData?.estado?.toLowerCase() || 'borrador'}
|
||||
required
|
||||
className={inputClass}
|
||||
>
|
||||
<option value="Borrador">Borrador</option>
|
||||
<option value="Pendiente">Pendiente revisión</option>
|
||||
<option value="Publicado">Publicado</option>
|
||||
<option value="Archivado">Archivado</option>
|
||||
<option value="borrador">Borrador</option>
|
||||
<option value="pendiente">Pendiente revisión</option>
|
||||
<option value="publicado">Publicado</option>
|
||||
<option value="archivado">Archivado</option>
|
||||
</select>
|
||||
</FieldWrapper>
|
||||
|
||||
@ -481,36 +481,40 @@ export const PublicationLayout: React.FC<PublicationLayoutProps> = ({
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
{/* Sticky Action Bar */}
|
||||
<PermissionGuard allowedRoles={['editor', 'admin']} userRole={userRole}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || isSaved}
|
||||
className="w-full relative flex items-center justify-center gap-3 py-4 px-6 rounded-2xl
|
||||
font-black uppercase tracking-widest text-sm overflow-hidden
|
||||
bg-gradient-to-r from-cyan-500 to-blue-600
|
||||
hover:from-cyan-400 hover:to-blue-500
|
||||
text-slate-950 shadow-[0_0_30px_rgba(6,182,212,0.3)]
|
||||
transition-all disabled:opacity-60 disabled:cursor-not-allowed
|
||||
active:scale-98"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Guardando…
|
||||
</>
|
||||
) : isSaved ? (
|
||||
<>
|
||||
<CheckCircle size={18} />
|
||||
¡Guardado!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={18} />
|
||||
{initialData?.$id ? 'Guardar Cambios' : 'Crear Publicación'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-slate-950/80 backdrop-blur-xl border-t border-slate-800/50 xl:pl-64">
|
||||
<div className="max-w-7xl mx-auto flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || isSaved}
|
||||
className="w-full md:w-auto relative flex items-center justify-center gap-3 py-3 px-8 rounded-xl
|
||||
font-black uppercase tracking-widest text-sm overflow-hidden
|
||||
bg-gradient-to-r from-cyan-500 to-blue-600
|
||||
hover:from-cyan-400 hover:to-blue-500
|
||||
text-slate-950 shadow-[0_0_20px_rgba(6,182,212,0.3)]
|
||||
transition-all disabled:opacity-60 disabled:cursor-not-allowed
|
||||
active:scale-98"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Guardando…
|
||||
</>
|
||||
) : isSaved ? (
|
||||
<>
|
||||
<CheckCircle size={18} />
|
||||
¡Guardado!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={18} />
|
||||
{initialData?.$id ? 'Actualizar Cambios' : 'Crear Publicación'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PermissionGuard>
|
||||
|
||||
{/* No permission notice */}
|
||||
|
||||
@ -56,28 +56,30 @@ export async function getLoggedInUser() {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get(COOKIE_NAME);
|
||||
|
||||
// Verificación de bypass (Funciona en todos los entornos)
|
||||
// Verificación de bypass (Evitamos imprimir log repetitivo a menos que sea un error)
|
||||
if (sessionCookie?.value === "admin-bypass-session") {
|
||||
console.log("Acceso de emergencia detectado en Dashboard.");
|
||||
return {
|
||||
$id: "admin-emergency",
|
||||
name: "Administrador (Emergencia)",
|
||||
email: "admin@almeira.com.co",
|
||||
prefs: {}
|
||||
prefs: { role: 'admin' }
|
||||
};
|
||||
}
|
||||
|
||||
if (!sessionCookie || !sessionCookie.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionClient = await createSessionClient();
|
||||
if (!sessionClient) {
|
||||
console.log("getLoggedInUser: No se encontró cookie de sesión o cliente inválido.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await sessionClient.account.get();
|
||||
console.log("getLoggedInUser: Sesión validada para usuario:", user.email);
|
||||
return user;
|
||||
return await sessionClient.account.get();
|
||||
} catch (error: any) {
|
||||
console.error("getLoggedInUser: Error al validar sesión:", error.message || error);
|
||||
if (error?.code !== 401) {
|
||||
console.error("getLoggedInUser: Error al validar sesión:", error.message || error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user