Skip to content

Commit 855643e

Browse files
#12 ahp survey with form to submit results to contribute to our investigation
1 parent 6d4a4a9 commit 855643e

4 files changed

Lines changed: 139 additions & 4 deletions

File tree

dashboard/src/components/AHPModal.tsx

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
22
import { X, ChevronRight, ChevronLeft, Check, AlertTriangle } from 'lucide-react';
33
import { MetricDef } from '../types';
44
import { useTranslation } from 'react-i18next';
5+
import { AHP_SURVEY_URL } from '../constants';
56

67
interface AHPModalProps {
78
metrics: MetricDef[];
@@ -36,6 +37,13 @@ export const AHPModal: React.FC<AHPModalProps> = ({ metrics, isOpen, onClose, on
3637
const [currentStep, setCurrentStep] = useState(0);
3738
const [forceConsistency, setForceConsistency] = useState(true);
3839

40+
// Research Submission state
41+
const [role, setRole] = useState<string>('');
42+
const [otherRole, setOtherRole] = useState<string>('');
43+
const [isSubmitting, setIsSubmitting] = useState(false);
44+
const [submitSuccess, setSubmitSuccess] = useState(false);
45+
const [submitError, setSubmitError] = useState(false);
46+
3947
// Calculate AHP Weights & Consistency
4048
const results = useMemo(() => {
4149
if (currentStep !== pairs.length) return null;
@@ -115,6 +123,44 @@ export const AHPModal: React.FC<AHPModalProps> = ({ metrics, isOpen, onClose, on
115123
}
116124
};
117125

126+
const GOOGLE_SCRIPT_URL = AHP_SURVEY_URL;
127+
128+
const handleSubmitResearch = async () => {
129+
if (!results) return;
130+
setIsSubmitting(true);
131+
setSubmitError(false);
132+
133+
const finalRole = role === 'other' ? otherRole : role;
134+
135+
// Prepare data object based on results
136+
const data: Record<string, any> = {
137+
role: finalRole,
138+
CR: results.CR,
139+
timestamp: new Date().toISOString()
140+
};
141+
142+
metrics.forEach((m, idx) => {
143+
data[`weight_${m.id}`] = results.weights[idx];
144+
});
145+
146+
try {
147+
await fetch(GOOGLE_SCRIPT_URL, {
148+
method: 'POST',
149+
mode: 'no-cors',
150+
headers: {
151+
'Content-Type': 'text/plain;charset=utf-8',
152+
},
153+
body: JSON.stringify(data)
154+
});
155+
setSubmitSuccess(true);
156+
} catch (error) {
157+
console.error("Error submitting AHP results:", error);
158+
setSubmitError(true);
159+
} finally {
160+
setIsSubmitting(false);
161+
}
162+
};
163+
118164
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
119165
const newVal = parseInt(e.target.value);
120166
const newSelections = [...selections];
@@ -140,7 +186,7 @@ export const AHPModal: React.FC<AHPModalProps> = ({ metrics, isOpen, onClose, on
140186
lg:border ${isDarkMode ? 'lg:border-neutral-800' : 'lg:border-neutral-200'}
141187
relative w-full h-[100dvh] lg:h-auto lg:max-h-[90vh] lg:max-w-2xl flex flex-col overflow-hidden lg:rounded-3xl shadow-2xl transition-all
142188
`} onClick={e => e.stopPropagation()}>
143-
189+
144190
{/* Header */}
145191
<div className={`flex items-center justify-between p-5 lg:p-6 border-b shrink-0 ${isDarkMode ? 'border-neutral-800' : 'border-neutral-100'}`}>
146192
<div className="flex items-center gap-4">
@@ -266,6 +312,64 @@ export const AHPModal: React.FC<AHPModalProps> = ({ metrics, isOpen, onClose, on
266312
</div>
267313
</div>
268314
)}
315+
316+
{results && results.CR <= 0.1 && (
317+
<div className={`flex flex-col gap-4 p-5 rounded-xl border ${isDarkMode ? 'bg-neutral-800/30 border-neutral-800' : 'bg-neutral-50 border-neutral-100'}`}>
318+
<div className="space-y-1">
319+
<h4 className="text-sm lg:text-base font-bold uppercase tracking-wider">{t('ahp.submit_research')}</h4>
320+
<p className="text-[10px] lg:text-[12px] opacity-70">{t('ahp.submit_research_desc')}</p>
321+
</div>
322+
323+
{submitSuccess ? (
324+
<div className="flex items-center gap-2 text-green-500 font-bold text-[10px] lg:text-[12px] uppercase tracking-wider p-2">
325+
<Check className="w-4 h-4" /> {t('ahp.submit_success')}
326+
</div>
327+
) : (
328+
<div className="space-y-4">
329+
<div className="space-y-2">
330+
<label className="text-[10px] lg:text-[12px] font-bold uppercase tracking-widest opacity-70">{t('ahp.role_label')}</label>
331+
<select
332+
value={role}
333+
onChange={(e) => setRole(e.target.value)}
334+
disabled={isSubmitting}
335+
className={`w-full p-2.5 rounded-lg text-sm outline-none border transition-colors focus:border-sky-800 ${isDarkMode ? 'bg-neutral-900 border-neutral-800' : 'bg-white border-neutral-200'}`}
336+
>
337+
<option value="">-- {t('ahp.role_label')} --</option>
338+
<option value="student">{t('ahp.role_student')}</option>
339+
<option value="researcher">{t('ahp.role_researcher')}</option>
340+
<option value="professional">{t('ahp.role_professional')}</option>
341+
<option value="citizen">{t('ahp.role_citizen')}</option>
342+
<option value="other">{t('ahp.role_other')}</option>
343+
</select>
344+
</div>
345+
{role === 'other' && (
346+
<div className="space-y-2 animate-in slide-in-from-top-2 duration-200">
347+
<input
348+
type="text"
349+
value={otherRole}
350+
onChange={(e) => setOtherRole(e.target.value)}
351+
disabled={isSubmitting}
352+
placeholder={t('ahp.role_other_placeholder')}
353+
className={`w-full p-2.5 rounded-lg text-sm outline-none border transition-colors focus:border-sky-800 ${isDarkMode ? 'bg-neutral-900 border-neutral-800' : 'bg-white border-neutral-200'}`}
354+
/>
355+
</div>
356+
)}
357+
{submitError && (
358+
<div className="text-red-500 text-[10px] lg:text-[12px] font-bold uppercase tracking-wider p-1">
359+
{t('ahp.submit_error')}
360+
</div>
361+
)}
362+
<button
363+
onClick={handleSubmitResearch}
364+
disabled={isSubmitting}
365+
className={`w-full py-2.5 rounded-xl text-[10px] lg:text-[12px] font-bold uppercase tracking-widest transition-all ${isSubmitting ? 'opacity-50 cursor-not-allowed bg-sky-900 text-white' : 'bg-sky-900 hover:bg-sky-800 text-white shadow-lg shadow-sky-800/30'}`}
366+
>
367+
{isSubmitting ? t('ahp.submitting') : t('ahp.submit_button')}
368+
</button>
369+
</div>
370+
)}
371+
</div>
372+
)}
269373
</div>
270374
)}
271375
</div>
@@ -292,7 +396,11 @@ export const AHPModal: React.FC<AHPModalProps> = ({ metrics, isOpen, onClose, on
292396
) : (
293397
<>
294398
<button
295-
onClick={() => { setCurrentStep(0); }}
399+
onClick={() => {
400+
setCurrentStep(0);
401+
setSubmitSuccess(false);
402+
setSubmitError(false);
403+
}}
296404
className={`px-4 py-2 rounded-xl text-[10px] lg:text-[12px] font-bold uppercase tracking-widest transition-all ${isDarkMode ? 'hover:bg-neutral-800' : 'hover:bg-neutral-200'}`}
297405
>
298406
{results && results.CR > 0.1 && forceConsistency ? t('ahp.review_comparisons') : t('ahp.retake_survey')}

dashboard/src/constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { MetricDef, ViewLevel, ScaleMethod } from './types';
33

44
export const DATA_BASE_URL = 'https://ushift.tecnico.ulisboa.pt/content/impt/';
5+
export const AHP_SURVEY_URL = "https://script.google.com/macros/s/AKfycbx1F1XmBbgTDu0WKiRtR6I4ocouXsnVKMDz1DHuMjx79kgfQzP1M8-A-NPL4CjlugmL/exec";
56

67
export const LEVEL_CONFIG: Record<ViewLevel, { file: string, parent?: ViewLevel, download_geojson: string, download_csv: string }> = {
78
'municipality': { file: `${DATA_BASE_URL}municipios_dashboard.geojson`, download_geojson: `${DATA_BASE_URL}municipios_aggregated.geojson`, download_csv: `${DATA_BASE_URL}municipios_aggregated.csv` },

dashboard/src/locales/en.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,20 @@
421421
"inconsistent_pairs_label": "Most inconsistent comparisons:",
422422
"apply_weights": "Apply Weights",
423423
"retake_survey": "Retake Survey",
424-
"review_comparisons": "Review Comparisons"
424+
"review_comparisons": "Review Comparisons",
425+
"submit_research": "Contribute to Our Research",
426+
"submit_research_desc": "We kindly ask you to contribute to our research by sharing your preferences anonymously.",
427+
"role_label": "Your Role (Optional)",
428+
"role_student": "Student",
429+
"role_researcher": "Researcher",
430+
"role_professional": "Professional",
431+
"role_citizen": "Citizen",
432+
"role_other": "Other",
433+
"role_other_placeholder": "Please specify...",
434+
"submit_button": "Submit Anonymously",
435+
"submitting": "Submitting...",
436+
"submit_success": "Thank you for your contribution!",
437+
"submit_error": "An error occurred. Please try again."
425438
},
426439
"download": {
427440
"title": "Download Data",

dashboard/src/locales/pt.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,20 @@
427427
"inconsistent_pairs_label": "Comparações mais inconsistentes:",
428428
"apply_weights": "Aplicar Pesos",
429429
"retake_survey": "Refazer Inquérito",
430-
"review_comparisons": "Rever Comparações"
430+
"review_comparisons": "Rever Comparações",
431+
"submit_research": "Contribua para a Nossa Investigação",
432+
"submit_research_desc": "Pedimos que contribua para a nossa investigação partilhando anonimamente as suas preferências.",
433+
"role_label": "O seu perfil (Opcional)",
434+
"role_student": "Estudante",
435+
"role_researcher": "Investigador",
436+
"role_professional": "Profissional",
437+
"role_citizen": "Cidadão",
438+
"role_other": "Outro",
439+
"role_other_placeholder": "Especifique...",
440+
"submit_button": "Submeter Anonimamente",
441+
"submitting": "A submeter...",
442+
"submit_success": "Obrigado pela sua contribuição!",
443+
"submit_error": "Ocorreu um erro. Tente novamente."
431444
},
432445
"download": {
433446
"title": "Descarregar Dados",

0 commit comments

Comments
 (0)