Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| }; | |