feat: add date filter for character activity

This commit is contained in:
Guarzo
2025-11-25 01:44:52 +00:00
parent b7c0b45c15
commit 88ed9cd39e
7 changed files with 87 additions and 13 deletions

View File

@@ -1,4 +1,7 @@
import { Dialog } from 'primereact/dialog'; import { Dialog } from 'primereact/dialog';
import { Menu } from 'primereact/menu';
import { MenuItem } from 'primereact/menuitem';
import { useState, useCallback, useRef, useMemo } from 'react';
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx'; import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
interface CharacterActivityProps { interface CharacterActivityProps {
@@ -6,17 +9,69 @@ interface CharacterActivityProps {
onHide: () => void; onHide: () => void;
} }
const periodOptions = [
{ value: 30, label: '30 Days' },
{ value: 365, label: '1 Year' },
{ value: null, label: 'All Time' },
];
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => { export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
const [selectedPeriod, setSelectedPeriod] = useState<number | null>(30);
const menuRef = useRef<Menu>(null);
const handlePeriodChange = useCallback((days: number | null) => {
setSelectedPeriod(days);
}, []);
const menuItems: MenuItem[] = useMemo(
() => [
{
label: 'Period',
items: periodOptions.map(option => ({
label: option.label,
icon: selectedPeriod === option.value ? 'pi pi-check' : undefined,
command: () => handlePeriodChange(option.value),
})),
},
],
[selectedPeriod, handlePeriodChange],
);
const selectedPeriodLabel = useMemo(
() => periodOptions.find(opt => opt.value === selectedPeriod)?.label || 'All Time',
[selectedPeriod],
);
const headerIcons = (
<>
<button
type="button"
className="p-dialog-header-icon p-link"
onClick={e => menuRef.current?.toggle(e)}
aria-label="Filter options"
>
<span className="pi pi-bars" />
</button>
<Menu model={menuItems} popup ref={menuRef} />
</>
);
return ( return (
<Dialog <Dialog
header="Character Activity" header={
<div className="flex items-center gap-2">
<span>Character Activity</span>
<span className="text-xs text-stone-400">({selectedPeriodLabel})</span>
</div>
}
visible={visible} visible={visible}
className="w-[550px] max-h-[90vh]" className="w-[550px] max-h-[90vh]"
onHide={onHide} onHide={onHide}
dismissableMask dismissableMask
contentClassName="p-0 h-full flex flex-col" contentClassName="p-0 h-full flex flex-col"
icons={headerIcons}
> >
<CharacterActivityContent /> <CharacterActivityContent selectedPeriod={selectedPeriod} />
</Dialog> </Dialog>
); );
}; };

View File

@@ -7,16 +7,28 @@ import {
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx'; } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
import { Column } from 'primereact/column'; import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react'; import { useMemo, useEffect } from 'react';
import { useCharacterActivityHandlers } from '@/hooks/Mapper/components/mapRootContent/hooks/useCharacterActivityHandlers';
export const CharacterActivityContent = () => { interface CharacterActivityContentProps {
selectedPeriod: number | null;
}
export const CharacterActivityContent = ({ selectedPeriod }: CharacterActivityContentProps) => {
const { const {
data: { characterActivityData }, data: { characterActivityData },
} = useMapRootState(); } = useMapRootState();
const { handleShowActivity } = useCharacterActivityHandlers();
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]); const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]); const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
// Reload activity data when period changes
useEffect(() => {
handleShowActivity(selectedPeriod);
}, [selectedPeriod, handleShowActivity]);
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center h-full w-full"> <div className="flex flex-col items-center justify-center h-full w-full">

View File

@@ -23,17 +23,17 @@ export const useCharacterActivityHandlers = () => {
/** /**
* Handle showing the character activity dialog * Handle showing the character activity dialog
*/ */
const handleShowActivity = useCallback(() => { const handleShowActivity = useCallback((days?: number | null) => {
// Update local state to show the dialog // Update local state to show the dialog
update(state => ({ update(state => ({
...state, ...state,
showCharacterActivity: true, showCharacterActivity: true,
})); }));
// Send the command to the server // Send the command to the server with optional days parameter
outCommand({ outCommand({
type: OutCommand.showActivity, type: OutCommand.showActivity,
data: {}, data: days !== undefined ? { days } : {},
}); });
}, [outCommand, update]); }, [outCommand, update]);

View File

@@ -68,4 +68,5 @@ export interface ActivitySummary {
passages: number; passages: number;
connections: number; connections: number;
signatures: number; signatures: number;
timestamp?: string;
} }

View File

@@ -43,13 +43,14 @@ defmodule WandererApp.Character.Activity do
## Parameters ## Parameters
- `map_id`: ID of the map - `map_id`: ID of the map
- `current_user`: Current user struct (used only to get user settings) - `current_user`: Current user struct (used only to get user settings)
- `days`: Optional number of days to filter activity (nil for all time)
## Returns ## Returns
- List of processed activity data - List of processed activity data
""" """
def process_character_activity(map_id, current_user) do def process_character_activity(map_id, current_user, days \\ nil) do
with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id), with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
{:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id), {:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id, days),
{:ok, user_characters} <- {:ok, user_characters} <-
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
process_activity_data(raw_activity, map_user_settings, user_characters) process_activity_data(raw_activity, map_user_settings, user_characters)

View File

@@ -463,7 +463,8 @@ defmodule WandererApp.Esi.ApiClient do
{:error, reason} -> {:error, reason} ->
# Check if this is a Finch pool error # Check if this is a Finch pool error
if is_exception(reason) and Exception.message(reason) =~ "unable to provide a connection" do if is_exception(reason) and
Exception.message(reason) =~ "unable to provide a connection" do
:telemetry.execute( :telemetry.execute(
[:wanderer_app, :finch, :pool_exhausted], [:wanderer_app, :finch, :pool_exhausted],
%{count: 1}, %{count: 1},
@@ -677,7 +678,8 @@ defmodule WandererApp.Esi.ApiClient do
{:error, reason} -> {:error, reason} ->
# Check if this is a Finch pool error # Check if this is a Finch pool error
if is_exception(reason) and Exception.message(reason) =~ "unable to provide a connection" do if is_exception(reason) and
Exception.message(reason) =~ "unable to provide a connection" do
:telemetry.execute( :telemetry.execute(
[:wanderer_app, :finch, :pool_exhausted], [:wanderer_app, :finch, :pool_exhausted],
%{count: 1}, %{count: 1},

View File

@@ -30,14 +30,17 @@ defmodule WandererAppWeb.MapActivityEventHandler do
def handle_ui_event( def handle_ui_event(
"show_activity", "show_activity",
_, params,
%{assigns: %{map_id: map_id, current_user: current_user}} = socket %{assigns: %{map_id: map_id, current_user: current_user}} = socket
) do ) do
Task.async(fn -> Task.async(fn ->
try do try do
# Extract days parameter (nil if not provided)
days = Map.get(params, "days")
# Get raw activity data from the domain logic # Get raw activity data from the domain logic
result = result =
WandererApp.Character.Activity.process_character_activity(map_id, current_user) WandererApp.Character.Activity.process_character_activity(map_id, current_user, days)
# Group activities by user_id and summarize # Group activities by user_id and summarize
summarized_result = summarized_result =