Philipe Cardoso Com 33 anos de idade, sou um carioca apaixonado por tecnologia e fotografia. Além de ser o criador do Portal Zoom Digital, que preserva sua essência desde os tempos em que era um blog, também sou um verdadeiro entusiasta e amante de todas as formas de tecnologia. Através do Portal, compartilho minha paixão pela tecnologia e trago as últimas novidades e tendências para os leitores. Também sou fascinado pelo mundo da fotografia, explorando o poder das imagens para capturar momentos únicos e transmitir histórias cativantes.

Código Calculadora Impressão 3D feita no Gemini

19 min read

Código da calculadora mostrada no vídeo:

 

import React, { useState, useEffect, useMemo } from ‘react’;
import {
Calculator,
Settings,
Save,
Trash2,
AlertCircle,
TrendingUp,
Package,
Zap,
Clock,
Info,
Download,
History,
MessageCircle,
Sliders,
CheckCircle2,
Share2,
FileSpreadsheet,
HelpCircle,
X,
ChevronRight,
Copy,
Coins,
Box
} from ‘lucide-react’;
// — Utilitários —
const parseLocalFloat = (str) => {
if (typeof str === ‘number’) return str;
if (!str || typeof str !== ‘string’ || str.trim() === ”) return0;
return parseFloat(str.replace(‘,’, ‘.’));
};
const parseSmartTime = (input) => {
if (!input) return0;
const str = input.toString().toLowerCase().replace(‘,’, ‘.’);
if (str.includes(‘:’)) {
const [h, m] = str.split(‘:’);
return parseFloat(h) + (parseFloat(m) / 60);
}
let totalHours = 0;
const matchH = str.match(/([\d.]+)h/);
const matchM = str.match(/([\d.]+)m/);
if (matchH) totalHours += parseFloat(matchH[1]);
if (matchM) totalHours += parseFloat(matchM[1]) / 60;
if (!matchH && !matchM) return parseFloat(str) || 0;
return totalHours;
};
const parseSmartWeight = (input) => {
if (!input) return0;
const str = input.toString().toLowerCase().replace(‘,’, ‘.’);
if (str.includes(‘kg’)) return parseFloat(str.replace(‘kg’, ”)) * 1000;
if (str.includes(‘g’)) return parseFloat(str.replace(‘g’, ”));
return parseFloat(str) || 0;
};
const formatCurrency = (value) => {
if (isNaN(value) || value === null) return’R$ 0,00′;
returnnewIntl.NumberFormat(‘pt-BR’, { style: ‘currency’, currency: ‘BRL’ }).format(value);
};
const formatPercentage = (value) => {
if (isNaN(value) || value === null) return’0,00%’;
return`${value.toFixed(1).replace(‘.’, ‘,’)}%`;
};
// — Presets —
const MATERIAL_PRESETS = [
{ label: ‘PLA’, cost: ‘100.00’ },
{ label: ‘PETG’, cost: ‘110.00’ },
{ label: ‘ABS’, cost: ‘90.00’ },
{ label: ‘Resina’, cost: ‘250.00’ }
];
const PRINTER_PRESETS = [
{ label: ‘Ender 3/V2’, watts: ‘150’, price: ‘1800.00’ },
{ label: ‘Bambu A1’, watts: ‘130’, price: ‘3600.00’ },
{ label: ‘Bambu X1C’, watts: ‘350’, price: ‘12000.00’ },
{ label: ‘Resina’, watts: ’80’, price: ‘2500.00’ }
];
// — Componentes UI —
const Toast = ({ message, type, onClose }) => (
<div className={`fixed top-4 right-4 z-50 px-6 py-4 rounded-2xl shadow-xl flex items-center gap-3 animate-fade-in-down border border-opacity-20 backdrop-blur-md ${
type === ‘error’ ? ‘bg-red-50 border-red-200 text-red-800’ : ‘bg-emerald-50 border-emerald-200 text-emerald-800’
}`}>
{type === ‘error’ ? <AlertCircle size={20}/> : <CheckCircle2 size={20}/>}
<span className=”font-medium text-sm”>{message}</span>
<button onClick={onClose} className=”ml-2 hover:opacity-70 transition-opacity”><X size={16}/></button>
</div>
);
const ConfirmModal = ({ isOpen, title, message, onConfirm, onCancel }) => {
if (!isOpen) returnnull;
return (
<div className=”fixed inset-0 z-50 flex items-center justify-center bg-gray-900/40 backdrop-blur-sm p-4 transition-all”>
<div className=”bg-white rounded-2xl shadow-2xl max-w-sm w-full p-6 animate-scale-in border border-gray-100″>
<h3 className=”text-xl font-bold text-gray-800 mb-2″>{title}</h3>
<p className=”text-gray-500 text-sm mb-8 leading-relaxed”>{message}</p>
<div className=”flex justify-end gap-3″>
<button onClick={onCancel} className=”px-5 py-2.5 text-gray-600 hover:bg-gray-100 rounded-xl text-sm font-semibold transition-colors”>Cancelar</button>
<button onClick={onConfirm} className=”px-5 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl text-sm font-semibold shadow-lg shadow-red-500/30 transition-all active:scale-95″>Confirmar</button>
</div>
</div>
</div>
);
};
const Card = ({ children, className = “”, noPadding = false }) => (
<div className={`bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden transition-all hover:shadow-md ${className}`}>
{children}
</div>
);
const Tooltip = ({ text, children }) => (
<div className=”relative flex items-center group”>
{children}
<div className=”absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-48 p-3 text-xs font-medium text-white bg-gray-800 rounded-xl shadow-xl opacity-0 group-hover:opacity-100 transition-all duration-200 z-50 pointer-events-none text-center translate-y-2 group-hover:translate-y-0″>
{text}
<div className=”absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-800″></div>
</div>
</div>
);
const InputField = ({ id, label, value, onChange, error, placeholder, icon: Icon, helpText, suffix, onBlur }) => (
<div className=”flex flex-col mb-5 relative group”>
<label htmlFor={id} className=”mb-2 text-xs font-bold text-gray-500 uppercase tracking-wide flex items-center justify-between”>
<span className=”flex items-center gap-1.5″>
{Icon && <Icon className=”w-3.5 h-3.5 text-indigo-500″ />}
{label}
{helpText && (
<Tooltip text={helpText}>
<Info className=”w-3.5 h-3.5 text-gray-400 cursor-help hover:text-indigo-500 transition-colors” />
</Tooltip>
)}
</span>
</label>
<div className=”relative”>
<input
id={id}
type=”text”
inputMode=”decimal”
value={value}
onChange={(e) => onChange(id, e.target.value)}
onBlur={onBlur}
placeholder={placeholder}
className={`w-full pl-4 pr-10 py-3 bg-gray-50/50 border rounded-xl transition-all duration-300 font-semibold text-gray-700 placeholder-gray-400
${error
? ‘border-red-300 bg-red-50/50 focus:ring-4 focus:ring-red-100 focus:border-red-400’
: ‘border-gray-200 focus:bg-white focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 hover:border-gray-300’
}
focus:outline-none`}
/>
{suffix && (
<span className=”absolute right-4 top-3.5 text-gray-400 text-xs font-bold pointer-events-none”>{suffix}</span>
)}
</div>
{error && <p className=”mt-1.5 text-xs text-red-500 flex items-center font-medium animate-pulse”><AlertCircle className=”w-3 h-3 mr-1″/>{error}</p>}
</div>
);
const SliderInput = ({ id, label, value, onChange, min = 0, max = 100, step = 1, suffix = “%”, presets = [] }) => {
const numericValue = parseLocalFloat(value);
return (
<div className=”mb-6 p-4 bg-gray-50/80 rounded-2xl border border-gray-100″>
<div className=”flex justify-between items-center mb-3″>
<label htmlFor={id} className=”text-sm font-bold text-gray-700″>{label}</label>
<div className=”flex items-center bg-white px-3 py-1 rounded-lg border border-gray-200 shadow-sm”>
<input
type=”number” value={numericValue}
onChange={(e) => onChange(id, e.target.value)}
className=”w-12 text-sm font-bold text-indigo-600 text-right focus:outline-none”
/>
<span className=”text-xs text-gray-400 font-bold ml-1″>{suffix}</span>
</div>
</div>
<div className=”flex flex-col gap-3″>
<input
type=”range” min={min} max={max} step={step} value={numericValue}
onChange={(e) => onChange(id, e.target.value)}
className=”w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600 hover:accent-indigo-500 transition-all”
/>
</div>
{presets.length > 0 && (
<div className=”flex gap-2 mt-3 overflow-x-auto pb-1 scrollbar-hide”>
{presets.map(p => (
<button key={p} onClick={() => onChange(id, p.toString())} className=”px-3 py-1.5 text-xs font-semibold bg-white border border-gray-200 text-gray-600 rounded-lg hover:border-indigo-300 hover:text-indigo-600 transition-all shadow-sm active:scale-95 whitespace-nowrap”>
{p}{suffix}
</button>
))}
</div>
)}
</div>
);
};
const ProgressBar = ({ label, value, total, colorClass }) => {
const percent = total > 0 ? (value / total) * 100 : 0;
return (
<div className=”mb-3 last:mb-0″>
<div className=”flex justify-between text-xs mb-1.5″>
<span className=”text-gray-500 font-medium flex items-center gap-1″>{label}</span>
<span className=”font-bold text-gray-700″>{formatCurrency(value)} <span className=”text-gray-400 font-normal ml-1″>({percent.toFixed(1)}%)</span></span>
</div>
<div className=”w-full bg-gray-100 rounded-full h-2 overflow-hidden”>
<div className={`h-full rounded-full ${colorClass} transition-all duration-700 ease-out`} style={{ width: `${Math.min(percent, 100)}%` }}></div>
</div>
</div>
);
};
// — Configurações Iniciais —
const DEFAULT_SETTINGS = {
consumo_watts: ‘150’,
custo_kwh: ‘1.25’,
valor_impressora: ‘6000.00’,
vida_util_horas: ‘4000’,
custo_filamento_padrao: ‘110.00’,
lucro_minimo_projeto: ‘15.00’
};
const DEFAULT_PROJECT = {
id: null,
nome: ”,
quantidade_pecas: ‘1’,
gramas_filamento: ”,
horas_impressao: ”,
custo_filamento_kg: ”,
custo_outros: ‘0’,
custo_embalagem: ‘0’,
margem_lucro_percentual: ‘100’,
falha_risco_percentual: ‘0’
};
// — Componente Principal —
export default function App() {
const [activeTab, setActiveTab] = useState(‘calc’);
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [project, setProject] = useState(DEFAULT_PROJECT);
const [history, setHistory] = useState([]);
// UI States
const [showAdvanced, setShowAdvanced] = useState(true);
const [viewUnitMode, setViewUnitMode] = useState(false);
// New Feedback States
const [notification, setNotification] = useState(null);
const [confirmDialog, setConfirmDialog] = useState(null);
// Inicialização
useEffect(() => {
const savedSettings = localStorage.getItem(‘calc3d-settings-v6’);
const savedProject = localStorage.getItem(‘calc3d-project-v6’);
const savedHistory = localStorage.getItem(‘calc3d-history-v1’);
if (savedSettings) setSettings(JSON.parse(savedSettings));
if (savedProject) setProject(JSON.parse(savedProject));
if (savedHistory) setHistory(JSON.parse(savedHistory));
else {
setProject(prev => ({…prev, custo_filamento_kg: DEFAULT_SETTINGS.custo_filamento_padrao}));
}
}, []);
// Persistência
useEffect(() => { localStorage.setItem(‘calc3d-settings-v6’, JSON.stringify(settings)); }, [settings]);
useEffect(() => { localStorage.setItem(‘calc3d-project-v6’, JSON.stringify(project)); }, [project]);
useEffect(() => { localStorage.setItem(‘calc3d-history-v1’, JSON.stringify(history)); }, [history]);
const showToast = (message, type = ‘success’) => {
setNotification({ message, type });
setTimeout(() => setNotification(null), 4000);
};
const handleSettingChange = (id, val) => setSettings(prev => ({ …prev, [id]: val }));
const handleProjectChange = (id, val) => setProject(prev => ({ …prev, [id]: val }));
const handleTimeBlur = () => {
const hours = parseSmartTime(project.horas_impressao);
if (hours > 0) handleProjectChange(‘horas_impressao’, hours.toFixed(2));
};
const handleWeightBlur = () => {
const grams = parseSmartWeight(project.gramas_filamento);
if (grams > 0) handleProjectChange(‘gramas_filamento’, grams.toFixed(1));
};
// — CÁLCULOS —
const results = useMemo(() => {
constS = {
watts: parseLocalFloat(settings.consumo_watts),
kwh: parseLocalFloat(settings.custo_kwh),
printerPrice: parseLocalFloat(settings.valor_impressora),
lifeHours: parseLocalFloat(settings.vida_util_horas),
minProfit: parseLocalFloat(settings.lucro_minimo_projeto)
};
constP = {
qty: Math.max(1, parseInt(project.quantidade_pecas) || 1),
grams: parseSmartWeight(project.gramas_filamento),
hours: parseSmartTime(project.horas_impressao),
filPrice: parseLocalFloat(project.custo_filamento_kg) || parseLocalFloat(settings.custo_filamento_padrao),
others: parseLocalFloat(project.custo_outros),
pack: parseLocalFloat(project.custo_embalagem),
margin: parseLocalFloat(project.margem_lucro_percentual),
risk: parseLocalFloat(project.falha_risco_percentual)
};
const kwhPrice = (S.watts / 1000) * S.kwh;
const depreciationPrice = S.lifeHours > 0 ? S.printerPrice / S.lifeHours : 0;
const costFilament = (P.grams / 1000) * P.filPrice;
const costEnergy = kwhPrice * P.hours;
const costDepreciation = depreciationPrice * P.hours;
const costExtras = P.others + P.pack;
const productionSum = costFilament + costEnergy + costDepreciation;
const costFailMargin = productionSum * (P.risk / 100);
const baseProductionCost = productionSum + costFailMargin;
// CUSTO TOTAL (Custo de manufatura + Extras)
const totalCost = baseProductionCost + costExtras;
const unitTotalCost = totalCost / P.qty;
const profitValue = baseProductionCost * (P.margin / 100);
const finalPrice = baseProductionCost + profitValue + costExtras;
const actualProfit = finalPrice – totalCost;
const unitPrice = finalPrice / P.qty;
const unitProfit = actualProfit / P.qty;
// Sugestão de Preço para Lucro Mínimo
const suggestedPriceMinProfit = totalCost + S.minProfit;
return {
S, P,
costs: { filament: costFilament, energy: costEnergy, depreciation: costDepreciation, risk: costFailMargin, packaging: P.pack, others: P.others, totalExtras: costExtras },
baseProductionCost,
totalCost,
unitTotalCost,
actualProfit, finalPrice, unitPrice, unitProfit,
isLowProfit: actualProfit < S.minProfit,
isHighRisk: P.risk > 30,
suggestedPriceMinProfit,
isValid: P.grams > 0 && P.hours > 0,
missingFields: { grams: P.grams <= 0, hours: P.hours <= 0 }
};
}, [settings, project]);
// — Ações —
const saveToHistory = () => {
if (!project.nome) {
showToast(“Dê um nome ao projeto para salvar.”, “error”);
return;
}
const newEntry = { …project, id: Date.now(), date: newDate().toISOString(), finalPrice: results.finalPrice };
setHistory(prev => [newEntry, …prev]);
showToast(“Projeto salvo no histórico!”);
};
const confirmReset = () => {
setConfirmDialog({
title: “Limpar Projeto”,
message: “Tem certeza que deseja apagar todos os dados atuais do formulário?”,
action: () => {
setProject({ …DEFAULT_PROJECT, custo_filamento_kg: settings.custo_filamento_padrao });
setConfirmDialog(null);
showToast(“Dados limpos com sucesso.”);
}
});
};
const confirmDeleteHistory = (id) => {
setConfirmDialog({
title: “Excluir Registro”,
message: “Essa ação não pode ser desfeita. Deseja continuar?”,
action: () => {
setHistory(prev => prev.filter(i => i.id !== id));
setConfirmDialog(null);
showToast(“Item excluído.”);
}
});
};
const loadFromHistory = (item) => {
setProject(item);
setActiveTab(‘calc’);
showToast(“Projeto carregado!”);
};
const applyPsychologicalPrice = () => {
const current = results.finalPrice;
const integerPart = Math.floor(current);
let suggestion = integerPart + 0.90;
if (suggestion < current) suggestion += 1;
const extras = results.costs.totalExtras;
const cost = results.baseProductionCost;
if (cost > 0) {
const newMargin = (((suggestion – extras) / cost) – 1) * 100;
handleProjectChange(‘margem_lucro_percentual’, newMargin.toFixed(2));
showToast(“Preço arredondado!”);
}
};
const openWhatsApp = () => {
const text = `*Orçamento 3D*\n📦 Projeto: ${project.nome || ‘Peça’}\n💰 Valor: ${formatCurrency(results.finalPrice)}\n(Gerado por ZoomCalc3D)`;
const url = `https://wa.me/?text=${encodeURIComponent(text)}`;
window.open(url, ‘_blank’);
};
// — COPY TO CLIPBOARD (Universal) —
const copyToClipboard = (text) => {
const textArea = document.createElement(“textarea”);
textArea.value = text;
// Evita scroll quando o elemento é inserido
textArea.style.top = “0”;
textArea.style.left = “0”;
textArea.style.position = “fixed”;
textArea.style.opacity = “0”; // Invisível
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand(‘copy’);
if(successful) showToast(“Copiado! Cole no Google Sheets (Ctrl+V).”, “success”);
else showToast(“Erro ao copiar.”, “error”);
} catch (err) {
console.error(‘Fallback: Oops, unable to copy’, err);
showToast(“Erro no navegador.”, “error”);
}
document.body.removeChild(textArea);
};
const copyToSheets = () => {
if (!results.isValid) return;
const fmt = (num) => typeof num === ‘number’ ? num.toFixed(2).replace(‘.’, ‘,’) : num;
const tsvContent = `
PROJETO\t${project.nome || “Sem nome”}
DATA\t${new Date().toLocaleDateString()}
Item\tTotal\tUnitário
Filamento\t${fmt(results.costs.filament)}\t${fmt(results.costs.filament/results.P.qty)}
Energia\t${fmt(results.costs.energy)}\t${fmt(results.costs.energy/results.P.qty)}
Desgaste\t${fmt(results.costs.depreciation)}\t${fmt(results.costs.depreciation/results.P.qty)}
Risco\t${fmt(results.costs.risk)}\t${fmt(results.costs.risk/results.P.qty)}
Extras\t${fmt(results.costs.totalExtras)}\t${fmt(results.costs.totalExtras/results.P.qty)}
CUSTO TOTAL\t${fmt(results.totalCost)}\t${fmt(results.totalCost/results.P.qty)}
LUCRO\t${fmt(results.actualProfit)}\t${fmt(results.actualProfit/results.P.qty)}
PREÇO FINAL\t${fmt(results.finalPrice)}\t${fmt(results.unitPrice)}
`.trim();
copyToClipboard(tsvContent);
};
// — EXPORT ALL HISTORY CSV —
const exportHistoryCSV = () => {
if (history.length === 0) {
showToast(“Histórico vazio.”, “error”);
return;
}
constBOM = “\uFEFF”;
const header = [“Data”, “Projeto”, “Qtd”, “Preco Final”];
const rows = history.map(item => [
newDate(item.date).toLocaleDateString(),
item.nome || “Sem nome”,
item.quantidade_pecas,
formatCurrency(item.finalPrice).replace(‘R$’, ”).trim()
]);
const csvContent = BOM + [header, …rows].map(e => e.join(“;”)).join(“\n”);
const blob = newBlob([csvContent], { type: ‘text/csv;charset=utf-8;’ });
const link = document.createElement(“a”);
link.href = URL.createObjectURL(blob);
link.download = `historico_completo_${new Date().toISOString().slice(0,10)}.csv`;
link.click();
};
const exportToCSV = () => {
if (!results.isValid) return;
const safeName = project.nome ? project.nome.replace(/[^a-z0-9]/gi, ‘_’) : ‘orcamento’;
constBOM = “\uFEFF”;
const fmt = (num) => typeof num === ‘number’ ? num.toFixed(2).replace(‘.’, ‘,’) : num;
const rows = [
[“ITEM”, “VALOR TOTAL”, “VALOR UNITARIO”],
[“Nome do Projeto”, project.nome || “-“],
[“CUSTO: Filamento”, fmt(results.costs.filament), fmt(results.costs.filament/results.P.qty)],
[“CUSTO: Energia”, fmt(results.costs.energy), fmt(results.costs.energy/results.P.qty)],
[“CUSTO: Desgaste”, fmt(results.costs.depreciation), fmt(results.costs.depreciation/results.P.qty)],
[“CUSTO: Risco”, fmt(results.costs.risk), fmt(results.costs.risk/results.P.qty)],
[“CUSTO: Extras”, fmt(results.costs.totalExtras), fmt(results.costs.totalExtras/results.P.qty)],
[“—“, “—“, “—“],
[“CUSTO TOTAL (PRODUÇÃO)”, fmt(results.totalCost), fmt(results.totalCost/results.P.qty)],
[“Lucro”, fmt(results.actualProfit), fmt(results.actualProfit/results.P.qty)],
[“PREÇO FINAL”, fmt(results.finalPrice), fmt(results.unitPrice)]
];
const csvContent = BOM + rows.map(e => e.join(“;”)).join(“\n”);
const blob = newBlob([csvContent], { type: ‘text/csv;charset=utf-8;’ });
const link = document.createElement(“a”);
link.href = URL.createObjectURL(blob);
link.download = `${safeName}.csv`;
link.click();
};
return (
<div className=”min-h-screen bg-slate-50 text-gray-800 font-sans pb-24 selection:bg-indigo-100 selection:text-indigo-800″>
{notification && <Toast message={notification.message} type={notification.type} onClose={() => setNotification(null)} />}
<ConfirmModal
isOpen={!!confirmDialog}
title={confirmDialog?.title}
message={confirmDialog?.message}
onConfirm={confirmDialog?.action}
onCancel={() => setConfirmDialog(null)}
/>
{/* Modern Gradient Header */}
<div className=”sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-gray-200/60 shadow-sm transition-all”>
<div className=”max-w-5xl mx-auto px-4 sm:px-6 h-18 flex items-center justify-between py-3″>
<div className=”flex items-center gap-3″>
<div className=”bg-gradient-to-br from-indigo-500 to-violet-600 text-white p-2 rounded-xl shadow-lg shadow-indigo-200″>
<Calculator size={22} className=”stroke-[2.5]” />
</div>
<div>
<span className=”font-bold text-xl tracking-tight text-gray-900 leading-tight block”>ZoomCalc3D</span>
<span className=”text-[10px] font-semibold text-gray-400 uppercase tracking-widest hidden sm:block”>Precificação Inteligente</span>
</div>
</div>
<nav className=”flex bg-gray-100/80 p-1.5 rounded-xl overflow-x-auto gap-1″>
{[{ id: ‘calc’, icon: Calculator, label: ‘Calc’ }, { id: ‘history’, icon: History, label: ‘Histórico’ }, { id: ‘settings’, icon: Settings, label: ‘Ajustes’ }].map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)} className={`px-4 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${activeTab === tab.id ? ‘bg-white text-indigo-600 shadow-sm’ : ‘text-gray-500 hover:text-gray-700 hover:bg-gray-200/50’}`}>
<tab.icon size={16} className={activeTab === tab.id ? “stroke-[2.5]” : “stroke-2”} /> {tab.label}
</button>
))}
</nav>
</div>
</div>
<main className=”max-w-5xl mx-auto px-4 sm:px-6 py-8″>
{/* — SETTINGS TAB — */}
{activeTab === ‘settings’ && (
<div className=”max-w-2xl mx-auto animate-fade-in”>
<div className=”mb-6″>
<h2 className=”text-2xl font-bold text-gray-900″>Configurações</h2>
<p className=”text-gray-500 text-sm”>Personalize os parâmetros da sua oficina.</p>
</div>
<Card className=”p-8 space-y-8″>
<div>
<div className=”flex justify-between items-center mb-6″>
<h3 className=”text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2″><Zap size={14}/> Perfil da Impressora</h3>
<div className=”flex gap-2″>
{PRINTER_PRESETS.map((p, i) => (
<button key={i} onClick={() => setSettings(prev => ({…prev, consumo_watts: p.watts, valor_impressora: p.price}))} className=”text-[10px] font-bold bg-indigo-50 text-indigo-600 px-3 py-1.5 rounded-lg border border-indigo-100 hover:bg-indigo-100 hover:border-indigo-200 transition-colors uppercase tracking-wide”>
{p.label}
</button>
))}
</div>
</div>
<div className=”grid grid-cols-1 sm:grid-cols-2 gap-6″>
<InputField id=”consumo_watts” label=”Consumo (Watts)” value={settings.consumo_watts} onChange={handleSettingChange} suffix=”W” icon={Zap} />
<InputField id=”custo_kwh” label=”Custo Energia (kWh)” value={settings.custo_kwh} onChange={handleSettingChange} icon={Zap} />
<InputField id=”valor_impressora” label=”Preço da Máquina” value={settings.valor_impressora} onChange={handleSettingChange} icon={Calculator} />
<InputField id=”vida_util_horas” label=”Vida Útil” value={settings.vida_util_horas} onChange={handleSettingChange} suffix=”h” icon={Clock} />
</div>
</div>
<div className=”border-t border-gray-100 pt-8″>
<h3 className=”text-sm font-bold text-gray-400 uppercase tracking-wider mb-6 flex items-center gap-2″><TrendingUp size={14}/> Parâmetros de Negócio</h3>
<div className=”grid grid-cols-1 sm:grid-cols-2 gap-6″>
<InputField id=”custo_filamento_padrao” label=”Custo Kg Padrão” value={settings.custo_filamento_padrao} onChange={handleSettingChange} icon={Package} />
<InputField id=”lucro_minimo_projeto” label=”Lucro Mínimo (R$)” value={settings.lucro_minimo_projeto} onChange={handleSettingChange} icon={TrendingUp} />
</div>
</div>
</Card>
</div>
)}
{/* — HISTORY TAB — */}
{activeTab === ‘history’ && (
<div className=”max-w-3xl mx-auto animate-fade-in”>
<div className=”flex justify-between items-end mb-6″>
<div>
<h2 className=”text-2xl font-bold text-gray-900 flex items-center gap-2″>Histórico</h2>
<p className=”text-gray-500 text-sm”>Seus orçamentos salvos.</p>
</div>
<div className=”flex gap-2″>
<button onClick={exportHistoryCSV} className=”bg-white border border-gray-200 text-gray-600 px-3 py-1.5 rounded-lg text-xs font-bold hover:bg-gray-50 flex items-center gap-2 transition-colors”>
<FileSpreadsheet size={14}/> Exportar Tudo (CSV)
</button>
<div className=”bg-indigo-50 text-indigo-600 px-3 py-1.5 rounded-lg text-xs font-bold border border-indigo-100″>
{history.length} Projetos
</div>
</div>
</div>
{history.length === 0 ? (
<div className=”text-center py-20 bg-white rounded-3xl border-2 border-dashed border-gray-200″>
<div className=”bg-gray-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4″>
<History className=”text-gray-300″ size={32}/>
</div>
<p className=”text-gray-500 font-medium”>Nenhum projeto salvo ainda.</p>
<button onClick={() => setActiveTab(‘calc’)} className=”mt-4 text-indigo-600 font-bold text-sm hover:underline”>Criar novo orçamento</button>
</div>
) : (
<div className=”space-y-4″>
{history.map(item => (
<div key={item.id} className=”bg-white p-5 rounded-2xl border border-gray-100 shadow-sm hover:shadow-lg hover:border-indigo-100 transition-all group”>
<div className=”flex justify-between items-center”>
<div>
<h3 className=”font-bold text-gray-800 text-lg group-hover:text-indigo-600 transition-colors”>{item.nome}</h3>
<div className=”flex gap-3 text-xs text-gray-400 mt-1 font-medium uppercase tracking-wide”>
<span>{new Date(item.date).toLocaleDateString()}</span>
<span>•</span>
<span>{item.quantidade_pecas} {item.quantidade_pecas > 1 ? ‘Peças’ : ‘Peça’}</span>
</div>
</div>
<div className=”flex items-center gap-6″>
<span className=”font-bold text-gray-900 text-xl”>{formatCurrency(item.finalPrice)}</span>
<div className=”flex gap-2″>
<button onClick={() => loadFromHistory(item)} className=”p-2.5 text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 rounded-xl transition-colors” title=”Carregar”><Download size={20}/></button>
<button onClick={() => confirmDeleteHistory(item.id)} className=”p-2.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors” title=”Excluir”><Trash2 size={20}/></button>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* — CALCULATOR TAB — */}
{activeTab === ‘calc’ && (
<div className=”grid grid-cols-1 lg:grid-cols-12 gap-8″>
<div className=”lg:col-span-5 space-y-6″>
{/* Form Card */}
<Card>
<div className=”bg-gray-50/80 backdrop-blur px-6 py-4 border-b border-gray-100 flex justify-between items-center”>
<h3 className=”font-bold text-gray-600 flex items-center gap-2 text-xs uppercase tracking-widest”>
<Sliders size={14}/> Parâmetros
</h3>
<div className=”flex gap-2″>
<button onClick={() => setShowAdvanced(!showAdvanced)} className={`text-[10px] font-bold px-3 py-1.5 rounded-lg border transition-all uppercase tracking-wide ${showAdvanced ? ‘bg-indigo-50 text-indigo-600 border-indigo-100’ : ‘bg-white text-gray-400 border-gray-200 hover:border-gray-300’}`}>
{showAdvanced ? ‘Completo’ : ‘Básico’}
</button>
<button onClick={confirmReset} className=”text-gray-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-lg transition-colors”><Trash2 size={16} /></button>
</div>
</div>
<div className=”p-6″>
<div className=”grid grid-cols-3 gap-4 mb-2″>
<div className=”col-span-2″><InputField id=”nome” label=”Nome do Projeto” value={project.nome} onChange={handleProjectChange} placeholder=”Ex: Vaso Geométrico” /></div>
<div className=”col-span-1″><InputField id=”quantidade_pecas” label=”Qtd. Peças (Finalizadas)” value={project.quantidade_pecas} onChange={handleProjectChange} placeholder=”1″ helpText=”Quantidade total de peças finalizadas no lote.”/></div>
</div>
<div className=”bg-gradient-to-br from-indigo-50 to-blue-50 p-5 rounded-2xl border border-indigo-100 mb-6 relative overflow-hidden”>
<div className=”absolute top-0 right-0 w-20 h-20 bg-white opacity-20 rounded-full blur-2xl -mr-10 -mt-10 pointer-events-none”></div>
<div className=”grid grid-cols-2 gap-4 relative z-10″>
<InputField id=”gramas_filamento” label=”Peso Total” value={project.gramas_filamento} onChange={handleProjectChange} onBlur={handleWeightBlur} suffix=”g” placeholder=”0″ error={results.missingFields.grams && “Obrigatório”} />
<InputField id=”horas_impressao” label=”Tempo Total” value={project.horas_impressao} onChange={handleProjectChange} onBlur={handleTimeBlur} suffix=”h” placeholder=”0:00″ error={results.missingFields.hours && “Obrigatório”} />
</div>
</div>
<div className=”mb-6″>
<div className=”flex justify-between items-center mb-3″>
<label className=”text-xs font-bold text-gray-500 uppercase tracking-wide”>Material (Kg)</label>
<div className=”flex gap-2″>
{MATERIAL_PRESETS.map(m => (
<button key={m.label} onClick={() => handleProjectChange(‘custo_filamento_kg’, m.cost)} className=”text-[10px] font-bold bg-white text-gray-500 px-2 py-1 rounded border border-gray-200 hover:border-indigo-300 hover:text-indigo-600 transition-all uppercase”>
{m.label}
</button>
))}
</div>
</div>
<InputField id=”custo_filamento_kg” label=”” value={project.custo_filamento_kg} onChange={handleProjectChange} placeholder={settings.custo_filamento_padrao} icon={Package} />
</div>
<div className={`transition-all duration-500 overflow-hidden ${showAdvanced ? ‘max-h-[500px] opacity-100’ : ‘max-h-0 opacity-0’}`}>
<div className=”border-t border-gray-100 pt-6 mt-2″>
<h4 className=”text-xs font-bold text-gray-400 uppercase mb-4 flex items-center gap-1.5″><TrendingUp size={12}/> Custos Extras</h4>
<div className=”grid grid-cols-2 gap-4 mb-4″>
<InputField id=”custo_outros” label=”Outros (Imãs, etc)” value={project.custo_outros} onChange={handleProjectChange} placeholder=”0.00″ />
<InputField id=”custo_embalagem” label=”Embalagem” value={project.custo_embalagem} onChange={handleProjectChange} placeholder=”0.00″ />
</div>
<SliderInput id=”falha_risco_percentual” label=”Margem de Risco” value={project.falha_risco_percentual} onChange={handleProjectChange} max={50} step={5} presets={[0, 5, 10, 20]} />
</div>
</div>
<div className=”border-t border-gray-100 pt-6 mt-2″>
<div className=”flex items-center gap-2 mb-2″>
<TrendingUp size={16} className=”text-indigo-600″/>
<span className=”text-sm font-bold text-gray-800″>Lucro Desejado</span>
</div>
<SliderInput id=”margem_lucro_percentual” label=”Markup” value={project.margem_lucro_percentual} onChange={handleProjectChange} max={300} step={10} presets={[50, 100, 150, 200]} />
</div>
</div>
</Card>
</div>
<div className=”lg:col-span-7 space-y-6″>
{results.isValid ? (
<>
<div className=”relative group perspective-1000″>
<div className=”absolute -inset-0.5 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200″></div>
<Card className=”relative bg-white border-none ring-1 ring-gray-200 overflow-hidden”>
{/* === DUAL HERO SECTION (Total vs Unit) === */}
<div className=”grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-gray-100″>
{/* 1. LADO ESQUERDO: TOTAL DO PROJETO */}
<div className=”p-6 text-center bg-gradient-to-br from-white to-blue-50/30 flex flex-col justify-between h-full relative overflow-hidden”>
<div className=”absolute top-0 right-0 p-2 opacity-10″><Box size={40} className=”text-blue-500″/></div>
<div>
<p className=”text-xs font-bold text-blue-400 uppercase tracking-widest mb-1″>Projeto Completo</p>
<div className=”text-4xl font-black text-gray-800 tracking-tighter drop-shadow-sm mb-1″>
<span className=”text-xl text-gray-400 font-bold mr-1″>R$</span>
{formatCurrency(results.finalPrice).replace(‘R$’, ”)}
</div>
</div>
<div className=”mt-4 space-y-2″>
<div className=”flex justify-between items-center px-3 py-1.5 bg-emerald-50 rounded-lg border border-emerald-100/50″>
<span className=”text-[10px] font-bold text-emerald-600 uppercase”>Lucro</span>
<span className=”text-sm font-bold text-emerald-700″>{formatCurrency(results.actualProfit)}</span>
</div>
<div className=”flex justify-between items-center px-3 py-1.5 bg-gray-50 rounded-lg border border-gray-100″>
<span className=”text-[10px] font-bold text-gray-400 uppercase”>Custo</span>
<span className=”text-sm font-bold text-gray-600″>{formatCurrency(results.totalCost)}</span>
</div>
</div>
</div>
{/* 2. LADO DIREITO: UNITÁRIO (Se houver > 1 peça) */}
{results.P.qty > 1 ? (
<div className=”p-6 text-center bg-gradient-to-br from-white to-indigo-50/30 flex flex-col justify-between h-full relative overflow-hidden”>
<div className=”absolute top-0 right-0 p-2 opacity-10″><Coins size={40} className=”text-indigo-500″/></div>
<div>
<p className=”text-xs font-bold text-indigo-400 uppercase tracking-widest mb-1″>Por Unidade</p>
<div className=”text-4xl font-black text-indigo-700 tracking-tighter drop-shadow-sm mb-1″>
<span className=”text-xl text-indigo-300 font-bold mr-1″>R$</span>
{formatCurrency(results.unitPrice).replace(‘R$’, ”)}
</div>
</div>
<div className=”mt-4 space-y-2″>
<div className=”flex justify-between items-center px-3 py-1.5 bg-emerald-50 rounded-lg border border-emerald-100/50″>
<span className=”text-[10px] font-bold text-emerald-600 uppercase”>Lucro</span>
<span className=”text-sm font-bold text-emerald-700″>{formatCurrency(results.unitProfit)}</span>
</div>
<div className=”flex justify-between items-center px-3 py-1.5 bg-gray-50 rounded-lg border border-gray-100″>
<span className=”text-[10px] font-bold text-gray-400 uppercase”>Custo</span>
<span className=”text-sm font-bold text-gray-600″>{formatCurrency(results.unitTotalCost)}</span>
</div>
</div>
</div>
) : (
/* Se for só 1 peça, ocupa o espaço com info extra ou vazio */
<div className=”hidden md:flex p-6 items-center justify-center bg-gray-50/30″>
<div className=”text-center text-gray-400″>
<p className=”text-xs font-medium mb-2″>Peça única no lote.</p>
<button onClick={applyPsychologicalPrice} className=”text-[10px] font-bold text-indigo-600 bg-white border border-indigo-100 px-3 py-1.5 rounded-full hover:bg-indigo-50 hover:border-indigo-200 transition-all shadow-sm inline-flex items-center gap-1″>
<Zap size={10} className=”fill-indigo-600″/> Arredondar
</button>
</div>
</div>
)}
</div>
{/* Round Price Button (Mobile Only or when needed) */}
<div className=”bg-white border-t border-gray-100 p-2 flex justify-center md:hidden”>
<button onClick={applyPsychologicalPrice} className=”text-xs font-bold text-indigo-600 bg-indigo-50 px-4 py-1.5 rounded-full hover:bg-indigo-100 transition-colors flex items-center gap-1″>
<Zap size={12} className=”fill-indigo-600″/> Arredondar Preço
</button>
</div>
{/* Tabela de Detalhes Estilizada */}
<div className=”border-t border-gray-100 bg-white”>
<div className=”bg-gray-50/50 px-8 py-3 text-[10px] font-bold text-gray-400 uppercase flex justify-between tracking-wider”>
<span className=”w-1/3″>Composição</span>
<span className=”w-1/3 text-right”>Total</span>
<span className=”w-1/3 text-right”>Unitário</span>
</div>
<div className=”divide-y divide-gray-50 text-sm”>
{[
{ label: ‘Filamento’, val: results.costs.filament, color: ‘text-blue-600′, bg:’bg-blue-500’ },
{ label: ‘Energia’, val: results.costs.energy, color: ‘text-amber-600′, bg:’bg-amber-500’ },
{ label: ‘Desgaste’, val: results.costs.depreciation, color: ‘text-purple-600′, bg:’bg-purple-500’ },
{ label: ‘Risco’, val: results.costs.risk, color: ‘text-rose-500′, bg:’bg-rose-500’ },
{ label: ‘Extras’, val: results.costs.totalExtras, color: ‘text-gray-600′, bg:’bg-gray-400’ },
].map((row, idx) => (
<div key={idx} className=”px-8 py-3 flex justify-between hover:bg-gray-50 transition-colors group”>
<span className=”w-1/3 font-semibold text-gray-700 flex items-center gap-2″>
<span className={`w-2 h-2 rounded-full ${row.bg}`}></span> {row.label}
</span>
<span className=”w-1/3 text-right font-medium text-gray-900″>{formatCurrency(row.val)}</span>
<span className=”w-1/3 text-right text-gray-400 font-mono text-xs pt-0.5″>{formatCurrency(row.val / results.P.qty)}</span>
</div>
))}
<div className=”px-8 py-3 flex justify-between bg-gray-50/80 font-bold border-t border-gray-100″>
<span className=”w-1/3 text-gray-800 text-xs uppercase pt-0.5″>Custo Total</span>
<span className=”w-1/3 text-right text-gray-800″>{formatCurrency(results.totalCost)}</span>
<span className=”w-1/3 text-right text-gray-500 font-mono text-xs pt-0.5″>{formatCurrency(results.unitTotalCost)}</span>
</div>
<div className=”px-8 py-4 bg-emerald-50/30 flex justify-between items-center border-t border-emerald-100/50″>
<div className=”w-1/3″><span className=”block font-bold text-emerald-700 text-xs uppercase”>Lucro Líquido</span></div>
<span className=”w-1/3 text-right font-bold text-emerald-700 text-lg”>{formatCurrency(results.actualProfit)}</span>
<span className=”w-1/3 text-right font-bold text-emerald-600/70 font-mono text-sm”>{formatCurrency(results.actualProfit / results.P.qty)}</span>
</div>
</div>
</div>
{/* Actions Footer */}
<div className=”grid grid-cols-1 sm:grid-cols-4 border-t border-gray-100 divide-y sm:divide-y-0 sm:divide-x divide-gray-100 bg-gray-50/30″>
<button onClick={() => saveToHistory()} className=”py-4 text-xs font-bold text-gray-500 hover:bg-white hover:text-indigo-600 flex justify-center items-center gap-2 transition-all uppercase tracking-wide group”>
<Save size={18} className=”text-gray-400 group-hover:text-indigo-500 transition-colors” /> Salvar
</button>
<button onClick={exportToCSV} className=”py-4 text-xs font-bold text-gray-500 hover:bg-white hover:text-emerald-600 flex justify-center items-center gap-2 transition-all uppercase tracking-wide group”>
<FileSpreadsheet size={18} className=”text-gray-400 group-hover:text-emerald-500 transition-colors” /> CSV
</button>
{/* Botão Copy to Sheets */}
<button onClick={copyToSheets} className=”py-4 text-xs font-bold text-gray-500 hover:bg-white hover:text-blue-600 flex justify-center items-center gap-2 transition-all uppercase tracking-wide group”>
<Copy size={18} className=”text-gray-400 group-hover:text-blue-500 transition-colors” /> Sheets
</button>
<button onClick={openWhatsApp} className=”py-4 text-xs font-bold text-gray-500 hover:bg-white hover:text-green-600 flex justify-center items-center gap-2 transition-all uppercase tracking-wide group”>
<MessageCircle size={18} className=”text-gray-400 group-hover:text-green-500 transition-colors” /> Enviar
</button>
</div>
</Card>
</div>
{results.isLowProfit && (
<div className=”bg-red-50 border border-red-200 p-5 rounded-xl flex items-start gap-4 animate-fade-in shadow-sm”>
<div className=”bg-red-100 p-2 rounded-full”>
<TrendingUp className=”text-red-600 w-5 h-5″ />
</div>
<div className=”flex-1″>
<span className=”font-bold text-red-900 block text-sm mb-1 uppercase tracking-wide”>Lucro Insuficiente</span>
<p className=”text-xs text-red-700 mb-2″>
O lucro atual ({formatCurrency(results.actualProfit)}) está abaixo do mínimo configurado de {formatCurrency(results.S.minProfit)}.
</p>
<div className=”bg-white/60 p-2 rounded-lg inline-block border border-red-100″>
<p className=”text-xs font-bold text-red-800″>
Preço Sugerido: <span className=”text-sm”>{formatCurrency(results.suggestedPriceMinProfit)}</span>
</p>
</div>
</div>
</div>
)}
<Card className=”p-8″>
<div className=”flex justify-between items-center mb-6″>
<h3 className=”font-bold text-gray-700 text-xs uppercase tracking-widest flex items-center gap-2″>
<Info size={14}/> Raio-X Visual
</h3>
</div>
<div className=”space-y-4″>
<ProgressBar label=”Material” value={results.costs.filament} total={results.finalPrice} colorClass=”bg-blue-500″ />
<div className=”grid grid-cols-2 gap-8″>
<ProgressBar label=”Energia” value={results.costs.energy} total={results.finalPrice} colorClass=”bg-amber-400″ />
<ProgressBar label=”Desgaste” value={results.costs.depreciation} total={results.finalPrice} colorClass=”bg-purple-500″ />
</div>
{(results.costs.risk > 0) && <ProgressBar label=”Risco” value={results.costs.risk} total={results.finalPrice} colorClass=”bg-rose-400″ />}
{(results.costs.totalExtras > 0) && <ProgressBar label=”Extras” value={results.costs.totalExtras} total={results.finalPrice} colorClass=”bg-gray-400″ />}
<div className=”pt-2 border-t border-gray-100 mt-2″>
<ProgressBar label=”Lucro Líquido” value={results.actualProfit} total={results.finalPrice} colorClass=”bg-emerald-500″ />
</div>
</div>
</Card>
</>
) : (
<div className=”h-full flex flex-col items-center justify-center text-center p-12 border-2 border-dashed border-gray-200 rounded-3xl bg-gray-50/50 hover:bg-gray-50 transition-colors”>
<div className=”bg-white p-6 rounded-3xl shadow-sm mb-6 animate-float”>
<Calculator size={48} className=”text-indigo-200 stroke-1″ />
</div>
<h3 className=”text-xl font-bold text-gray-800 mb-2″>Vamos começar?</h3>
<p className=”text-gray-400 font-medium max-w-xs mx-auto mb-8″>
Preencha o <strong>peso</strong> e o <strong>tempo</strong> para gerar seu orçamento detalhado.
</p>
<div className=”flex gap-2 justify-center”>
<div className=”w-2 h-2 rounded-full bg-gray-300″></div>
<div className=”w-2 h-2 rounded-full bg-gray-300″></div>
<div className=”w-2 h-2 rounded-full bg-gray-300″></div>
</div>
</div>
)}
</div>
</div>
)}
</main>
</div>
);
}
Philipe Cardoso Com 33 anos de idade, sou um carioca apaixonado por tecnologia e fotografia. Além de ser o criador do Portal Zoom Digital, que preserva sua essência desde os tempos em que era um blog, também sou um verdadeiro entusiasta e amante de todas as formas de tecnologia. Através do Portal, compartilho minha paixão pela tecnologia e trago as últimas novidades e tendências para os leitores. Também sou fascinado pelo mundo da fotografia, explorando o poder das imagens para capturar momentos únicos e transmitir histórias cativantes.

Hospedagem de Sites como Negócio: Análise do Livro de…

Este artigo é uma análise do livro “Hospedagem de Sites: Como Empreender na Internet com um Negócio Escalável e Rentável”, escrito por Gustavo Gallas,...
Philipe Cardoso
3 min read

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *