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 { 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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue