feat: Implement maintenance mode functionality

Introduces a new maintenance mode feature to allow administrators to temporarily disable the application for updates or maintenance.

This includes:
- A dedicated `MaintenancePage` component.
- Global state management for maintenance mode and message in `App.tsx`.
- Firestore integration for storing and retrieving maintenance settings.
- Updates to `AdminDashboard.tsx` to manage these settings.
- Firebase security rules adjustments for the new settings collection.
- Animate transitions for signal results in `SignalHistory.tsx`.
This commit is contained in:
desartstudio95
2026-05-08 11:42:38 +02:00
parent 15b618dda3
commit 6af03ef95e
6 changed files with 216 additions and 11 deletions
+89 -2
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Users, ShieldCheck, Search, Loader2 } from 'lucide-react';
import { Users, ShieldCheck, Search, Loader2, Settings, ShieldAlert } from 'lucide-react';
import { cn } from '../lib/utils';
import { collection, onSnapshot, doc, updateDoc, addDoc, deleteDoc, query, orderBy } from 'firebase/firestore';
import { collection, onSnapshot, doc, updateDoc, addDoc, deleteDoc, query, orderBy, getDoc, setDoc } from 'firebase/firestore';
import { db, handleFirestoreError, OperationType, auth } from '../lib/firebase';
import { ImageUploader } from './ImageUploader';
@@ -25,6 +25,9 @@ export const AdminDashboard: React.FC = () => {
const [testText, setTestText] = useState('');
const [testImageUrls, setTestImageUrls] = useState<string[]>([]);
const [submittingTestimonial, setSubmittingTestimonial] = useState(false);
const [maintenanceMode, setMaintenanceMode] = useState(false);
const [maintenanceMessage, setMaintenanceMessage] = useState('');
const [savingSettings, setSavingSettings] = useState(false);
const handleCreateTestimonial = async (e: React.FormEvent) => {
e.preventDefault();
@@ -70,12 +73,41 @@ export const AdminDashboard: React.FC = () => {
console.error("Testimonials fetch error:", error);
});
const settingsRef = doc(db, 'settings', 'app');
const unsubSettings = onSnapshot(settingsRef, (docSnap) => {
if (docSnap.exists()) {
setMaintenanceMode(docSnap.data().maintenanceMode || false);
setMaintenanceMessage(docSnap.data().maintenanceMessage || '');
}
}, (error) => {
console.warn("Failed to listen to settings:", error);
});
return () => {
unsub();
unsubTestimonials();
unsubSettings();
};
}, []);
const handleSaveSettings = async () => {
setSavingSettings(true);
try {
const settingsRef = doc(db, 'settings', 'app');
// Using setDoc with merge to create it if it doesn't exist
await setDoc(settingsRef, {
maintenanceMode,
maintenanceMessage
}, { merge: true });
alert('Configurações salvas com sucesso!');
} catch (err) {
console.error(err);
alert('Erro ao salvar as configurações.');
} finally {
setSavingSettings(false);
}
};
const handleToggleApproval = async (userId: string, currentStatus: boolean) => {
try {
await updateDoc(doc(db, 'users', userId), {
@@ -156,6 +188,61 @@ export const AdminDashboard: React.FC = () => {
</div>
</div>
<div className="glass-card p-6 space-y-4 border-l-4 border-l-brand-red mb-8">
<div className="flex items-center gap-3 mb-4">
<Settings className="text-zinc-400" size={20} />
<h3 className="font-black uppercase tracking-widest text-zinc-300 text-sm">Configurações do Sistema</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4 bg-black/20 p-4 rounded-xl border border-white/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ShieldAlert className={maintenanceMode ? "text-brand-red" : "text-zinc-500"} size={18} />
<span className="text-sm font-bold text-white uppercase tracking-wider">Modo de Manutenção</span>
</div>
<button
onClick={() => setMaintenanceMode(!maintenanceMode)}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
maintenanceMode ? "bg-brand-red" : "bg-zinc-700"
)}
>
<span className={cn(
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
maintenanceMode ? "translate-x-6" : "translate-x-1"
)} />
</button>
</div>
<p className="text-xs text-zinc-500 leading-relaxed">
O modo de manutenção bloqueia o acesso ao aplicativo para todos os usuários comuns, exibindo uma tela de aviso. Somente administradores poderão acessar a plataforma.
</p>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black uppercase text-zinc-500 tracking-widest">
Mensagem de Manutenção (Opcional)
</label>
<textarea
placeholder="Ex: Estamos realizando melhorias..."
className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-white focus:outline-none focus:ring-1 focus:ring-brand-red text-sm min-h-[80px]"
value={maintenanceMessage}
onChange={(e) => setMaintenanceMessage(e.target.value)}
disabled={!maintenanceMode}
/>
</div>
</div>
<button
onClick={handleSaveSettings}
disabled={savingSettings}
className="mt-6 w-full md:w-auto px-6 py-2.5 bg-white/10 hover:bg-white/20 text-white text-xs font-black uppercase tracking-widest rounded-lg transition-colors flex items-center justify-center gap-2"
>
{savingSettings ? <Loader2 size={16} className="animate-spin" /> : <ShieldCheck size={16} />}
Salvar Configurações
</button>
</div>
<form onSubmit={handleCreateTestimonial} className="glass-card p-6 space-y-4">
<h3 className="font-black uppercase tracking-widest text-brand-red text-sm">Postar Resultado como Admin</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+67
View File
@@ -0,0 +1,67 @@
import React from 'react';
import { motion } from 'motion/react';
import { ShieldAlert, RefreshCw, Activity } from 'lucide-react';
interface MaintenancePageProps {
message?: string;
}
export const MaintenancePage: React.FC<MaintenancePageProps> = ({ message }) => {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center p-6 relative overflow-hidden">
{/* Background Ambience */}
<div className="absolute inset-0 z-0">
<div className="absolute top-1/-4 left-1/4 w-96 h-96 bg-brand-red/10 rounded-full blur-[120px]" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-brand-red/5 rounded-full blur-[120px]" />
</div>
{/* Grid Pattern */}
<div
className="absolute inset-0 z-0 opacity-20 pointer-events-none"
style={{ backgroundImage: 'linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px)', backgroundSize: '40px 40px' }}
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="relative z-10 max-w-xl w-full flex flex-col items-center text-center"
>
<div className="relative mb-8">
<div className="absolute inset-0 bg-brand-red/20 blur-xl rounded-full" />
<div className="w-24 h-24 bg-black border border-brand-red/30 rounded-2xl flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-tr from-brand-red/20 to-transparent" />
<Activity className="text-brand-red w-10 h-10 animate-pulse" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-black italic tracking-tighter mb-4 text-white uppercase drop-shadow-sm">
SISTEMA EM <span className="text-brand-red">MANUTENÇÃO</span>
</h1>
<p className="text-zinc-400 text-lg md:text-xl font-medium mb-8 max-w-md mx-auto">
{message || 'Estamos realizando atualizações e melhorias no QuantScan IA. Voltaremos em breve com o sistema operando em capacidade máxima.'}
</p>
<div className="glass-card w-full p-6 bg-black/40 border border-brand-red/10 flex flex-col items-center">
<div className="flex items-center gap-3 text-brand-red mb-3">
<ShieldAlert size={20} />
<span className="font-black text-xs tracking-widest uppercase">Status do Servidor Offline</span>
</div>
<div className="w-full bg-white/5 rounded-full h-1.5 mb-2 overflow-hidden">
<div className="bg-brand-red h-full w-full rounded-full animate-pulse" style={{ animationDuration: '2s' }} />
</div>
<p className="text-[10px] uppercase tracking-widest text-zinc-500 font-bold">Aguardando reconexão institucional</p>
</div>
<button
onClick={() => window.location.reload()}
className="mt-10 flex items-center gap-2 text-zinc-400 hover:text-white transition-colors text-xs font-black uppercase tracking-widest"
>
<RefreshCw size={14} />
Tentar Novamente
</button>
</motion.div>
</div>
);
};
+14 -7
View File
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { motion } from 'motion/react';
import { Signal, SignalResult, SignalType } from '../types';
import { cn } from '../lib/utils';
import { Clock, TrendingUp, TrendingDown, ChevronDown, ChevronUp } from 'lucide-react';
@@ -169,14 +170,20 @@ export const SignalHistory: React.FC = () => {
</div>
<div className="text-right">
<span className="text-[10px] text-zinc-500 font-black uppercase">Resultado</span>
<div className={cn(
"flex items-center justify-end gap-1 font-black italic uppercase text-sm",
signal.result === SignalResult.GAIN ? "text-green-500" :
signal.result === SignalResult.LOSS ? "text-brand-red" :
"text-zinc-500"
)}>
<motion.div
key={signal.result}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, type: 'spring', bounce: 0.4 }}
className={cn(
"flex items-center justify-end gap-1 font-black italic uppercase text-sm",
signal.result === SignalResult.GAIN ? "text-green-500" :
signal.result === SignalResult.LOSS ? "text-brand-red" :
"text-zinc-500"
)}
>
{signal.result}
</div>
</motion.div>
</div>
</div>