feat: Refactor UI and subscription logic

Introduce improved UI elements for the application name and logo. Implement enhanced subscription management and display features, including expiration checks and remaining days calculation. Update pricing for various subscription plans to reflect new offerings.

This commit also includes:
- Fixes for rendering inconsistencies.
- Refinements to the analysis view and error handling.
- Clarifications in admin dashboard user profile fields for subscription details.
This commit is contained in:
desartstudio95
2026-05-11 16:57:29 +02:00
parent d81a49c2a4
commit 11981e28b0
6 changed files with 140 additions and 29 deletions
+2 -2
View File
@@ -306,7 +306,7 @@ export default function App() {
<div className="space-y-0.5">
<h1 className="text-2xl font-black italic tracking-tighter uppercase leading-none">
{isSecretAdminRoute ? <>FXBROS <span className="text-brand-red">ADMIN</span></> : <>QUANT<span className="text-brand-red">SCAN</span></>}
{isSecretAdminRoute ? <>FXBROS <span className="text-brand-red">ADMIN</span></> : <><span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span></>}
</h1>
<p className="text-zinc-500 font-medium text-[10px] leading-tight">
{isSecretAdminRoute ? "Acesso Restrito" : "Acesse a inteligência institucional Pro."}
@@ -626,7 +626,7 @@ export default function App() {
/>
<div className="flex flex-col">
<span className="text-[10px] text-zinc-500 tracking-widest uppercase font-bold">Olá Humano, Bem-Vindo</span>
<div className="font-black italic text-xl tracking-tighter uppercase leading-none">QUANT<span className="text-brand-red">SCAN</span></div>
<div className="font-black italic text-xl tracking-tighter uppercase leading-none"><span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span></div>
</div>
</div>
<button onClick={handleLogout} className="text-zinc-500 hover:text-white p-2">
+88 -16
View File
@@ -12,8 +12,10 @@ interface UserProfile {
isPremium: boolean;
isAdmin: boolean;
analysisLimit?: number;
plan?: 'basic' | 'pro' | 'elite';
plan?: 'basic' | 'pro' | 'elite' | 'lifetime';
isApproved?: boolean;
subscriptionStartsAt?: number;
subscriptionEndsAt?: number | null;
}
export const AdminDashboard: React.FC = () => {
@@ -108,12 +110,20 @@ export const AdminDashboard: React.FC = () => {
}
};
const handleToggleApproval = async (userId: string, currentStatus: boolean) => {
const handleToggleApproval = async (user: any) => {
try {
await updateDoc(doc(db, 'users', userId), {
isApproved: !currentStatus
}).catch(err => {
handleFirestoreError(err, OperationType.UPDATE, 'users/' + userId);
const updates: any = { isApproved: !user.isApproved };
// Se estiver aprovando pela primeira vez (ou não tem data), define o início e fim
if (!user.isApproved && !user.subscriptionStartsAt) {
updates.subscriptionStartsAt = Date.now();
if (user.plan !== 'lifetime') {
updates.subscriptionEndsAt = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30 dias
}
}
await updateDoc(doc(db, 'users', user.uid), updates).catch(err => {
handleFirestoreError(err, OperationType.UPDATE, 'users/' + user.uid);
throw err;
});
} catch (err) {
@@ -122,25 +132,37 @@ export const AdminDashboard: React.FC = () => {
}
};
const handlePlanChange = async (userId: string, newPlan: 'basic' | 'pro' | 'elite') => {
const handlePlanChange = async (user: any, newPlan: 'basic' | 'pro' | 'elite' | 'lifetime') => {
let limit = 8;
let isPremium = false;
if (newPlan === 'pro') {
limit = 15;
isPremium = true;
} else if (newPlan === 'elite') {
} else if (newPlan === 'elite' || newPlan === 'lifetime') {
limit = 999999; // Represents unlimited
isPremium = true;
}
const updates: any = {
plan: newPlan,
analysisLimit: limit,
isPremium
};
// Atualiza a validade se mudar o plano
if (newPlan === 'lifetime') {
updates.subscriptionEndsAt = null; // Lifetime não expira
} else {
// Se não tinha data de fim e já estava aprovado, define 30 dias a partir de agora
if (user.isApproved && !user.subscriptionEndsAt) {
updates.subscriptionEndsAt = Date.now() + 30 * 24 * 60 * 60 * 1000;
}
}
try {
await updateDoc(doc(db, 'users', userId), {
plan: newPlan,
analysisLimit: limit,
isPremium
}).catch(err => {
handleFirestoreError(err, OperationType.UPDATE, 'users/' + userId);
await updateDoc(doc(db, 'users', user.uid), updates).catch(err => {
handleFirestoreError(err, OperationType.UPDATE, 'users/' + user.uid);
throw err;
});
} catch (err) {
@@ -149,6 +171,26 @@ export const AdminDashboard: React.FC = () => {
}
};
const handleRenewSubscription = async (user: any) => {
try {
const now = Date.now();
const currentEnd = user.subscriptionEndsAt || now;
// Retoma do momento atual se já expirou, ou adiciona ao fim atual se ainda está ativo
const newEnd = (currentEnd > now ? currentEnd : now) + 30 * 24 * 60 * 60 * 1000;
await updateDoc(doc(db, 'users', user.uid), {
subscriptionEndsAt: newEnd
}).catch(err => {
handleFirestoreError(err, OperationType.UPDATE, 'users/' + user.uid);
throw err;
});
alert('Assinatura renovada com sucesso por 30 dias!');
} catch (err) {
console.error("Failed to renew user", err);
alert('Falha ao renovar assinatura.');
}
};
const handleDeleteTestimonial = async (id: string) => {
if (!confirm('Tem certeza que deseja remover este resultado?')) return;
try {
@@ -309,6 +351,8 @@ export const AdminDashboard: React.FC = () => {
<th className="p-4 text-center">Aprovação</th>
<th className="p-4 text-center">Plano</th>
<th className="p-4 text-center">Limite Diário</th>
<th className="p-4 text-center">Assinatura</th>
<th className="p-4 text-center">Ações</th>
<th className="p-4 text-center">Admin</th>
</tr>
</thead>
@@ -332,7 +376,7 @@ export const AdminDashboard: React.FC = () => {
</td>
<td className="p-4 text-center">
<button
onClick={() => handleToggleApproval(user.uid, !!user.isApproved)}
onClick={() => handleToggleApproval(user)}
className={cn(
"px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
user.isApproved
@@ -346,12 +390,13 @@ export const AdminDashboard: React.FC = () => {
<td className="p-4 text-center">
<select
value={user.plan || 'basic'}
onChange={(e) => handlePlanChange(user.uid, e.target.value as 'basic' | 'pro' | 'elite')}
onChange={(e) => handlePlanChange(user, e.target.value as 'basic' | 'pro' | 'elite' | 'lifetime')}
className="bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white uppercase font-black tracking-widest focus:outline-none focus:border-brand-red/50 cursor-pointer"
>
<option value="basic">Básico</option>
<option value="pro">Premium (Pro)</option>
<option value="elite">Elite</option>
<option value="lifetime">Lifetime</option>
</select>
</td>
<td className="p-4 text-center">
@@ -359,6 +404,33 @@ export const AdminDashboard: React.FC = () => {
{user.analysisLimit === 999999 ? 'Ilimitado' : user.analysisLimit ?? 8}
</span>
</td>
<td className="p-4 text-center">
{user.plan === 'lifetime' ? (
<span className="text-[10px] font-black text-brand-red uppercase tracking-widest">Vitalício</span>
) : user.subscriptionEndsAt ? (
<div className="flex flex-col items-center justify-center">
<span className={cn(
"text-[10px] font-bold font-mono",
user.subscriptionEndsAt < Date.now() ? "text-brand-red" :
user.subscriptionEndsAt < Date.now() + 3*24*60*60*1000 ? "text-orange-500" : "text-zinc-400"
)}>
{user.subscriptionEndsAt < Date.now() ? "Expirado" : "Vence"}: {new Date(user.subscriptionEndsAt).toLocaleDateString('pt-BR')}
</span>
</div>
) : (
<span className="text-[10px] text-zinc-600 font-bold">-</span>
)}
</td>
<td className="p-4 text-center">
{user.plan !== 'lifetime' && user.isApproved && (
<button
onClick={() => handleRenewSubscription(user)}
className="px-3 py-1 bg-white/10 hover:bg-brand-red/20 text-white hover:text-brand-red border border-white/10 hover:border-brand-red/30 rounded text-[10px] font-black uppercase tracking-widest transition-all"
>
Renovar
</button>
)}
</td>
<td className="p-4 text-center">
{user.isAdmin && (
<span className="bg-brand-red/20 text-brand-red px-2 py-1 rounded text-[10px] font-black uppercase tracking-widest border border-brand-red/20">
+43 -4
View File
@@ -18,6 +18,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido');
const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
const now = Date.now();
const isLifetime = userData?.plan === 'lifetime';
const endsAt = userData?.subscriptionEndsAt;
const isExpired = !!(!isLifetime && endsAt && now > endsAt);
const daysRemaining = !isLifetime && endsAt ? (endsAt - now) / (1000 * 60 * 60 * 24) : null;
const isEndingSoon = daysRemaining !== null && Math.ceil(daysRemaining) <= 3 && Math.ceil(daysRemaining) > 0;
useEffect(() => {
const fetchUsage = async () => {
if (!auth.currentUser) return;
@@ -54,6 +61,10 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
}, [result, userData]);
const handleStartAnalysis = async () => {
if (isExpired) {
setError("Sua assinatura expirou. Por favor, contate o administrador para renovar o plano.");
return;
}
if (!preview || !file) return;
if (usageInfo.used >= usageInfo.limit) {
setError(`Limite diário atingido (${usageInfo.used}/${usageInfo.limit}). Faça upgrade para análises ilimitadas.`);
@@ -129,6 +140,30 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
return (
<div className="max-w-4xl mx-auto space-y-5">
{isExpired && (
<div className="bg-brand-red/10 border border-brand-red p-4 rounded-xl flex items-start gap-3">
<AlertTriangle className="text-brand-red shrink-0 mt-0.5" size={20} />
<div>
<h3 className="text-brand-red font-black text-sm uppercase tracking-widest">Assinatura Expirada</h3>
<p className="text-red-200/70 text-xs mt-1">
Sua assinatura expirou. Renove seu plano para continuar usando o scanner analisador.
</p>
</div>
</div>
)}
{isEndingSoon && (
<div className="bg-orange-500/10 border border-orange-500 p-4 rounded-xl flex items-start gap-3">
<AlertTriangle className="text-orange-500 shrink-0 mt-0.5" size={20} />
<div>
<h3 className="text-orange-500 font-black text-sm uppercase tracking-widest">Assinatura Expirando</h3>
<p className="text-orange-200/70 text-xs mt-1">
Sua assinatura expira em {Math.ceil(daysRemaining)} dia(s). Renove agora para não perder o acesso ao scanner.
</p>
</div>
</div>
)}
<header className="space-y-1">
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-2.5 uppercase">
<div className="w-7 h-7 rounded-lg border-2 border-brand-red flex items-center justify-center red-glow">
@@ -193,10 +228,11 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
{...getRootProps()}
className={cn(
"w-full flex flex-col items-center justify-center py-10 rounded-xl cursor-pointer transition-all duration-300 border border-transparent",
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20"
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20",
isExpired ? "opacity-50 pointer-events-none" : ""
)}
>
<input {...getInputProps()} />
<input {...getInputProps()} disabled={isExpired} />
<div className="w-16 h-16 bg-brand-gray rounded-xl flex items-center justify-center mb-4 border border-white/5 group-hover:border-brand-red/30 transition-colors">
<Upload size={24} className="text-zinc-600 group-hover:scale-110 transition-transform" />
</div>
@@ -205,10 +241,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
</div>
<div className="flex gap-3 w-full">
<label className="flex-1 bg-brand-dark border-2 border-white/5 text-white py-3.5 rounded-xl font-black text-xs hover:border-brand-red/50 transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer relative overflow-hidden group">
<label className={cn(
"flex-1 bg-brand-dark border-2 border-white/5 text-white py-3.5 rounded-xl font-black text-xs hover:border-brand-red/50 transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer relative overflow-hidden group",
isExpired ? "opacity-50 pointer-events-none" : ""
)}>
<Camera size={16} className="text-zinc-400 group-hover:text-brand-red transition-colors" />
<span className="whitespace-nowrap pt-0.5">TIRAR FOTO</span>
<input type="file" accept="image/*" capture="environment" className="opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange={(e) => {
<input type="file" accept="image/*" capture="environment" disabled={isExpired} className="opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange={(e) => {
const f = e.target.files?.[0];
if (f) onDrop([f]);
}} />
+2 -2
View File
@@ -40,7 +40,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
referrerPolicy="no-referrer"
/>
<span className="font-black italic text-2xl tracking-tighter uppercase">
QUANT<span className="text-brand-red">SCAN</span>
<span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span>
</span>
</div>
@@ -334,7 +334,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
<TrendingUp size={18} className="text-white" />
</div>
<span className="font-black italic text-xl tracking-tighter uppercase">
QUANT<span className="text-brand-red">SCAN</span>
<span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span>
</span>
</div>
<p className="text-zinc-600 text-xs max-w-lg mx-auto">
+1 -1
View File
@@ -37,7 +37,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin
/>
<div className="flex flex-col">
<span className="font-black italic text-xl tracking-tighter uppercase leading-none">
QUANT<span className="text-brand-red">SCAN</span>
<span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span>
</span>
<span className="text-[10px] font-black uppercase text-zinc-500 tracking-[0.3em] mt-1">IA TRADER</span>
</div>
+4 -4
View File
@@ -237,7 +237,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
<section id="planos" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
<PlanCard
title="Plano Begin"
price="500 MZN"
price="1.700 MT"
description="Para quem está começando e quer testar a potência."
buttonText="Assinar Agora"
onClick={handleAction}
@@ -251,7 +251,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
/>
<PlanCard
title="Plano Pro"
price="1.500 MZN"
price="3.500 MT"
description="O plano mais popular para traders consistentes."
buttonText="Assinar Agora"
highlight={true}
@@ -268,7 +268,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
/>
<PlanCard
title="Plano Elite"
price="3.000 MZN"
price="5.700 MT"
description="Acesso total às ferramentas institucionais."
buttonText="Ser Elite"
onClick={handleAction}
@@ -282,7 +282,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
/>
<PlanCard
title="Plano Lifetime"
price="8.000 MT"
price="10.000 MT"
description="O acesso definitivo para toda a vida."
buttonText="Assinar Vitalício"
highlight={true}