different stats view

This commit is contained in:
chark1es 2025-03-01 16:04:07 -08:00
parent c4e5689593
commit 9d7058d533
2 changed files with 76 additions and 140 deletions

View file

@ -1,31 +1,11 @@
import { useEffect, useState, useCallback, useMemo } from "react"; import { useEffect, useState, useCallback, useMemo } from "react";
import { Get } from "../../../scripts/pocketbase/Get";
import { Authentication } from "../../../scripts/pocketbase/Authentication"; import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { DataSyncService } from "../../../scripts/database/DataSyncService"; import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema"; import { Collections } from "../../../schemas/pocketbase/schema";
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import type { Log } from "../../../schemas/pocketbase"; import type { Log } from "../../../schemas/pocketbase";
interface PaginatedResponse {
page: number;
perPage: number;
totalItems: number;
totalPages: number;
items: Log[];
}
interface CacheEntry {
timestamp: number;
data: Log[];
totalItems: number;
totalPages: number;
lastFetched: string; // ISO date string to track when we last fetched all logs
}
const LOGS_PER_PAGE = 5; const LOGS_PER_PAGE = 5;
const CACHE_EXPIRY = 5 * 60 * 1000; // 5 minutes
const CACHE_KEY_PREFIX = 'logs_cache_';
const BATCH_SIZE = 100; // Number of logs to fetch per batch
export default function ShowProfileLogs() { export default function ShowProfileLogs() {
const [logs, setLogs] = useState<Log[]>([]); const [logs, setLogs] = useState<Log[]>([]);
@ -35,62 +15,10 @@ export default function ShowProfileLogs() {
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalLogs, setTotalLogs] = useState(0); const [totalLogs, setTotalLogs] = useState(0);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [cachedLogs, setCachedLogs] = useState<Log[]>([]); const [allLogs, setAllLogs] = useState<Log[]>([]);
const [isFetchingAll, setIsFetchingAll] = useState(false); const [isFetchingAll, setIsFetchingAll] = useState(false);
const getCacheKey = (userId: string) => `${CACHE_KEY_PREFIX}${userId}`; const fetchLogs = async (skipCache = false) => {
const saveToCache = (userId: string, data: CacheEntry) => {
try {
localStorage.setItem(getCacheKey(userId), JSON.stringify(data));
} catch (error) {
console.warn('Failed to save to cache:', error);
}
};
const getFromCache = (userId: string): CacheEntry | null => {
try {
const cached = localStorage.getItem(getCacheKey(userId));
if (!cached) return null;
const parsedCache = JSON.parse(cached) as CacheEntry;
if (Date.now() - parsedCache.timestamp > CACHE_EXPIRY) {
localStorage.removeItem(getCacheKey(userId));
return null;
}
return parsedCache;
} catch (error) {
console.warn('Failed to read from cache:', error);
return null;
}
};
const fetchAllLogs = async (userId: string): Promise<Log[]> => {
const dataSync = DataSyncService.getInstance();
let allLogs: Log[] = [];
let page = 1;
let hasMore = true;
// First, sync all logs for this user
await dataSync.syncCollection(
Collections.LOGS,
`user_id = "${userId}"`,
"-created"
);
// Then get all logs from IndexedDB
allLogs = await dataSync.getData<Log>(
Collections.LOGS,
false, // Don't force sync again
`user_id = "${userId}"`,
"-created"
);
return allLogs;
};
const fetchLogs = async (page: number, skipCache = false) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@ -105,34 +33,29 @@ export default function ShowProfileLogs() {
} }
try { try {
// Check cache first if not skipping
if (!skipCache) {
const cached = getFromCache(userId);
if (cached) {
setCachedLogs(cached.data);
setTotalPages(Math.ceil(cached.data.length / LOGS_PER_PAGE));
setTotalLogs(cached.data.length);
setLoading(false);
return;
}
}
setIsFetchingAll(true); setIsFetchingAll(true);
const allLogs = await fetchAllLogs(userId);
// Save to cache // Use DataSyncService to fetch logs
saveToCache(userId, { const dataSync = DataSyncService.getInstance();
timestamp: Date.now(),
data: allLogs,
totalItems: allLogs.length,
totalPages: Math.ceil(allLogs.length / LOGS_PER_PAGE),
lastFetched: new Date().toISOString()
});
setCachedLogs(allLogs); // First sync logs for this user
setCurrentPage(page); await dataSync.syncCollection(
setTotalPages(Math.ceil(allLogs.length / LOGS_PER_PAGE)); Collections.LOGS,
setTotalLogs(allLogs.length); `user_id = "${userId}"`,
"-created"
);
// Then get all logs from IndexedDB
const fetchedLogs = await dataSync.getData<Log>(
Collections.LOGS,
false, // Don't force sync again
`user_id = "${userId}"`,
"-created"
);
setAllLogs(fetchedLogs);
setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE));
setTotalLogs(fetchedLogs.length);
} catch (error) { } catch (error) {
console.error("Failed to fetch logs:", error); console.error("Failed to fetch logs:", error);
setError("Error loading activity"); setError("Error loading activity");
@ -148,17 +71,17 @@ export default function ShowProfileLogs() {
// When not searching, return only the current page of logs // When not searching, return only the current page of logs
const startIndex = (currentPage - 1) * LOGS_PER_PAGE; const startIndex = (currentPage - 1) * LOGS_PER_PAGE;
const endIndex = startIndex + LOGS_PER_PAGE; const endIndex = startIndex + LOGS_PER_PAGE;
return cachedLogs.slice(startIndex, endIndex); return allLogs.slice(startIndex, endIndex);
} }
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return cachedLogs.filter(log => { return allLogs.filter(log => {
return ( return (
log.message.toLowerCase().includes(query) || log.message?.toLowerCase().includes(query) ||
new Date(log.created).toLocaleString().toLowerCase().includes(query) (log.created && new Date(log.created).toLocaleString().toLowerCase().includes(query))
); );
}); });
}, [searchQuery, cachedLogs, currentPage]); }, [searchQuery, allLogs, currentPage]);
// Update displayed logs whenever filtered results change // Update displayed logs whenever filtered results change
useEffect(() => { useEffect(() => {
@ -169,6 +92,10 @@ export default function ShowProfileLogs() {
const debouncedSearch = useCallback( const debouncedSearch = useCallback(
debounce((query: string) => { debounce((query: string) => {
setSearchQuery(query); setSearchQuery(query);
// Reset to first page when searching
if (query.trim()) {
setCurrentPage(1);
}
}, 300), }, 300),
[] []
); );
@ -190,20 +117,20 @@ export default function ShowProfileLogs() {
}; };
const handleRefresh = () => { const handleRefresh = () => {
fetchLogs(currentPage, true); fetchLogs(true);
}; };
useEffect(() => { useEffect(() => {
fetchLogs(1); fetchLogs();
}, []); }, []);
if (loading && !cachedLogs.length) { if (loading && !allLogs.length) {
return ( return (
<p className="text-base-content/70 flex items-center gap-2"> <p className="text-base-content/70 flex items-center gap-2">
<svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" /> <path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg> </svg>
{isFetchingAll ? 'Fetching all activity...' : 'Loading activity...'} {isFetchingAll ? 'Fetching your activity...' : 'Loading activity...'}
</p> </p>
); );
} }
@ -219,7 +146,7 @@ export default function ShowProfileLogs() {
); );
} }
if (logs.length === 0 && !searchQuery) { if (logs.length === 0 && !searchQuery && !loading) {
return ( return (
<p className="text-base-content/70 flex items-center gap-2"> <p className="text-base-content/70 flex items-center gap-2">
<svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@ -256,7 +183,7 @@ export default function ShowProfileLogs() {
{isFetchingAll && ( {isFetchingAll && (
<div className="mb-4"> <div className="mb-4">
<p className="text-sm opacity-70">Fetching all logs, please wait...</p> <p className="text-sm opacity-70">Fetching all activity, please wait...</p>
</div> </div>
)} )}
@ -296,11 +223,11 @@ export default function ShowProfileLogs() {
</div> </div>
{/* Pagination Controls */} {/* Pagination Controls */}
{!searchQuery && ( {!searchQuery && totalLogs > LOGS_PER_PAGE && (
<div className="flex justify-between items-center mt-6 pt-4 border-t border-base-200"> <div className="flex justify-between items-center mt-6 pt-4 border-t border-base-200">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm opacity-70"> <span className="text-sm opacity-70">
Showing {(currentPage - 1) * LOGS_PER_PAGE + 1}-{Math.min(currentPage * LOGS_PER_PAGE, totalLogs)} of {totalLogs} results Showing {totalLogs ? (currentPage - 1) * LOGS_PER_PAGE + 1 : 0}-{Math.min(currentPage * LOGS_PER_PAGE, totalLogs)} of {totalLogs} results
</span> </span>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Get } from "../../../scripts/pocketbase/Get";
import { Authentication } from "../../../scripts/pocketbase/Authentication"; import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { DataSyncService } from "../../../scripts/database/DataSyncService"; import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema"; import { Collections } from "../../../schemas/pocketbase/schema";
@ -8,32 +7,35 @@ import type { Event, Log, User } from "../../../schemas/pocketbase";
// Extended User interface with points property // Extended User interface with points property
interface ExtendedUser extends User { interface ExtendedUser extends User {
points?: number; points?: number;
member_type?: string;
} }
export function Stats() { export function Stats() {
const [eventsAttended, setEventsAttended] = useState(0); const [eventsAttended, setEventsAttended] = useState(0);
const [loyaltyPoints, setLoyaltyPoints] = useState(0); const [loyaltyPoints, setLoyaltyPoints] = useState(0);
const [activityLevel, setActivityLevel] = useState("Low");
const [activityDesc, setActivityDesc] = useState("New Member");
const [pointsChange, setPointsChange] = useState("No activity"); const [pointsChange, setPointsChange] = useState("No activity");
const [membershipStatus, setMembershipStatus] = useState("Member");
const [memberSince, setMemberSince] = useState<string | null>(null);
const [upcomingEvents, setUpcomingEvents] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchStats = async () => { const fetchStats = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const get = Get.getInstance();
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const dataSync = DataSyncService.getInstance(); const dataSync = DataSyncService.getInstance();
const userId = auth.getCurrentUser()?.id; const userId = auth.getCurrentUser()?.id;
if (!userId) return; if (!userId) return;
// Get current quarter dates // Get current date
const now = new Date(); const now = new Date();
const month = now.getMonth(); // 0-11
// Get current quarter dates for points calculation
const month = now.getMonth(); // 0-11
let quarterStart = new Date(); let quarterStart = new Date();
// Fall: Sept-Dec // Fall: Sept-Dec
if (month >= 8 && month <= 11) { if (month >= 8 && month <= 11) {
quarterStart = new Date(now.getFullYear(), 8, 1); // Sept 1 quarterStart = new Date(now.getFullYear(), 8, 1); // Sept 1
@ -58,7 +60,17 @@ export function Stats() {
const user = await dataSync.getItem<ExtendedUser>(Collections.USERS, userId); const user = await dataSync.getItem<ExtendedUser>(Collections.USERS, userId);
const totalPoints = user?.points || 0; const totalPoints = user?.points || 0;
// Sync logs for the current quarter // Set membership status and date
setMembershipStatus(user?.member_type || "Member");
if (user?.created) {
const createdDate = new Date(user.created);
setMemberSince(createdDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long'
}));
}
// Sync logs for the current quarter to calculate points change
await dataSync.syncCollection( await dataSync.syncCollection(
Collections.LOGS, Collections.LOGS,
`user_id = "${userId}" && created >= "${quarterStart.toISOString()}" && type = "update" && part = "event check-in"`, `user_id = "${userId}" && created >= "${quarterStart.toISOString()}" && type = "update" && part = "event check-in"`,
@ -75,41 +87,38 @@ export function Stats() {
// Calculate quarterly points // Calculate quarterly points
const quarterlyPoints = logs.reduce((total, log) => { const quarterlyPoints = logs.reduce((total, log) => {
const pointsMatch = log.message.match(/Awarded (\d+) points/); const pointsMatch = log.message?.match(/Awarded (\d+) points/);
if (pointsMatch) { if (pointsMatch) {
return total + parseInt(pointsMatch[1]); return total + parseInt(pointsMatch[1]);
} }
return total; return total;
}, 0); }, 0);
// Set points change message
setPointsChange(quarterlyPoints > 0 ? `+${quarterlyPoints} this quarter` : "No activity");
// Sync events collection // Sync events collection
await dataSync.syncCollection(Collections.EVENTS); await dataSync.syncCollection(Collections.EVENTS);
// Get events from IndexedDB // Get events from IndexedDB
const events = await dataSync.getData<Event>(Collections.EVENTS); const events = await dataSync.getData<Event>(Collections.EVENTS);
// Count attended events
const attendedEvents = events.filter(event => const attendedEvents = events.filter(event =>
event.attendees?.some(attendee => attendee.user_id === userId) event.attendees?.some(attendee => attendee.user_id === userId)
); );
setEventsAttended(attendedEvents.length);
const numEventsAttended = attendedEvents.length; // Count upcoming events (events that haven't ended yet)
setEventsAttended(numEventsAttended); const upcoming = events.filter(event => {
if (!event.end_date) return false;
const endDate = new Date(event.end_date);
return endDate > now && event.published;
});
setUpcomingEvents(upcoming.length);
// Set loyalty points
setLoyaltyPoints(totalPoints); setLoyaltyPoints(totalPoints);
// Set points change message with quarterly points
setPointsChange(quarterlyPoints > 0 ? `+${quarterlyPoints} this quarter` : "No activity");
// Determine activity level
if (numEventsAttended >= 10) {
setActivityLevel("High");
setActivityDesc("Very Active");
} else if (numEventsAttended >= 5) {
setActivityLevel("Medium");
setActivityDesc("Active Member");
} else if (numEventsAttended >= 1) {
setActivityLevel("Low");
setActivityDesc("Getting Started");
}
} catch (error) { } catch (error) {
console.error("Error fetching stats:", error); console.error("Error fetching stats:", error);
} finally { } finally {
@ -160,10 +169,10 @@ export function Stats() {
</div> </div>
<div className="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-accent transition-all duration-300 hover:-translate-y-1 transform"> <div className="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-accent transition-all duration-300 hover:-translate-y-1 transform">
<div className="stat"> <div className="stat">
<div className="stat-title font-medium opacity-80">Activity Level</div> <div className="stat-title font-medium opacity-80">Upcoming Events</div>
<div className="stat-value text-accent">{activityLevel}</div> <div className="stat-value text-accent">{upcomingEvents}</div>
<div className="stat-desc flex items-center gap-2 mt-1"> <div className="stat-desc flex items-center gap-2 mt-1">
<div className="badge badge-accent badge-sm">{activityDesc}</div> <div className="badge badge-accent badge-sm">Available to attend</div>
</div> </div>
</div> </div>
</div> </div>