diff --git a/package-lock.json b/package-lock.json index 274f82a..e285cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -806,6 +806,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index 39a8614..8e07cbf 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -11,6 +11,7 @@ import FootprintChart from "@/components/charts/FootprintChart"; import ComparisonSection from "@/components/dashboard/ComparisonSection"; import ShareButton from "@/components/ui/ShareButton"; import { getUserFootprints } from "@/lib/firebase/firestore"; +import { ActivityIcon, getActivityIconConfig } from "@/utils/activityIcons"; type SortOption = "newest" | "oldest" | "highest_impact" | "lowest_impact"; @@ -38,12 +39,13 @@ function StatCard({ title, value, change, icon, color }: StatCardProps) {

{value}

{change !== undefined && (

0 + className={`text-sm mt-1 ${ + change > 0 ? "text-red-600" : change < 0 - ? "text-green-600" - : "text-gray-600" - }`} + ? "text-green-600" + : "text-gray-600" + }`} > {change > 0 ? "↗" : change < 0 ? "↘" : "→"}{" "} {Math.abs(change).toFixed(1)}% from last week @@ -322,14 +324,14 @@ export default function Dashboard({ )} - {/* Recent Activities / Activity History (UPDATED SECTION) */} + {/* Recent Activities / Activity History (UPDATED WITH ICONS) */} {activityHistory.length > 0 && (

Activity History

- {/* NEW: Sort Dropdown UI */} + {/* Sort Dropdown UI */}
Sort by:
@@ -344,38 +346,77 @@ export default function Dashboard({ ))} - {/* Custom chevron/sort icon */}
- + + +
- {/* Sorted Activity List */} + {/* Sorted Activity List with Icons */}
{activityHistory.map((entry: any) => ( -
-
- - {/* Using toLocaleString for better date/time display */} - {entry.timestamp.toLocaleString()} - - - +{formatCO2Amount(entry.result.totalCO2)} +
+
+ + {entry.timestamp.toLocaleString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +
+ Total CO₂: + + +{formatCO2Amount(entry.result.totalCO2)} + +
+
{Object.entries(entry.activities).map( - ([activity, value]) => - (value as number) > 0 ? ( - { + if ((value as number) <= 0) return null; + + // Map activity input names to ActivityType + const activityType = + activity === 'streamingHours' ? 'streaming' : + activity === 'codingHours' ? 'coding' : + activity === 'videoCallHours' ? 'video_calls' : + activity === 'cloudStorageGB' ? 'cloud_storage' : + activity === 'gamingHours' ? 'gaming' : + activity === 'socialMediaHours' ? 'social_media' : + 'emails'; + + const config = getActivityIconConfig(activityType as ActivityType); + + return ( +
- {activity}: {value as number} - - ) : null + + + {activity.replace(/Hours|GB/, '')}: {value as number} + {activity.includes('Hours') ? 'h' : + activity.includes('GB') ? 'GB' : ''} + +
+ ); + } )}
diff --git a/src/components/forms/ActivityForm.tsx b/src/components/forms/ActivityForm.tsx index 48684d4..5bcde20 100644 --- a/src/components/forms/ActivityForm.tsx +++ b/src/components/forms/ActivityForm.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { ActivityInput } from '@/types'; import { calculateCarbonFootprint } from '@/lib/calculations/carbonFootprint'; import { ACTIVITY_LABELS, ACTIVITY_DESCRIPTIONS } from '@/constants/co2Factors'; +import { ActivityIcon, getActivityIconConfig } from '@/utils/activityIcons'; interface ActivityFormProps { onSubmit: ( @@ -13,7 +14,7 @@ interface ActivityFormProps { breakdown: Record; equivalents: Array<{ description: string; value: number; unit: string }>; }, - customToastMessage?: string // <-- Add this optional property + customToastMessage?: string ) => void; initialValues?: Partial; } @@ -131,59 +132,59 @@ export default function ActivityForm({ onSubmit, initialValues }: ActivityFormPr const formFields = [ { key: 'emails' as keyof ActivityInput, + activityType: 'emails' as const, label: ACTIVITY_LABELS.emails, description: ACTIVITY_DESCRIPTIONS.emails, max: 500, step: 1, - icon: '📧', }, { key: 'streamingHours' as keyof ActivityInput, + activityType: 'streaming' as const, label: ACTIVITY_LABELS.streaming, description: ACTIVITY_DESCRIPTIONS.streaming, max: 24, step: 0.5, - icon: '📺', }, { key: 'codingHours' as keyof ActivityInput, + activityType: 'coding' as const, label: ACTIVITY_LABELS.coding, description: ACTIVITY_DESCRIPTIONS.coding, max: 24, step: 0.5, - icon: '💻', }, { key: 'videoCallHours' as keyof ActivityInput, + activityType: 'video_calls' as const, label: ACTIVITY_LABELS.video_calls, description: ACTIVITY_DESCRIPTIONS.video_calls, max: 24, step: 0.5, - icon: '📹', }, { key: 'cloudStorageGB' as keyof ActivityInput, + activityType: 'cloud_storage' as const, label: ACTIVITY_LABELS.cloud_storage, description: ACTIVITY_DESCRIPTIONS.cloud_storage, max: 1000, step: 1, - icon: '☁️', }, { key: 'gamingHours' as keyof ActivityInput, + activityType: 'gaming' as const, label: ACTIVITY_LABELS.gaming, description: ACTIVITY_DESCRIPTIONS.gaming, max: 24, step: 0.5, - icon: '🎮', }, { key: 'socialMediaHours' as keyof ActivityInput, + activityType: 'social_media' as const, label: ACTIVITY_LABELS.social_media, description: ACTIVITY_DESCRIPTIONS.social_media, max: 24, step: 0.5, - icon: '📱', }, ]; @@ -197,98 +198,137 @@ export default function ActivityForm({ onSubmit, initialValues }: ActivityFormPr

Enter your digital activities for today to calculate your carbon footprint

+
+
+
+ Low Impact +
+
+
+ Medium Impact +
+
+
+ High Impact +
+
- {formFields.map((field) => ( -
-
- {field.icon} - -
- -

- {field.description} -

- -
-
- handleInputChange(field.key, parseFloat(e.target.value))} - onBlur={() => handleBlur(field.key)} - className={`flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider ${ - errors[field.key] ? 'border border-red-500' : '' - }`} - aria-invalid={!!errors[field.key]} - aria-describedby={`${field.key}-error ${field.key}-hint`} + {formFields.map((field) => { + const iconConfig = getActivityIconConfig(field.activityType); + + return ( +
+
+ -
+
+ +

+ {iconConfig.impactLevel.charAt(0).toUpperCase() + + iconConfig.impactLevel.slice(1)} carbon impact +

+
+
+ +

+ {field.description} +

+ +
+
handleInputChange(field.key, parseFloat(e.target.value) || 0)} + onChange={(e) => handleInputChange(field.key, parseFloat(e.target.value))} onBlur={() => handleBlur(field.key)} - className={`w-20 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 text-gray-900 ${ - errors[field.key] - ? 'border-red-500 focus:ring-red-500' - : 'border-gray-300 focus:ring-green-500' + className={`flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider ${ + errors[field.key] ? 'border border-red-500' : '' }`} + style={{ + background: `linear-gradient(to right, + ${iconConfig.impactLevel === 'low' ? '#10b981' : + iconConfig.impactLevel === 'medium' ? '#f59e0b' : '#ef4444'} + ${(activities[field.key] / field.max) * 100}%, + #e5e7eb ${(activities[field.key] / field.max) * 100}%)` + }} aria-invalid={!!errors[field.key]} aria-describedby={`${field.key}-error ${field.key}-hint`} /> - - {field.key.includes('Hours') ? 'hrs' : - field.key.includes('GB') ? 'GB' : - field.key === 'emails' ? 'emails' : 'units'} - +
+ handleInputChange(field.key, parseFloat(e.target.value) || 0)} + onBlur={() => handleBlur(field.key)} + className={` + w-20 px-3 py-2 border-2 rounded-md + focus:outline-none focus:ring-2 transition-all text-gray-900 + ${errors[field.key] + ? 'border-red-500 focus:ring-red-500' + : `${iconConfig.borderColor} focus:ring-${iconConfig.color.replace('text-', '').replace('-600', '-500')}` + } + `} + aria-invalid={!!errors[field.key]} + aria-describedby={`${field.key}-error ${field.key}-hint`} + /> + + {field.key.includes('Hours') ? 'hrs' : + field.key.includes('GB') ? 'GB' : + field.key === 'emails' ? '' : 'units'} + +
-
- - {/* Progress bar */} -
-
-
- {/* Hint */} -

- {field.key.includes('Hours') - ? `Enter duration in ${field.key.includes('streaming') || field.key.includes('gaming') ? 'hours' : 'hours'} (e.g., 2.5)` - : field.key === 'emails' - ? 'Enter number of emails sent today' - : field.key === 'cloudStorageGB' - ? 'Enter storage used in GB' - : 'Enter quantity'} -

- - {/* Error message */} - {errors[field.key] && ( -

+ {field.key.includes('Hours') + ? `Enter duration in hours (e.g., 2.5)` + : field.key === 'emails' + ? 'Enter number of emails sent today' + : field.key === 'cloudStorageGB' + ? 'Enter storage used in GB' + : 'Enter quantity'}

- )} + + {/* Error message */} + {errors[field.key] && ( + + )} +
-
- ))} + ); + })}
{/* Form error message */} {errors.form && ( -
-

+

+

+ ⚠️ {errors.form}

@@ -298,7 +338,7 @@ export default function ActivityForm({ onSubmit, initialValues }: ActivityFormPr