Volunteer Weekly Check In Portal
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import BoardSDK from '@api/BoardSDK.js';
import { Button } from '@components/ui/button';
import { Input } from '@components/ui/input';
import { Label } from '@components/ui/label';
import { Textarea } from '@components/ui/textarea';
import { Calendar } from '@components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover';
import { KPICard } from '@components/KPICard';
import { Skeleton } from '@components/ui/skeleton';
import { CalendarIcon, Clock, Users, ClipboardCheck, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '@lib/utils';
const board = new BoardSDK();
export default function App() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [cursor, setCursor] = useState(null);
const [hasMore, setHasMore] = useState(false);
// Form state
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [date, setDate] = useState(new Date());
const [hours, setHours] = useState('');
const [tasks, setTasks] = useState('');
const [feedback, setFeedback] = useState('');
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
// Weekly stats
const [weeklyStats, setWeeklyStats] = useState({ totalHours: 0, volunteerCount: 0, checkInCount: 0 });
const [statsLoading, setStatsLoading] = useState(true);
// Load saved volunteer info from localStorage
useEffect(() => {
const savedName = localStorage.getItem('volunteer_name');
const savedEmail = localStorage.getItem('volunteer_email');
if (savedName) setName(savedName);
if (savedEmail) setEmail(savedEmail);
}, []);
// Fetch initial items
useEffect(() => {
loadItems();
loadWeeklyStats();
}, []);
async function loadItems(loadMore = false) {
try {
setLoading(true);
const query = board.items()
.withColumns(['dateOfVolunteering', 'emailAddress', 'hoursVolunteered', 'feedback', 'tasksCompleted'])
.orderBy({ column: 'dateOfVolunteering', direction: 'desc' });
if (loadMore && cursor) {
query.withPagination({ cursor });
} else {
query.withPagination({ limit: 100 });
}
const result = await query.execute();
if (loadMore) {
setItems((prev) => [...prev, ...result.items]);
} else {
setItems(result.items);
}
setCursor(result.cursor);
setHasMore(!!result.cursor);
} catch (error) {
console.error('Failed to load check-ins:', error);
} finally {
setLoading(false);
}
}
async function loadWeeklyStats() {
try {
setStatsLoading(true);
const startOfWeek = getStartOfWeek(new Date());
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(endOfWeek.getDate() + 6);
const startStr = startOfWeek.toISOString().split('T')[0];
const endStr = endOfWeek.toISOString().split('T')[0];
// Get sum of hours and count for this week
const stats = await board.aggregate()
.sum('hoursVolunteered', 'totalHours')
.countItems('checkInCount')
.where({ dateOfVolunteering: { between: { from: startStr, to: endStr } } })
.execute();
// Count unique volunteers
const weekItems = await board.items()
.withColumns(['emailAddress'])
.where({ dateOfVolunteering: { between: { from: startStr, to: endStr } } })
.withPagination({ limit: 500 })
.execute();
const uniqueEmails = new Set(
weekItems.items
.map(item => item.emailAddress?.email)
.filter(Boolean)
);
setWeeklyStats({
totalHours: stats[0]?.totalHours || 0,
checkInCount: stats[0]?.checkInCount || 0,
volunteerCount: uniqueEmails.size
});
} catch (error) {
console.error('Failed to load weekly stats:', error);
} finally {
setStatsLoading(false);
}
}
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
if (!name.trim() || !email.trim() || !hours) return;
// Save volunteer info to localStorage
localStorage.setItem('volunteer_name', name.trim());
localStorage.setItem('volunteer_email', email.trim());
const newEntry = {
id: `temp-${Date.now()}`,
name: name.trim(),
dateOfVolunteering: date,
emailAddress: { email: email.trim(), label: 'Work' },
hoursVolunteered: parseFloat(hours),
feedback: feedback.trim() || null,
tasksCompleted: tasks.trim() || null,
createdAt: new Date().toISOString()
};
// Optimistic update (SNAP)
const prevItems = items;
setItems([newEntry, ...items]);
// Update weekly stats optimistically
const prevStats = weeklyStats;
if (isThisWeek(date)) {
setWeeklyStats({
totalHours: weeklyStats.totalHours + parseFloat(hours),
checkInCount: weeklyStats.checkInCount + 1,
volunteerCount: weeklyStats.volunteerCount // Will update on next load
});
}
setSubmitting(true);
try {
const created = await board.item().create({
name: name.trim(),
dateOfVolunteering: date,
emailAddress: { email: email.trim(), label: 'Work' },
hoursVolunteered: parseFloat(hours),
feedback: feedback.trim() || null,
tasksCompleted: tasks.trim() || null
})
.returnColumns(['dateOfVolunteering', 'emailAddress', 'hoursVolunteered', 'feedback', 'tasksCompleted'])
.inGroup('topics')
.execute();
// Replace temp with real item
setItems(prevItems => prevItems.map(item => item.id === newEntry.id ? created : item));
// Clear form (but keep name/email)
setDate(new Date());
setHours('');
setTasks('');
setFeedback('');
// Reload stats to get accurate volunteer count
loadWeeklyStats();
} catch (error) {
console.error('Failed to create check-in:', error);
// Rollback
setItems(prevItems);
setWeeklyStats(prevStats);
} finally {
setSubmitting(false);
}
}, [name, email, date, hours, tasks, feedback, items, weeklyStats]);
// Group items by week
const groupedByWeek = useMemo(() => {
const groups = {};
for (const item of items) {
if (!item.dateOfVolunteering) continue;
const itemDate = new Date(item.dateOfVolunteering);
const weekStart = getStartOfWeek(itemDate);
const weekKey = weekStart.toISOString().split('T')[0];
if (!groups[weekKey]) {
groups[weekKey] = {
weekStart,
entries: [],
totalHours: 0
};
}
groups[weekKey].entries.push(item);
groups[weekKey].totalHours += item.hoursVolunteered || 0;
}
return Object.entries(groups)
.sort(([a], [b]) => b.localeCompare(a))
.map(([key, data]) => ({
weekKey: key,
weekLabel: getWeekLabel(data.weekStart),
...data
}));
}, [items]);
return (
);
}
function WeekSection({ week }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
{/* Header */}
{/* Check-in Form */}
{/* Weekly Overview for ED */}
{/* Recent Check-Ins by Week */}
) : groupedByWeek.length === 0 ? (
) : (
Volunteer Check-In
Log your volunteer hours whenever you work
Quick Check-In
This Week
{statsLoading ? (
<>
>
) : (
<>
>
)}
Recent Activity
{loading ? (No check-ins yet
Submit your first check-in above to get started
{groupedByWeek.map((week) => (
))}
{hasMore && (
)}
)}
{/* Week Header */}
{/* Week Entries */}
{isExpanded && (
);
}
function EntryCard({ entry }) {
return (
{week.entries.map((entry) => (
))}
)}
{/* Header */}
{/* Tasks */}
{entry.tasksCompleted && (
);
}
// Helper functions
function getStartOfWeek(date) {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day;
return new Date(d.setDate(diff));
}
function getWeekLabel(weekStart) {
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const now = new Date();
const thisWeekStart = getStartOfWeek(now);
if (weekStart.toDateString() === thisWeekStart.toDateString()) {
return 'This Week';
}
const lastWeekStart = new Date(thisWeekStart);
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
if (weekStart.toDateString() === lastWeekStart.toDateString()) {
return 'Last Week';
}
const startStr = weekStart.toLocaleDateString([], { month: 'short', day: 'numeric' });
const endStr = weekEnd.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
return `${startStr} - ${endStr}`;
}
function isThisWeek(date) {
const weekStart = getStartOfWeek(new Date());
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const d = new Date(date);
return d >= weekStart && d <= weekEnd;
}
{entry.name}
{entry.emailAddress?.email}
{entry.dateOfVolunteering?.toLocaleDateString()}
{entry.hoursVolunteered?.toFixed(1)}h
Tasks Completed
)}
{/* Feedback */}
{entry.feedback && (
{entry.tasksCompleted}
Feedback
)}
{entry.feedback}
Driven by curiosity and built on purpose, this is where bold thinking meets thoughtful execution. Let’s create something meaningful together.

