VirtualLabo / src /components /lab /ChemicalCalculator.tsx
rinogeek's picture
Initial commit: Virtual Labo Chimique - Docker deployment
538d81e
import { useState } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Calculator, FlaskConical, Atom } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { substances, Substance } from "@/data/substances";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
// Atomic masses (g/mol)
const atomicMasses: Record<string, number> = {
H: 1.008, C: 12.011, N: 14.007, O: 15.999, S: 32.065,
Cl: 35.453, Na: 22.990, Ca: 40.078, Mg: 24.305, Fe: 55.845,
Cu: 63.546, Zn: 65.38, Ag: 107.868, Br: 79.904, I: 126.904,
};
export const ChemicalCalculator = () => {
const [selectedSubstance, setSelectedSubstance] = useState<string>("");
const [mass, setMass] = useState<string>("");
const [volume, setVolume] = useState<string>("");
const [concentration, setConcentration] = useState<string>("");
const [results, setResults] = useState<{
molarMass?: number;
moles?: number;
calculatedConcentration?: number;
calculatedMass?: number;
}>({});
// Calculate molar mass from formula
const calculateMolarMass = (formula: string): number => {
let molarMass = 0;
// Remove subscript numbers and parse formula
const cleanFormula = formula.replace(/[₀₁₂₃₄₅₆₇₈₉]/g, (match) => {
const subscripts = '₀₁₂₃₄₅₆₇₈₉';
return subscripts.indexOf(match).toString();
});
// Simple parser for basic formulas
const regex = /([A-Z][a-z]?)(\d*)/g;
let match;
while ((match = regex.exec(cleanFormula)) !== null) {
const element = match[1];
const count = match[2] ? parseInt(match[2]) : 1;
if (atomicMasses[element]) {
molarMass += atomicMasses[element] * count;
}
}
return Math.round(molarMass * 100) / 100;
};
const handleCalculate = () => {
const substance = substances.find(s => s.id === selectedSubstance);
if (!substance) return;
const molarMass = calculateMolarMass(substance.formula);
const newResults: typeof results = { molarMass };
// Calculate moles from mass
if (mass) {
const massNum = parseFloat(mass);
if (!isNaN(massNum)) {
newResults.moles = Math.round((massNum / molarMass) * 1000) / 1000;
}
}
// Calculate concentration from mass and volume
if (mass && volume) {
const massNum = parseFloat(mass);
const volumeNum = parseFloat(volume);
if (!isNaN(massNum) && !isNaN(volumeNum) && volumeNum > 0) {
const moles = massNum / molarMass;
newResults.calculatedConcentration = Math.round((moles / (volumeNum / 1000)) * 1000) / 1000;
}
}
// Calculate mass from concentration and volume
if (concentration && volume) {
const concNum = parseFloat(concentration);
const volumeNum = parseFloat(volume);
if (!isNaN(concNum) && !isNaN(volumeNum)) {
const moles = concNum * (volumeNum / 1000);
newResults.calculatedMass = Math.round((moles * molarMass) * 100) / 100;
}
}
setResults(newResults);
};
const handleReset = () => {
setMass("");
setVolume("");
setConcentration("");
setResults({});
};
const selectedSubstanceData = substances.find(s => s.id === selectedSubstance);
return (
<Card className="p-4 space-y-4">
<div className="flex items-center gap-2">
<Calculator className="w-5 h-5 text-primary" />
<h3 className="font-semibold text-lg">Calculateur Chimique</h3>
</div>
<div className="space-y-4">
{/* Substance Selection */}
<div className="space-y-2">
<Label htmlFor="substance">Substance</Label>
<Select value={selectedSubstance} onValueChange={setSelectedSubstance}>
<SelectTrigger id="substance">
<SelectValue placeholder="Sélectionner une substance" />
</SelectTrigger>
<SelectContent>
{substances.map((substance) => (
<SelectItem key={substance.id} value={substance.id}>
{substance.name} ({substance.formula})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedSubstanceData && (
<div className="flex gap-2 flex-wrap">
<Badge variant="outline">
<Atom className="w-3 h-3 mr-1" />
{selectedSubstanceData.formula}
</Badge>
<Badge variant="outline">
{selectedSubstanceData.state}
</Badge>
<Badge variant={selectedSubstanceData.hazard === 'high' ? 'destructive' : 'secondary'}>
{selectedSubstanceData.hazard === 'high' ? 'Dangereux' :
selectedSubstanceData.hazard === 'medium' ? 'Attention' : 'Sûr'}
</Badge>
</div>
)}
<Separator />
{/* Input Fields */}
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="mass">Masse (g)</Label>
<Input
id="mass"
type="number"
step="0.01"
value={mass}
onChange={(e) => setMass(e.target.value)}
placeholder="Ex: 10.5"
/>
</div>
<div className="space-y-2">
<Label htmlFor="volume">Volume (mL)</Label>
<Input
id="volume"
type="number"
step="0.1"
value={volume}
onChange={(e) => setVolume(e.target.value)}
placeholder="Ex: 100"
/>
</div>
<div className="space-y-2">
<Label htmlFor="concentration">Concentration (mol/L)</Label>
<Input
id="concentration"
type="number"
step="0.01"
value={concentration}
onChange={(e) => setConcentration(e.target.value)}
placeholder="Ex: 0.5"
/>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
<Button onClick={handleCalculate} disabled={!selectedSubstance}>
<FlaskConical className="w-4 h-4 mr-2" />
Calculer
</Button>
<Button onClick={handleReset} variant="outline">
Réinitialiser
</Button>
</div>
{/* Results */}
{results.molarMass && (
<>
<Separator />
<div className="space-y-3 bg-muted/50 p-4 rounded-lg">
<h4 className="font-semibold text-sm">Résultats</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Masse molaire:</span>
<span className="font-medium">{results.molarMass} g/mol</span>
</div>
{results.moles !== undefined && (
<div className="flex justify-between">
<span className="text-muted-foreground">Quantité de matière:</span>
<span className="font-medium">{results.moles} mol</span>
</div>
)}
{results.calculatedConcentration !== undefined && (
<div className="flex justify-between">
<span className="text-muted-foreground">Concentration calculée:</span>
<span className="font-medium">{results.calculatedConcentration} mol/L</span>
</div>
)}
{results.calculatedMass !== undefined && (
<div className="flex justify-between">
<span className="text-muted-foreground">Masse calculée:</span>
<span className="font-medium">{results.calculatedMass} g</span>
</div>
)}
</div>
{/* Stoichiometry hint */}
{results.moles && (
<div className="text-xs text-muted-foreground pt-2 border-t">
💡 Astuce: Pour une réaction 1:1, vous auriez besoin de {results.moles} mol du réactif complémentaire.
</div>
)}
</div>
</>
)}
</div>
</Card>
);
};