different stats view
This commit is contained in:
parent
c4e5689593
commit
9d7058d533
2 changed files with 76 additions and 140 deletions
|
@ -1,31 +1,11 @@
|
|||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { Get } from "../../../scripts/pocketbase/Get";
|
||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||
import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
||||
import { Collections } from "../../../schemas/pocketbase/schema";
|
||||
import debounce from 'lodash/debounce';
|
||||
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 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() {
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
|
@ -35,62 +15,10 @@ export default function ShowProfileLogs() {
|
|||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalLogs, setTotalLogs] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [cachedLogs, setCachedLogs] = useState<Log[]>([]);
|
||||
const [allLogs, setAllLogs] = useState<Log[]>([]);
|
||||
const [isFetchingAll, setIsFetchingAll] = useState(false);
|
||||
|
||||
const getCacheKey = (userId: string) => `${CACHE_KEY_PREFIX}${userId}`;
|
||||
|
||||
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) => {
|
||||
const fetchLogs = async (skipCache = false) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
@ -105,34 +33,29 @@ export default function ShowProfileLogs() {
|
|||
}
|
||||
|
||||
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);
|
||||
const allLogs = await fetchAllLogs(userId);
|
||||
|
||||
// Save to cache
|
||||
saveToCache(userId, {
|
||||
timestamp: Date.now(),
|
||||
data: allLogs,
|
||||
totalItems: allLogs.length,
|
||||
totalPages: Math.ceil(allLogs.length / LOGS_PER_PAGE),
|
||||
lastFetched: new Date().toISOString()
|
||||
});
|
||||
// Use DataSyncService to fetch logs
|
||||
const dataSync = DataSyncService.getInstance();
|
||||
|
||||
setCachedLogs(allLogs);
|
||||
setCurrentPage(page);
|
||||
setTotalPages(Math.ceil(allLogs.length / LOGS_PER_PAGE));
|
||||
setTotalLogs(allLogs.length);
|
||||
// First sync logs for this user
|
||||
await dataSync.syncCollection(
|
||||
Collections.LOGS,
|
||||
`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) {
|
||||
console.error("Failed to fetch logs:", error);
|
||||
setError("Error loading activity");
|
||||
|
@ -148,17 +71,17 @@ export default function ShowProfileLogs() {
|
|||
// When not searching, return only the current page of logs
|
||||
const startIndex = (currentPage - 1) * LOGS_PER_PAGE;
|
||||
const endIndex = startIndex + LOGS_PER_PAGE;
|
||||
return cachedLogs.slice(startIndex, endIndex);
|
||||
return allLogs.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return cachedLogs.filter(log => {
|
||||
return allLogs.filter(log => {
|
||||
return (
|
||||
log.message.toLowerCase().includes(query) ||
|
||||
new Date(log.created).toLocaleString().toLowerCase().includes(query)
|
||||
log.message?.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
|
||||
useEffect(() => {
|
||||
|
@ -169,6 +92,10 @@ export default function ShowProfileLogs() {
|
|||
const debouncedSearch = useCallback(
|
||||
debounce((query: string) => {
|
||||
setSearchQuery(query);
|
||||
// Reset to first page when searching
|
||||
if (query.trim()) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
@ -190,20 +117,20 @@ export default function ShowProfileLogs() {
|
|||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLogs(currentPage, true);
|
||||
fetchLogs(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs(1);
|
||||
fetchLogs();
|
||||
}, []);
|
||||
|
||||
if (loading && !cachedLogs.length) {
|
||||
if (loading && !allLogs.length) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
{isFetchingAll ? 'Fetching all activity...' : 'Loading activity...'}
|
||||
{isFetchingAll ? 'Fetching your activity...' : 'Loading activity...'}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -219,7 +146,7 @@ export default function ShowProfileLogs() {
|
|||
);
|
||||
}
|
||||
|
||||
if (logs.length === 0 && !searchQuery) {
|
||||
if (logs.length === 0 && !searchQuery && !loading) {
|
||||
return (
|
||||
<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">
|
||||
|
@ -256,7 +183,7 @@ export default function ShowProfileLogs() {
|
|||
|
||||
{isFetchingAll && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
@ -296,11 +223,11 @@ export default function ShowProfileLogs() {
|
|||
</div>
|
||||
|
||||
{/* 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 items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Get } from "../../../scripts/pocketbase/Get";
|
||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||
import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
||||
import { Collections } from "../../../schemas/pocketbase/schema";
|
||||
|
@ -8,32 +7,35 @@ import type { Event, Log, User } from "../../../schemas/pocketbase";
|
|||
// Extended User interface with points property
|
||||
interface ExtendedUser extends User {
|
||||
points?: number;
|
||||
member_type?: string;
|
||||
}
|
||||
|
||||
export function Stats() {
|
||||
const [eventsAttended, setEventsAttended] = 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 [membershipStatus, setMembershipStatus] = useState("Member");
|
||||
const [memberSince, setMemberSince] = useState<string | null>(null);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const get = Get.getInstance();
|
||||
const auth = Authentication.getInstance();
|
||||
const dataSync = DataSyncService.getInstance();
|
||||
const userId = auth.getCurrentUser()?.id;
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
// Get current quarter dates
|
||||
// Get current 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();
|
||||
|
||||
// Fall: Sept-Dec
|
||||
if (month >= 8 && month <= 11) {
|
||||
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 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(
|
||||
Collections.LOGS,
|
||||
`user_id = "${userId}" && created >= "${quarterStart.toISOString()}" && type = "update" && part = "event check-in"`,
|
||||
|
@ -75,41 +87,38 @@ export function Stats() {
|
|||
|
||||
// Calculate quarterly points
|
||||
const quarterlyPoints = logs.reduce((total, log) => {
|
||||
const pointsMatch = log.message.match(/Awarded (\d+) points/);
|
||||
const pointsMatch = log.message?.match(/Awarded (\d+) points/);
|
||||
if (pointsMatch) {
|
||||
return total + parseInt(pointsMatch[1]);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
|
||||
// Set points change message
|
||||
setPointsChange(quarterlyPoints > 0 ? `+${quarterlyPoints} this quarter` : "No activity");
|
||||
|
||||
// Sync events collection
|
||||
await dataSync.syncCollection(Collections.EVENTS);
|
||||
|
||||
// Get events from IndexedDB
|
||||
const events = await dataSync.getData<Event>(Collections.EVENTS);
|
||||
|
||||
// Count attended events
|
||||
const attendedEvents = events.filter(event =>
|
||||
event.attendees?.some(attendee => attendee.user_id === userId)
|
||||
);
|
||||
setEventsAttended(attendedEvents.length);
|
||||
|
||||
const numEventsAttended = attendedEvents.length;
|
||||
setEventsAttended(numEventsAttended);
|
||||
// Count upcoming events (events that haven't ended yet)
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
console.error("Error fetching stats:", error);
|
||||
} finally {
|
||||
|
@ -160,10 +169,10 @@ export function Stats() {
|
|||
</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="stat">
|
||||
<div className="stat-title font-medium opacity-80">Activity Level</div>
|
||||
<div className="stat-value text-accent">{activityLevel}</div>
|
||||
<div className="stat-title font-medium opacity-80">Upcoming Events</div>
|
||||
<div className="stat-value text-accent">{upcomingEvents}</div>
|
||||
<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>
|
||||
|
|
Loading…
Reference in a new issue