/** * Student Election Portal - ReactJS Application * Features: * - 3 Languages: Marathi (मराठी), Hindi (हिंदी), English * - Location Dropdowns: Strictly 4 Districts (Chhatrapati Sambhajinagar, Beed, Dharashiv, Jalna) * - Device Camera Capture for: * 1) Passport Size Photo (Front/Selfie or Back Camera) * 2) Degree Photo (Camera) * 3) Marksheet Photos (Multiple Camera Captures) * 4) Aadhar Photo (Camera) * - Official Helpline Number: 9561131729 + Auto-Call Dial CTA (`tel:9561131729`) */ const { useState, useEffect, useRef, useMemo } = React; const HELPLINE_NUMBER = "9561131729"; /* ========================================================================== MULTI-LANGUAGE TRANSLATION DICTIONARY (Marathi, Hindi, English) ========================================================================== */ const TRANSLATIONS = { mr: { langName: "मराठी", portalTitle: "विद्यार्थी निवडणूक पोर्टल", userFormTab: "नोंदणी फॉर्म", adminPanelTab: "ॲडमिन पॅनेल", bannerTitle: "विद्यार्थी निवडणूक", bannerSubtitle: "तुमचा आवाज, आपले भविष्य", voteBtnText: "VOTE", candidateName: "उमेदवाराचे नाव", permanentAddress: "कायमचा पत्ता", collageName: "कॉलेजचे नाव", mobileNumber: "मोबाईल नंबर", degreeName: "पदवीचे नाव (Degree)", passportPhoto: "पासपोर्ट साईज फोटो (कॅमेऱ्याने काढा)", capturePassportPhoto: "कॅमेऱ्याने पासपोर्ट फोटो काढा", retakePhoto: "पुन्हा कॅमेऱ्याने फोटो काढा", degreePhoto: "पदवी फोटो (Degree Photo - कॅमेरा)", captureDegreePhoto: "कॅमेऱ्याने पदवी फोटो काढा", marksheetPhotos: "गुणपत्रिका फोटो (Marksheet Photos - कॅमेऱ्याने एकापेक्षा जास्त)", captureMarksheetPhotos: "कॅमेऱ्याने गुणपत्रिका फोटो काढा", marksheetSubHint: "एकापेक्षा जास्त गुणपत्रिका फोटो कॅमेऱ्याने काढू शकता", addMoreMarksheetCamera: "📷 + आणखी फोटो काढा", filesSelected: "फोटो काढले", aadharPhoto: "आधार कार्ड फोटो (कॅमेरा)", captureAadharPhoto: "कॅमेऱ्याने आधार फोटो काढा", openCameraBtn: "📷 कॅमेरा सुरू करा", deviceCameraBtn: "📱 मोबाईल कॅमेरा", captureShotBtn: "📸 फोटो काढा (Capture)", switchCameraBtn: "🔄 कॅमेरा बदला", zilla: "जिल्हा (Zilla)", dist: "डिस्ट्रिक्ट (Dist)", taluka: "तालुका (Taluka)", khanda: "खंड (Khanda)", submitBtn: "सबमिट करा (Submit)", submittingBtn: "सबमिट होत आहे...", helplineTitle: "मदत क्रमांक (Helpline Number)", callNowCta: "आत्ताच कॉल करा", autoDialCta: "कॉल करा: 9561131729", allFieldsCompulsoryErr: "सर्व माहिती आणि सर्व ४ प्रकारचे फोटो (पासपोर्ट, पदवी, गुणपत्रिका, आधार) कॅमेऱ्याने काढणे अनिवार्य आहे.", invalidMobileErr: "कृपया १० अंकी वैध मोबाईल नंबर प्रविष्ट करा.", registrationSuccessMsg: "उमेदवाराची नोंदणी यशस्वीरित्या पूर्ण झाली!", adminLoginTitle: "ॲडमिन लॉगिन", enterOtpSubtitle: "पुढे जाण्यासाठी ६ अंकी OTP प्रविष्ट करा", verifyOtpBtn: "OTP तपासा (Verify OTP)", verifyingOtpBtn: "तपासत आहे...", resendOtpBtn: "OTP पुन्हा पाठवा (Resend OTP)", demoPasscodeLabel: "डेमो OTP:", autoFillBtn: "ऑटो-फिल", enterAll6DigitsErr: "कृपया संपूर्ण ६ अंकी OTP प्रविष्ट करा.", candidateListTitle: "उमेदवारांची यादी", usersRegisteredLabel: "नोंदणीकृत उमेदवार", searchCandidatePlaceholder: "उमेदवार शोधा (नाव, मोबाईल, कॉलेज)...", colCandidate: "उमेदवार", colMobile: "मोबाईल", colAddressCollage: "पत्ता, कॉलेज", loadingCandidates: "उमेदवारांची यादी लोड होत आहे...", noCandidatesFound: "कोणताही उमेदवार सापडला नाही.", candidateDetailsTitle: "उमेदवाराची संपूर्ण माहिती", closeBtn: "बंद करा ✕" }, hi: { langName: "हिंदी", portalTitle: "छात्र चुनाव पोर्टल", userFormTab: "पंजीकरण फॉर्म", adminPanelTab: "एडमिन पैनल", bannerTitle: "छात्र चुनाव", bannerSubtitle: "आपकी आवाज़, हमारा भविष्य", voteBtnText: "VOTE", candidateName: "उम्मीदवार का नाम", permanentAddress: "स्थायी पता", collageName: "कॉलेज का नाम", mobileNumber: "मोबाइल नंबर", degreeName: "डिग्री का नाम", passportPhoto: "पासपोर्ट साइज फोटो (कैमरे से खींचें)", capturePassportPhoto: "कैमरे से पासपोर्ट फोटो खींचें", retakePhoto: "दोबारा कैमरे से फोटो खींचें", degreePhoto: "डिग्री फोटो (Degree Photo - कैमरा)", captureDegreePhoto: "कैमरे से डिग्री फोटो खींचें", marksheetPhotos: "मार्कशीट फोटो (Marksheet Photos - कैमरे से एक से अधिक)", captureMarksheetPhotos: "कैमरे से मार्कशीट फोटो खींचें", marksheetSubHint: "आप कैमरे से एक से अधिक मार्कशीट फोटो खींच सकते हैं", addMoreMarksheetCamera: "📷 + और फोटो खींचें", filesSelected: "फोटो खींचे गए", aadharPhoto: "आधार कार्ड फोटो (कैमरा)", captureAadharPhoto: "कैमरे से आधार फोटो खींचें", openCameraBtn: "📷 कैमरा खोलें", deviceCameraBtn: "📱 मोबाइल कैमरा", captureShotBtn: "📸 फोटो खींचें (Capture)", switchCameraBtn: "🔄 कैमरा बदलें", zilla: "ज़िला (Zilla)", dist: "डिस्ट्रिक्ट (Dist)", taluka: "तालुका (Taluka)", khanda: "खंड (Khanda)", submitBtn: "सबमिट करें (Submit)", submittingBtn: "सबमिट हो रहा है...", helplineTitle: "हेल्पलाइन नंबर (Helpline)", callNowCta: "अभी कॉल करें", autoDialCta: "कॉल करें: 9561131729", allFieldsCompulsoryErr: "सभी फ़ील्ड और चारों फोटो (पासपोर्ट, डिग्री, मार्कशीट, आधार) कैमरे से खींचना अनिवार्य है।", invalidMobileErr: "कृपया 10 अंकों का मान्य मोबाइल नंबर दर्ज करें।", registrationSuccessMsg: "उम्मीदवार का पंजीकरण सफलतापूर्वक हो गया!", adminLoginTitle: "एडमिन लॉगिन", enterOtpSubtitle: "आगे बढ़ने के लिए OTP दर्ज करें", verifyOtpBtn: "OTP सत्यापित करें (Verify OTP)", verifyingOtpBtn: "सत्यापित हो रहा है...", resendOtpBtn: "OTP पुनः भेजें (Resend OTP)", demoPasscodeLabel: "डेमो पासकोड:", autoFillBtn: "ऑटो-फिल", enterAll6DigitsErr: "कृपया OTP के सभी 6 अंक दर्ज करें।", candidateListTitle: "उम्मीदवार सूची", usersRegisteredLabel: "पंजीकृत उम्मीदवार", searchCandidatePlaceholder: "उम्मीदवार खोजें...", colCandidate: "उम्मीदवार", colMobile: "मोबाइल", colAddressCollage: "पता, कॉलेज", loadingCandidates: "उम्मीदवार लोड हो रहे हैं...", noCandidatesFound: "कोई उम्मीदवार नहीं मिला।", candidateDetailsTitle: "उम्मीदवार का विवरण", closeBtn: "बंद करें ✕" }, en: { langName: "English", portalTitle: "Student Election Portal", userFormTab: "User Form", adminPanelTab: "Admin Panel", bannerTitle: "Student Election", bannerSubtitle: "Your Voice, Our Future", voteBtnText: "VOTE", candidateName: "Candidate Name", permanentAddress: "Permanent Address", collageName: "Collage Name", mobileNumber: "Mobile Number", degreeName: "Degree Name", passportPhoto: "Passport Size Photo (Camera)", capturePassportPhoto: "Capture Passport Size Photo by Camera", retakePhoto: "Tap to Retake Photo with Camera", degreePhoto: "Degree Photo (Camera)", captureDegreePhoto: "Capture Degree Photo by Camera", marksheetPhotos: "Marksheet Photos (Multiple Camera Captures)", captureMarksheetPhotos: "Capture Marksheet Photo by Camera", marksheetSubHint: "Capture one or multiple semester marksheet photos using camera", addMoreMarksheetCamera: "📷 + Capture More", filesSelected: "photos captured", aadharPhoto: "Aadhar Photo (Camera)", captureAadharPhoto: "Capture Aadhar Photo by Camera", openCameraBtn: "📷 Open Camera", deviceCameraBtn: "📱 Device Camera", captureShotBtn: "📸 Capture Photo", switchCameraBtn: "🔄 Flip Camera", zilla: "Zilla", dist: "Dist", taluka: "Taluka", khanda: "Khanda", submitBtn: "Submit", submittingBtn: "Submitting...", helplineTitle: "Helpline Number", callNowCta: "Call Helpline", autoDialCta: "Auto Call: 9561131729", allFieldsCompulsoryErr: "All fields and all 4 camera photos (Passport, Degree, Marksheet, Aadhar) are compulsory.", invalidMobileErr: "Please enter a valid 10-digit Mobile Number.", registrationSuccessMsg: "Registration submitted successfully!", adminLoginTitle: "Admin Login", enterOtpSubtitle: "Enter OTP to continue", verifyOtpBtn: "Verify OTP", verifyingOtpBtn: "Verifying...", resendOtpBtn: "Resend OTP", demoPasscodeLabel: "Demo Passcode:", autoFillBtn: "Auto-fill", enterAll6DigitsErr: "Please enter all 6 digits of the OTP.", candidateListTitle: "Candidate List", usersRegisteredLabel: "Users Registered", searchCandidatePlaceholder: "Search Candidate...", colCandidate: "Candidate", colMobile: "Mobile", colAddressCollage: "Address, Collage", loadingCandidates: "Loading candidates...", noCandidatesFound: "No candidates found matching your search.", candidateDetailsTitle: "Candidate Details", closeBtn: "Close ✕" } }; /* ========================================================================== SVG ICONS & ILLUSTRATIONS (Matching Screenshot Design) ========================================================================== */ const ElectionBannerIllustration = ({ t }) => ( {t.bannerTitle} {t.bannerSubtitle} {t.voteBtnText} ); const CandidateAvatarIllustration = () => ( ); const ShieldLockIllustration = () => ( ); const UsersRegisteredIcon = () => ( ); /* Small Field Icons */ const Icons = { user: ( ), home: ( ), college: ( ), phone: ( ), degree: ( ), pin: ( ), camera: ( ), chevronDown: ( ), search: ( ), menu: ( ), arrowLeft: ( ), logout: ( ) }; /* ========================================================================== LIVE DEVICE CAMERA CAPTURE MODAL (WebRTC getUserMedia + Native Fallback) ========================================================================== */ function LiveCameraModal({ title, defaultFacingMode = "environment", isPassport = false, onCaptureFile, onClose, t }) { const videoRef = useRef(null); const streamRef = useRef(null); const [facingMode, setFacingMode] = useState(defaultFacingMode); const [camError, setCamError] = useState(""); const stopStream = () => { if (streamRef.current) { streamRef.current.getTracks().forEach((tr) => tr.stop()); streamRef.current = null; } }; useEffect(() => { let active = true; const startCamera = async () => { stopStream(); setCamError(""); try { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error("Camera API not supported in this browser."); } const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: facingMode }, width: { ideal: 1280 }, height: { ideal: 960 } }, audio: false }); if (!active) { stream.getTracks().forEach((tr) => tr.stop()); return; } streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; } } catch (err) { setCamError(err.message || "Unable to access device camera."); } }; startCamera(); return () => { active = false; stopStream(); }; }, [facingMode]); const handleShutterClick = () => { const video = videoRef.current; if (!video || !video.videoWidth) return; const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL("image/jpeg", 0.9); canvas.toBlob( (blob) => { if (!blob) return; const fileName = `camera_${Date.now()}.jpg`; const fileObj = new File([blob], fileName, { type: "image/jpeg" }); stopStream(); onCaptureFile(fileObj, dataUrl); onClose(); }, "image/jpeg", 0.9 ); }; const toggleFacingMode = () => { setFacingMode((prev) => (prev === "user" ? "environment" : "user")); }; return (
e.stopPropagation()}>
📷 {title}
{camError ? (
{camError}
) : ( <>
); } /* ========================================================================== REUSABLE INPUT & SELECT COMPONENTS WITH RED ASTERISK PLACEHOLDER ========================================================================== */ function CompulsoryTextInput({ icon, label, value, onChange, type = "text", maxLength }) { const [focused, setFocused] = useState(false); const showOverlayPlaceholder = !value && !focused; return (
{icon}
{showOverlayPlaceholder && (
{label} *
)} setFocused(true)} onBlur={() => setFocused(false)} onChange={(e) => onChange(e.target.value)} required />
); } function CompulsorySelectDropdown({ icon, label, value, options, onChange }) { return (
{icon}
{!value && (
{label} *
)} {Icons.chevronDown}
); } /* ========================================================================== 1. USER SCREEN: CANDIDATE REGISTRATION FORM ========================================================================== */ function UserRegistrationScreen({ locationsData, t }) { const [candidateName, setCandidateName] = useState(""); const [permanentAddress, setPermanentAddress] = useState(""); const [collageName, setCollageName] = useState(""); const [mobileNumber, setMobileNumber] = useState(""); const [degreeName, setDegreeName] = useState(""); // 1. Passport Size Photo (Captured by Camera) const [passportPhoto, setPassportPhoto] = useState(null); const [passportPreview, setPassportPreview] = useState(""); // 2. Degree Photo (Captured by Camera) const [degreePhoto, setDegreePhoto] = useState(null); const [degreePreview, setDegreePreview] = useState(""); // 3. Multiple Marksheet Photos (Captured by Camera) const [marksheetItems, setMarksheetItems] = useState([]); // 4. Aadhar Photo (Captured by Camera) const [aadharPhoto, setAadharPhoto] = useState(null); const [aadharPreview, setAadharPreview] = useState(""); // Location fields (Strictly Chhatrapati Sambhajinagar, Beed, Dharashiv, Jalna) const [zilla, setZilla] = useState(""); const [dist, setDist] = useState(""); const [taluka, setTaluka] = useState(""); const [khanda, setKhanda] = useState(""); const [submitting, setSubmitting] = useState(false); const [statusMessage, setStatusMessage] = useState(null); // Active Live Camera Modal state: null or { target: 'passport' | 'degree' | 'marksheet' | 'aadhar', title, facingMode, isPassport } const [activeCameraModal, setActiveCameraModal] = useState(null); // Native mobile camera input refs (`capture="user"` / `capture="environment"`) const passportCameraInputRef = useRef(null); const degreeCameraInputRef = useRef(null); const marksheetCameraInputRef = useRef(null); const aadharCameraInputRef = useRef(null); // Strictly 4 Districts: Chhatrapati Sambhajinagar, Beed, Dharashiv, Jalna const default4Districts = ["Chhatrapati Sambhajinagar", "Beed", "Dharashiv", "Jalna"]; const zillaOptions = useMemo(() => { if (!locationsData?.districts) return default4Districts; return [...new Set(locationsData.districts.map((d) => d.zilla))]; }, [locationsData]); const distOptions = useMemo(() => { if (!locationsData?.districts) return zilla ? [zilla] : default4Districts; const matched = locationsData.districts.filter((d) => !zilla || d.zilla === zilla); const list = matched.map((d) => d.dist); return list.length > 0 ? [...new Set(list)] : default4Districts; }, [locationsData, zilla]); const talukaOptions = useMemo(() => { if (!locationsData?.districts) return []; const matchedDistrict = locationsData.districts.find( (d) => d.zilla === zilla || d.dist === dist ); if (matchedDistrict && matchedDistrict.talukas) { return matchedDistrict.talukas.map((tk) => tk.name); } const allTalukas = []; locationsData.districts.forEach((d) => { d.talukas?.forEach((tk) => allTalukas.push(tk.name)); }); return [...new Set(allTalukas)]; }, [locationsData, zilla, dist]); const khandaOptions = useMemo(() => { if (!locationsData?.districts) return []; for (const d of locationsData.districts) { const foundTaluka = d.talukas?.find((tk) => tk.name === taluka); if (foundTaluka && foundTaluka.khandas) { return foundTaluka.khandas; } } if (zilla) { const distObj = locationsData.districts.find((d) => d.zilla === zilla); if (distObj) { const list = []; distObj.talukas?.forEach((tk) => tk.khandas?.forEach((k) => list.push(k))); return [...new Set(list)]; } } const allKhandas = []; locationsData.districts.forEach((d) => { d.talukas?.forEach((tk) => { tk.khandas?.forEach((k) => allKhandas.push(k)); }); }); return [...new Set(allKhandas)]; }, [locationsData, zilla, taluka]); const handleZillaChange = (val) => { setZilla(val); const found = locationsData?.districts?.find((d) => d.zilla === val); if (found) { setDist(found.dist); setTaluka(""); setKhanda(""); } }; const handleDistChange = (val) => { setDist(val); const found = locationsData?.districts?.find((d) => d.dist === val); if (found) { setZilla(found.zilla); setTaluka(""); setKhanda(""); } }; const handleTalukaChange = (val) => { setTaluka(val); setKhanda(""); }; // Handle single file from either Live Camera Modal or Native Camera Input const applyCapturedSinglePhoto = (file, dataUrl, target) => { if (target === "passport") { setPassportPhoto(file); setPassportPreview(dataUrl); } else if (target === "degree") { setDegreePhoto(file); setDegreePreview(dataUrl); } else if (target === "aadhar") { setAadharPhoto(file); setAadharPreview(dataUrl); } else if (target === "marksheet") { setMarksheetItems((prev) => [ ...prev, { id: `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, file, preview: dataUrl } ]); } }; const handleNativeCameraInput = (fileList, target) => { if (!fileList || fileList.length === 0) return; Array.from(fileList).forEach((file) => { const reader = new FileReader(); reader.onloadend = () => { applyCapturedSinglePhoto(file, reader.result, target); }; reader.readAsDataURL(file); }); }; const openLiveCamera = (target, title, facingMode = "environment", isPassport = false) => { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { setActiveCameraModal({ target, title, facingMode, isPassport }); } else { // Fallback to native mobile camera input if (target === "passport") passportCameraInputRef.current?.click(); if (target === "degree") degreeCameraInputRef.current?.click(); if (target === "marksheet") marksheetCameraInputRef.current?.click(); if (target === "aadhar") aadharCameraInputRef.current?.click(); } }; const removeMarksheetItem = (idToRemove, e) => { e.stopPropagation(); setMarksheetItems((prev) => prev.filter((item) => item.id !== idToRemove)); }; const handleSubmit = async (e) => { e.preventDefault(); setStatusMessage(null); if ( !candidateName.trim() || !permanentAddress.trim() || !collageName.trim() || !mobileNumber.trim() || !degreeName.trim() || !passportPhoto || !degreePhoto || marksheetItems.length === 0 || !aadharPhoto || !zilla.trim() || !dist.trim() || !taluka.trim() || !khanda.trim() ) { setStatusMessage({ type: "error", text: t.allFieldsCompulsoryErr }); return; } const digits = mobileNumber.replace(/\D/g, ""); if (digits.length < 10) { setStatusMessage({ type: "error", text: t.invalidMobileErr }); return; } try { setSubmitting(true); const formData = new FormData(); formData.append("candidate_name", candidateName.trim()); formData.append("permanent_address", permanentAddress.trim()); formData.append("collage_name", collageName.trim()); formData.append("mobile_number", digits); formData.append("degree_name", degreeName.trim()); formData.append("passport_photo", passportPhoto); formData.append("degree_photo", degreePhoto); marksheetItems.forEach((item) => { formData.append("marksheet_photos[]", item.file); }); formData.append("aadhar_photo", aadharPhoto); formData.append("zilla", zilla.trim()); formData.append("dist", dist.trim()); formData.append("taluka", taluka.trim()); formData.append("khanda", khanda.trim()); const response = await fetch("api/register.php", { method: "POST", body: formData }); const result = await response.json(); if (!response.ok || !result.success) { throw new Error(result.message || "Failed to submit registration."); } setStatusMessage({ type: "success", text: t.registrationSuccessMsg }); // Reset form fields setCandidateName(""); setPermanentAddress(""); setCollageName(""); setMobileNumber(""); setDegreeName(""); setPassportPhoto(null); setPassportPreview(""); setDegreePhoto(null); setDegreePreview(""); setMarksheetItems([]); setAadharPhoto(null); setAadharPreview(""); setZilla(""); setDist(""); setTaluka(""); setKhanda(""); } catch (err) { setStatusMessage({ type: "error", text: err.message || "An error occurred while saving registration." }); } finally { setSubmitting(false); } }; return (
{/* Election Banner */}
{/* Registration Form Card */}
{statusMessage && (
{statusMessage.text}
)}
setMobileNumber(val.replace(/[^\d+]/g, ""))} /> {/* 1. Passport Size Photo Capture by Camera */}
{ handleNativeCameraInput(e.target.files, "passport"); e.target.value = ""; }} />
openLiveCamera("passport", t.passportPhoto, "user", true)} > {passportPhoto ? (
Passport Preview
{passportPhoto.name}
{t.retakePhoto}
) : ( <>
{Icons.camera}
{t.capturePassportPhoto}
e.stopPropagation()}>
)}
{/* 2. Degree Photo Capture by Camera */}
{ handleNativeCameraInput(e.target.files, "degree"); e.target.value = ""; }} />
openLiveCamera("degree", t.degreePhoto, "environment", false)} > {degreePhoto ? (
Degree Preview
{degreePhoto.name}
{t.retakePhoto}
) : ( <>
{Icons.camera}
{t.captureDegreePhoto}
e.stopPropagation()}>
)}
{/* 3. Multiple Marksheet Photos Capture by Camera */}
{marksheetItems.length > 0 && ( {marksheetItems.length} {t.filesSelected} )}
{ handleNativeCameraInput(e.target.files, "marksheet"); e.target.value = ""; }} />
0 ? "has-file" : ""}`} onClick={() => openLiveCamera("marksheet", t.marksheetPhotos, "environment", false)} > {marksheetItems.length === 0 ? ( <>
{Icons.camera}
{t.captureMarksheetPhotos}
{t.marksheetSubHint}
e.stopPropagation()}>
) : (
e.stopPropagation()}> {marksheetItems.map((item, idx) => (
{`Marksheet
))}
openLiveCamera("marksheet", t.marksheetPhotos, "environment", false)} > {t.addMoreMarksheetCamera}
)}
{/* 4. Aadhar Photo Capture by Camera */}
{ handleNativeCameraInput(e.target.files, "aadhar"); e.target.value = ""; }} />
openLiveCamera("aadhar", t.aadharPhoto, "environment", false)} > {aadharPhoto ? (
Aadhar Preview
{aadharPhoto.name}
{t.retakePhoto}
) : ( <>
{Icons.camera}
{t.captureAadharPhoto}
e.stopPropagation()}>
)}
{/* Zilla Dropdown (Strictly Chhatrapati Sambhajinagar, Beed, Dharashiv, Jalna) */} {/* Dist Dropdown */} {/* Taluka Dropdown */} {/* Khanda Dropdown */} {/* Dedicated Helpline Card + Auto-Call Dial CTA */}
{t.helplineTitle} 📞 {HELPLINE_NUMBER}
{Icons.phone} {t.callNowCta}
{/* Live Camera Capture Modal */} {activeCameraModal && ( applyCapturedSinglePhoto(file, dataUrl, activeCameraModal.target) } onClose={() => setActiveCameraModal(null)} t={t} /> )}
); } /* ========================================================================== 2. ADMIN SCREEN: OTP LOGIN ========================================================================== */ function AdminOtpLoginScreen({ onLoginSuccess, t }) { const [digits, setDigits] = useState(["", "", "", "", "", ""]); const [loading, setLoading] = useState(false); const [feedback, setFeedback] = useState(null); const inputsRef = useRef([]); const handleDigitChange = (index, val) => { const cleanChar = val.replace(/\D/g, "").slice(-1); const next = [...digits]; next[index] = cleanChar; setDigits(next); if (cleanChar && index < 5) { inputsRef.current[index + 1]?.focus(); } }; const handleKeyDown = (index, e) => { if (e.key === "Backspace" && !digits[index] && index > 0) { inputsRef.current[index - 1]?.focus(); } }; const handlePaste = (e) => { e.preventDefault(); const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6); if (!pasted) return; const next = ["", "", "", "", "", ""]; for (let i = 0; i < pasted.length; i++) { next[i] = pasted[i]; } setDigits(next); const focusIdx = Math.min(pasted.length, 5); inputsRef.current[focusIdx]?.focus(); }; const verifyOtpCode = async (e) => { if (e) e.preventDefault(); setFeedback(null); const otpString = digits.join(""); if (otpString.length !== 6) { setFeedback({ type: "error", text: t.enterAll6DigitsErr }); return; } try { setLoading(true); const res = await fetch("api/admin_login.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "verify", otp: otpString }) }); const data = await res.json(); if (!res.ok || !data.success) { throw new Error(data.message || "Invalid OTP."); } onLoginSuccess(data.token); } catch (err) { setFeedback({ type: "error", text: err.message }); } finally { setLoading(false); } }; const handleResendOtp = async () => { setFeedback(null); try { const res = await fetch("api/admin_login.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "resend" }) }); const data = await res.json(); setFeedback({ type: "success", text: data.message || "OTP: 123456" }); } catch { setFeedback({ type: "success", text: "Default Admin OTP: 123456" }); } }; const fillDemoOtp = () => { setDigits(["1", "2", "3", "4", "5", "6"]); setFeedback(null); }; return (

{t.adminLoginTitle}

{t.enterOtpSubtitle}

{feedback && (
{feedback.text}
)}
{digits.map((digit, idx) => ( (inputsRef.current[idx] = el)} type="text" inputMode="numeric" maxLength={1} className="otp-digit-input" value={digit} onChange={(e) => handleDigitChange(idx, e.target.value)} onKeyDown={(e) => handleKeyDown(idx, e)} aria-label={`OTP digit ${idx + 1}`} /> ))}
{t.demoPasscodeLabel} 123456
); } /* ========================================================================== 3. ADMIN SCREEN: CANDIDATE LIST (Stats Card + Search + 3-Column Table) ========================================================================== */ function AdminCandidateListScreen({ adminToken, onSelectCandidate, onLogout, t }) { const [candidates, setCandidates] = useState([]); const [totalRegistered, setTotalRegistered] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchCandidates = async (searchVal = "") => { try { setLoading(true); setError(null); const url = `api/candidates.php?search=${encodeURIComponent(searchVal)}`; const res = await fetch(url, { headers: { "X-Admin-Token": adminToken } }); const data = await res.json(); if (res.status === 401 || data.unauthorized) { onLogout(); return; } if (!res.ok || !data.success) { throw new Error(data.message || "Failed to load candidates."); } setCandidates(data.candidates || []); setTotalRegistered(data.total_registered ?? 0); } catch (err) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { const timer = setTimeout(() => { fetchCandidates(searchQuery); }, 180); return () => clearTimeout(timer); }, [searchQuery]); const formatAddressAndCollage = (c) => { const shortAddr = (c.permanent_address || "").split(",")[0].trim() || c.zilla || ""; if (shortAddr && c.collage_name) { return `${shortAddr}, ${c.collage_name}`; } return `${c.permanent_address || ""} ${c.collage_name || ""}`.trim(); }; return (
{t.candidateListTitle}
{totalRegistered}
{t.usersRegisteredLabel}
{Icons.search} setSearchQuery(e.target.value)} />
{error && (
{error}
)}
{loading ? ( ) : candidates.length === 0 ? ( ) : ( candidates.map((cand) => ( onSelectCandidate(cand)}> )) )}
{t.colCandidate} {t.colMobile} {t.colAddressCollage}
{t.loadingCandidates}
{t.noCandidatesFound}
{cand.candidate_name} {cand.mobile_number} {formatAddressAndCollage(cand)}
); } /* ========================================================================== 4. ADMIN SCREEN: CANDIDATE DETAILS ========================================================================== */ function AdminCandidateDetailsScreen({ candidate, onBack, t }) { const [previewModal, setPreviewModal] = useState(null); if (!candidate) return null; // Parse multiple marksheet photos JSON array safely let marksheetList = []; if (candidate.marksheet_photos) { try { const parsed = JSON.parse(candidate.marksheet_photos); if (Array.isArray(parsed)) { marksheetList = parsed; } else if (typeof parsed === "string" && parsed) { marksheetList = [parsed]; } } catch { marksheetList = [candidate.marksheet_photos]; } } const passportPhotoUrl = candidate.passport_photo || "uploads/sample_passport.svg"; return (
{t.candidateDetailsTitle}
{/* Top Row: Passport Photo (Clickable) + Name, Mobile, Address, Collage */}
setPreviewModal({ title: `${candidate.candidate_name} - ${t.passportPhoto}`, src: passportPhotoUrl }) } > {candidate.candidate_name} { e.target.src = "uploads/sample_passport.svg"; }} />

{candidate.candidate_name}

{Icons.home} {candidate.permanent_address}
{Icons.college} {candidate.collage_name}
{/* Bottom Section: Degree + Photos on Left, Zilla/Dist/Taluka/Khanda on Right */}
{Icons.degree} {candidate.degree_name}
Degree Photo
setPreviewModal({ title: `${candidate.candidate_name} - ${t.degreePhoto}`, src: candidate.degree_photo }) } > Degree Photo { e.target.src = "uploads/sample_degree.svg"; }} />
Aadhar Photo
setPreviewModal({ title: `${candidate.candidate_name} - ${t.aadharPhoto}`, src: candidate.aadhar_photo }) } > Aadhar Photo { e.target.src = "uploads/sample_aadhar.svg"; }} />
{/* Multiple Marksheet Photos Gallery */} {marksheetList.length > 0 && (
Marksheet Photos ({marksheetList.length})
{marksheetList.map((imgUrl, idx) => (
setPreviewModal({ title: `${candidate.candidate_name} - Marksheet #${idx + 1}`, src: imgUrl }) } > {`Marksheet { e.target.src = "uploads/sample_marksheet.svg"; }} />
))}
)}
{Icons.pin} Zilla : {candidate.zilla}
{Icons.pin} Dist : {candidate.dist}
{Icons.pin} Taluka : {candidate.taluka}
{Icons.pin} Khanda : {candidate.khanda}
{/* Fullscreen Lightbox when clicking Passport / Degree / Marksheet / Aadhar Photo */} {previewModal && (
setPreviewModal(null)}>
e.stopPropagation()}>
{previewModal.title}
{previewModal.title}
)}
); } /* ========================================================================== MAIN ROOT APPLICATION ========================================================================== */ function App() { const [role, setRole] = useState(() => { return window.location.hash === "#admin" ? "admin" : "user"; }); // 3 Languages support: 'mr' (Marathi), 'hi' (Hindi), 'en' (English) const [lang, setLang] = useState(() => { return localStorage.getItem("student_election_lang") || "mr"; }); const t = TRANSLATIONS[lang] || TRANSLATIONS.en; const [adminToken, setAdminToken] = useState(() => { return sessionStorage.getItem("student_election_admin_token") || ""; }); const [selectedCandidate, setSelectedCandidate] = useState(null); const [locationsData, setLocationsData] = useState(null); useEffect(() => { fetch("data/locations.json?v=" + Date.now()) .then((r) => r.json()) .then((json) => setLocationsData(json)) .catch(() => {}); }, []); const handleLanguageChange = (nextLang) => { setLang(nextLang); localStorage.setItem("student_election_lang", nextLang); }; const switchRole = (nextRole) => { setRole(nextRole); window.location.hash = nextRole === "admin" ? "admin" : "user"; if (nextRole === "user") { setSelectedCandidate(null); } }; const handleAdminLoginSuccess = (token) => { sessionStorage.setItem("student_election_admin_token", token); setAdminToken(token); }; const handleAdminLogout = () => { sessionStorage.removeItem("student_election_admin_token"); setAdminToken(""); setSelectedCandidate(null); }; const isDetailsView = role === "admin" && adminToken && selectedCandidate; return ( <> {/* Top Bar: Role Switcher + 3-Language Switcher (मराठी / हिंदी / English) + Quick Dial */}
🗳️ {t.portalTitle}
📞 {HELPLINE_NUMBER}
{/* Mobile Responsive App Container */}
{role === "user" ? ( ) : !adminToken ? ( ) : selectedCandidate ? ( setSelectedCandidate(null)} t={t} /> ) : ( setSelectedCandidate(cand)} onLogout={handleAdminLogout} t={t} /> )}
{/* Sticky Bottom Helpline Bar with Auto-Call Dial CTA (`tel:9561131729`) */}
{t.helplineTitle} 📞 {HELPLINE_NUMBER}
{Icons.phone} {t.autoDialCta}
); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render();