mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-01 21:42:37 +00:00
feat: enhance character activty and summmarize by user (#206)
This commit is contained in:
@@ -8,13 +8,20 @@ import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/com
|
||||
import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx';
|
||||
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
|
||||
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
|
||||
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivity';
|
||||
import { TrackAndFollow } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/TrackAndFollow';
|
||||
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
|
||||
import { useTrackAndFollowHandlers } from './hooks/useTrackAndFollowHandlers';
|
||||
|
||||
export interface MapRootContentProps {}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
const { interfaceSettings, data } = useMapRootState();
|
||||
const { isShowMenu } = interfaceSettings;
|
||||
const { showCharacterActivity, showTrackAndFollow } = data;
|
||||
const { handleHideCharacterActivity } = useCharacterActivityHandlers();
|
||||
const { handleHideTracking } = useTrackAndFollowHandlers();
|
||||
|
||||
const themeClass = `${interfaceSettings.theme ?? 'default'}-theme`;
|
||||
|
||||
@@ -49,7 +56,11 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
</div>
|
||||
)}
|
||||
<OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} />
|
||||
<MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} />
|
||||
{showMapSettings && <MapSettings visible={showMapSettings} onHide={() => setShowMapSettings(false)} />}
|
||||
{showCharacterActivity && (
|
||||
<CharacterActivity visible={showCharacterActivity} onHide={handleHideCharacterActivity} />
|
||||
)}
|
||||
{showTrackAndFollow && <TrackAndFollow visible={showTrackAndFollow} onHide={handleHideTracking} />}
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
/* Character Activity Component Styles */
|
||||
.characterActivity {
|
||||
&Dialog {
|
||||
background-color: var(--surface-card);
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
width: 80vw;
|
||||
max-width: 650px;
|
||||
|
||||
/* Note: Overlay z-index settings are handled in fix-dialog.scss */
|
||||
|
||||
.p-dialog-header {
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--surface-section);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-header-icon {
|
||||
color: var(--text-color-secondary);
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0 none;
|
||||
outline-offset: 0;
|
||||
box-shadow: 0 0 0 0.2rem var(--focus-ring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced close button styling */
|
||||
.p-dialog-header-close {
|
||||
color: var(--text-color) !important;
|
||||
background: var(--surface-hover) !important;
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
border-radius: 50% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
position: relative !important;
|
||||
margin-left: 0.5rem !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.p-dialog-header-close:hover {
|
||||
background: var(--surface-hover) !important;
|
||||
color: var(--primary-color) !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.p-dialog-header-close:focus {
|
||||
box-shadow: 0 0 0 0.2rem var(--primary-color-light) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.p-dialog-header-close-icon {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-content {
|
||||
padding: 0;
|
||||
background-color: var(--surface-card);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
height: calc(100% - 70px);
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
.p-dialog-footer {
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--surface-section);
|
||||
border-top: 1px solid var(--surface-border);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set a fixed height for the container so scrolling works properly */
|
||||
&Container {
|
||||
width: 100%;
|
||||
height: 400px; // fixed height to match the DataTable's scrollHeight
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&Datatable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
flex: 1;
|
||||
table-layout: fixed !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.p-datatable-wrapper {
|
||||
border-radius: 0;
|
||||
overflow: auto !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--surface-section);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable-table {
|
||||
width: 100% !important;
|
||||
table-layout: fixed !important;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.p-datatable-thead > tr > th {
|
||||
background-color: var(--surface-section);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--surface-border);
|
||||
padding: 2px 8px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
line-height: 1.333;
|
||||
font-size: 12px;
|
||||
height: 24px !important;
|
||||
|
||||
&.headerCharacter {
|
||||
text-align: left;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
&.headerStandard {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Row styling */
|
||||
.p-datatable-tbody > tr {
|
||||
background-color: var(--surface-card);
|
||||
transition: background-color 0.2s;
|
||||
height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* Cell styling */
|
||||
& > td {
|
||||
border: 1px solid var(--surface-border);
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.333;
|
||||
color: var(--text-color);
|
||||
height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty message styling */
|
||||
.p-datatable-emptymessage td {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Character info styles */
|
||||
.characterInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.characterNameContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.characterName {
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.333;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.nameText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
display: inline;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.corporationTicker {
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.8;
|
||||
display: inline;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
max-width: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Cell styling */
|
||||
.characterNameCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.activityValueCell {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 1.333;
|
||||
font-variant-numeric: tabular-nums;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
/* Column styles */
|
||||
.characterColumn {
|
||||
min-width: 200px;
|
||||
width: 40%;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.numericColumn {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Empty state message */
|
||||
.emptyMessage {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Loading container */
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
margin-top: 1rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Error message styling */
|
||||
.errorMessage {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
/* Table row compact style */
|
||||
.tableRowCompact {
|
||||
font-size: 12px !important;
|
||||
line-height: 1.333 !important;
|
||||
height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import classes from './CharacterActivity.module.scss';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
|
||||
/**
|
||||
* Summary of a character's activity
|
||||
*/
|
||||
export interface ActivitySummary {
|
||||
character_id: string;
|
||||
character_name: string;
|
||||
corporation_ticker: string;
|
||||
alliance_ticker?: string;
|
||||
portrait_url: string;
|
||||
passages: number;
|
||||
connections: number;
|
||||
signatures: number;
|
||||
user_id?: string;
|
||||
user_name?: string;
|
||||
is_user?: boolean;
|
||||
}
|
||||
|
||||
interface CharacterActivityProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const getRowClassName = () => classes.tableRowCompact;
|
||||
|
||||
const renderCharacterTemplate = (rowData: ActivitySummary) => {
|
||||
return (
|
||||
<div className={classes.characterNameCell}>
|
||||
<div className={classes.characterInfo}>
|
||||
<div className={classes.characterPortrait}>
|
||||
<img src={rowData.portrait_url} alt={rowData.character_name} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className={classes.characterNameContainer}>
|
||||
<div className={classes.characterName}>
|
||||
{rowData.is_user ? (
|
||||
<>
|
||||
<span className={classes.nameText}>{rowData.user_name}</span>
|
||||
<span className={classes.corporationTicker}>[{rowData.corporation_ticker}]</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className={classes.nameText}>{rowData.character_name}</span>
|
||||
<span className={classes.corporationTicker}>[{rowData.corporation_ticker}]</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderValueTemplate = (rowData: ActivitySummary, field: keyof ActivitySummary) => {
|
||||
return <div className={classes.activityValueCell}>{rowData[field] as number}</div>;
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold">Character Activity</h2>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Component that displays character activity in a dialog.
|
||||
*/
|
||||
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
|
||||
const { data } = useMapRootState();
|
||||
const { characterActivityData } = data;
|
||||
const [localActivity, setLocalActivity] = useState<ActivitySummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Use the new structure directly
|
||||
const activity = useMemo(() => {
|
||||
return characterActivityData?.activity || [];
|
||||
}, [characterActivityData]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalActivity(activity);
|
||||
setLoading(characterActivityData?.loading !== false);
|
||||
}, [activity, characterActivityData]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={renderHeader}
|
||||
visible={visible}
|
||||
className={classes.characterActivityDialog}
|
||||
onHide={onHide}
|
||||
dismissableMask
|
||||
draggable={false}
|
||||
resizable={false}
|
||||
closable
|
||||
modal={true}
|
||||
>
|
||||
<div className={classes.characterActivityContainer}>
|
||||
{loading && (
|
||||
<div className={classes.loadingContainer}>
|
||||
<ProgressSpinner style={{ width: '50px', height: '50px' }} strokeWidth="4" />
|
||||
<div className={classes.loadingText}>Loading character activity data...</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && localActivity.length === 0 && (
|
||||
<div className={classes.emptyMessage}>No character activity data available</div>
|
||||
)}
|
||||
{!loading && localActivity.length > 0 && (
|
||||
<DataTable
|
||||
value={localActivity}
|
||||
className={classes.characterActivityDatatable}
|
||||
scrollable
|
||||
scrollHeight="400px"
|
||||
emptyMessage="No character activity data available"
|
||||
sortField="passages"
|
||||
sortOrder={-1}
|
||||
responsiveLayout="scroll"
|
||||
size="small"
|
||||
rowClassName={getRowClassName}
|
||||
rowHover
|
||||
>
|
||||
<Column
|
||||
field="character_name"
|
||||
header="Character"
|
||||
body={renderCharacterTemplate}
|
||||
sortable
|
||||
className={classes.characterColumn}
|
||||
headerClassName={`${classes.headerCharacter} text-xs`}
|
||||
/>
|
||||
<Column
|
||||
field="passages"
|
||||
header="Passages"
|
||||
body={rowData => renderValueTemplate(rowData, 'passages')}
|
||||
sortable
|
||||
className={classes.numericColumn}
|
||||
headerClassName={`${classes.headerStandard} text-xs`}
|
||||
/>
|
||||
<Column
|
||||
field="connections"
|
||||
header="Connections"
|
||||
body={rowData => renderValueTemplate(rowData, 'connections')}
|
||||
sortable
|
||||
className={classes.numericColumn}
|
||||
headerClassName={`${classes.headerStandard} text-xs`}
|
||||
/>
|
||||
<Column
|
||||
field="signatures"
|
||||
header="Signatures"
|
||||
body={rowData => renderValueTemplate(rowData, 'signatures')}
|
||||
sortable
|
||||
className={classes.numericColumn}
|
||||
headerClassName={`${classes.headerStandard} text-xs`}
|
||||
/>
|
||||
</DataTable>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -22,8 +22,15 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
|
||||
const handleAddCharacter = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.addCharacter,
|
||||
data: null,
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
const handleShowActivity = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.showActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
@@ -36,6 +43,12 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
command: handleAddCharacter,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Character Activity',
|
||||
icon: 'pi pi-chart-bar',
|
||||
command: handleShowActivity,
|
||||
visible: canTrackCharacters,
|
||||
},
|
||||
{
|
||||
label: 'On the map',
|
||||
icon: 'pi pi-hashtag',
|
||||
@@ -61,7 +74,14 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
},
|
||||
] as MenuItem[]
|
||||
).filter(item => item.visible);
|
||||
}, [canTrackCharacters, handleAddCharacter, onShowMapSettings, onShowOnTheMap, setInterfaceSettings]);
|
||||
}, [
|
||||
canTrackCharacters,
|
||||
handleAddCharacter,
|
||||
handleShowActivity,
|
||||
onShowMapSettings,
|
||||
onShowOnTheMap,
|
||||
setInterfaceSettings,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="ml-1">
|
||||
|
||||
@@ -40,7 +40,7 @@ export type UserSettingsRemote = {
|
||||
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
|
||||
|
||||
export interface MapSettingsProps {
|
||||
show: boolean;
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ const THEME_SETTING: SettingsListItem = {
|
||||
options: THEME_OPTIONS,
|
||||
};
|
||||
|
||||
export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
export const MapSettings = ({ visible, onHide }: MapSettingsProps) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
|
||||
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
|
||||
@@ -213,12 +213,12 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
header="Map user settings"
|
||||
visible={show}
|
||||
visible
|
||||
draggable={false}
|
||||
style={{ width: '550px' }}
|
||||
onShow={handleShow}
|
||||
onHide={() => {
|
||||
if (!show) return;
|
||||
if (!visible) return;
|
||||
setActiveIndex(0);
|
||||
onHide();
|
||||
}}
|
||||
|
||||
@@ -21,10 +21,11 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
|
||||
|
||||
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
|
||||
|
||||
const handleAddCharacter = useCallback(() => {
|
||||
const handleShowTracking = useCallback(() => {
|
||||
// Use the OutCommand pattern for showing the tracking dialog
|
||||
outCommand({
|
||||
type: OutCommand.addCharacter,
|
||||
data: null,
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
@@ -63,22 +64,25 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={handleAddCharacter}
|
||||
onClick={handleShowTracking}
|
||||
id="show-tracking-button"
|
||||
>
|
||||
<i className="pi pi-user-plus"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
{canTrackCharacters && (
|
||||
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={onShowOnTheMap}
|
||||
>
|
||||
<i className="pi pi-hashtag"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
<>
|
||||
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={onShowOnTheMap}
|
||||
>
|
||||
<i className="pi pi-hashtag"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
/* TrackAndFollow Dialog Styles */
|
||||
.trackFollowDialog {
|
||||
background-color: var(--surface-card);
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
width: 500px;
|
||||
|
||||
:global {
|
||||
/* Dialog header */
|
||||
.p-dialog-header {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--surface-section);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.p-dialog-header-close {
|
||||
color: var(--text-color);
|
||||
background: transparent;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 0.2rem var(--primary-color-light);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog content */
|
||||
.p-dialog-content {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--surface-card);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
height: calc(100% - 70px);
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dialog mask */
|
||||
.p-dialog-mask.p-component-overlay {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Character grid styles */
|
||||
.characterGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.characterGridHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
background-color: var(--surface-section);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
padding: 0.75rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.characterGridBody {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.characterGridRow {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: 0.5rem 0;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gridCellTrack,
|
||||
.gridCellFollow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gridCellCharacter {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Character info styles */
|
||||
.characterInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.characterDetails {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.characterName {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.characterCorp {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Empty state message */
|
||||
.emptyMessage {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Custom Checkbox and Radio Button Styles */
|
||||
.wdCheckbox, .wdRadio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--surface-border);
|
||||
background-color: var(--surface-card);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.wdCheckbox {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wdCheckboxChecked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.wdCheckboxIcon {
|
||||
color: var(--primary-color-text);
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wdRadio {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.wdRadioChecked {
|
||||
border-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.wdRadioDot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.trackingCell {
|
||||
.p-checkbox {
|
||||
.p-checkbox-box {
|
||||
background-color: var(--surface-card);
|
||||
border: 2px solid var(--surface-border);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&.p-highlight {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.p-checkbox-icon {
|
||||
color: var(--primary-color-text);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.followCell {
|
||||
.p-radiobutton {
|
||||
.p-radiobutton-box {
|
||||
background-color: var(--surface-card);
|
||||
border: 2px solid var(--surface-border);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&.p-highlight {
|
||||
background-color: var(--surface-card);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.p-radiobutton-icon {
|
||||
background-color: var(--primary-color);
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 640px) {
|
||||
.trackingCharacterHeader {
|
||||
grid-template-columns: 60px 60px 1fr;
|
||||
}
|
||||
|
||||
.trackingCharacterRow {
|
||||
grid-template-columns: 60px 60px 1fr;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radioContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.portraitImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.corporationTicker {
|
||||
margin-left: 0.5rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
margin-left: 0.25rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.hoverRow:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { VirtualScroller } from 'primereact/virtualscroller';
|
||||
import classes from './TrackAndFollow.module.scss';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
|
||||
import { TrackingCharacter } from './types';
|
||||
|
||||
interface TrackAndFollowProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
<div className="dialog-header">
|
||||
<span>Track & Follow</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
|
||||
const [trackedCharacters, setTrackedCharacters] = useState<string[]>([]);
|
||||
const [followedCharacter, setFollowedCharacter] = useState<string | null>(null);
|
||||
const { outCommand, data } = useMapRootState();
|
||||
const { trackingCharactersData } = data;
|
||||
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || characters.length === 0) {
|
||||
return;
|
||||
}
|
||||
const tracked = characters.filter(char => char.tracked).map(char => char.id);
|
||||
setTrackedCharacters(tracked);
|
||||
|
||||
const followed = characters.find(char => char.followed);
|
||||
setFollowedCharacter(followed ? followed.id : null);
|
||||
}, [visible, characters]);
|
||||
|
||||
const handleTrackToggle = (characterId: string) => {
|
||||
setTrackedCharacters(prev => {
|
||||
if (!prev.includes(characterId)) {
|
||||
return [...prev, characterId];
|
||||
}
|
||||
if (followedCharacter === characterId) {
|
||||
setFollowedCharacter(null);
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
}
|
||||
return prev.filter(id => id !== characterId);
|
||||
});
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
};
|
||||
|
||||
const handleFollowToggle = (characterId: string) => {
|
||||
if (followedCharacter !== characterId && !trackedCharacters.includes(characterId)) {
|
||||
setTrackedCharacters(prev => [...prev, characterId]);
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
}
|
||||
setFollowedCharacter(prev => (prev === characterId ? null : characterId));
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
};
|
||||
|
||||
const rowTemplate = (character: TrackingCharacter) => {
|
||||
return (
|
||||
<TrackingCharacterWrapper
|
||||
key={character.id}
|
||||
character={character}
|
||||
isTracked={trackedCharacters.includes(character.id)}
|
||||
isFollowed={followedCharacter === character.id}
|
||||
onTrackToggle={() => handleTrackToggle(character.id)}
|
||||
onFollowToggle={() => handleFollowToggle(character.id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={renderHeader()}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
modal
|
||||
className={classes.trackFollowDialog}
|
||||
closeOnEscape
|
||||
showHeader={true}
|
||||
closable={true}
|
||||
>
|
||||
<div className={classes.characterGrid}>
|
||||
<div className={classes.characterGridHeader}>
|
||||
<div>Track</div>
|
||||
<div>Follow</div>
|
||||
<div>Character</div>
|
||||
</div>
|
||||
<VirtualScroller
|
||||
items={characters}
|
||||
itemSize={48}
|
||||
itemTemplate={rowTemplate}
|
||||
className={`${classes.characterGridBody} h-72 w-full`}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
/* Character grid row styles */
|
||||
.characterGridRow {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: 0.5rem 0;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gridCellTrack,
|
||||
.gridCellFollow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gridCellCharacter {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Character info styles */
|
||||
.characterInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.characterDetails {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.characterName {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radioContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.portraitImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.corporationTicker {
|
||||
margin-left: 0.5rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.allianceTicker {
|
||||
margin-left: 0.25rem;
|
||||
color: var(--text-color-secondary, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 640px) {
|
||||
.characterGridRow {
|
||||
grid-template-columns: 60px 60px 1fr;
|
||||
}
|
||||
|
||||
.characterPortrait {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { TrackingCharacter } from './types';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
|
||||
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
|
||||
import classes from './TrackingCharacterWrapper.module.scss';
|
||||
|
||||
interface TrackingCharacterWrapperProps {
|
||||
character: TrackingCharacter;
|
||||
isTracked: boolean;
|
||||
isFollowed: boolean;
|
||||
onTrackToggle: () => void;
|
||||
onFollowToggle: () => void;
|
||||
}
|
||||
|
||||
export const TrackingCharacterWrapper = ({
|
||||
character,
|
||||
isTracked,
|
||||
isFollowed,
|
||||
onTrackToggle,
|
||||
onFollowToggle,
|
||||
}: TrackingCharacterWrapperProps) => {
|
||||
const trackCheckboxId = `track-${character.id}`;
|
||||
const followRadioId = `follow-${character.id}`;
|
||||
|
||||
return (
|
||||
<div className={classes.characterGridRow}>
|
||||
<div className={classes.gridCellTrack}>
|
||||
<Tooltip target={`#${trackCheckboxId}`} content="Track this character on the map" position="top" />
|
||||
<div className={classes.checkboxContainer}>
|
||||
<WdCheckbox label="" value={isTracked} onChange={() => onTrackToggle()} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.gridCellFollow}>
|
||||
<Tooltip target={`#${followRadioId}`} content="Follow this character's movements on the map" position="top" />
|
||||
<div className={classes.radioContainer}>
|
||||
<WdRadioButton
|
||||
id={followRadioId}
|
||||
name="followed_character"
|
||||
checked={isFollowed}
|
||||
onChange={() => onFollowToggle()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.gridCellCharacter}>
|
||||
<div className={classes.characterInfo}>
|
||||
<div className={classes.characterPortrait}>
|
||||
<img src={character.portrait_url} alt={character.name} className={classes.portraitImage} />
|
||||
</div>
|
||||
<div className={classes.characterDetails}>
|
||||
<span className={classes.characterName}>{character.name}</span>
|
||||
<span className={classes.corporationTicker}>[{character.corporation_ticker}]</span>
|
||||
{character.alliance_ticker && <span className={classes.allianceTicker}>[{character.alliance_ticker}]</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Interface for a character that can be tracked and followed
|
||||
*/
|
||||
export interface TrackingCharacter {
|
||||
id: string;
|
||||
name: string;
|
||||
corporation_ticker: string;
|
||||
alliance_ticker?: string;
|
||||
portrait_url: string;
|
||||
tracked: boolean;
|
||||
followed: boolean;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import type { ActivitySummary } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivity';
|
||||
|
||||
/**
|
||||
* Hook for character activity related handlers
|
||||
*/
|
||||
export const useCharacterActivityHandlers = () => {
|
||||
const { outCommand, update } = useMapRootState();
|
||||
|
||||
/**
|
||||
* Handle hiding the character activity dialog
|
||||
*/
|
||||
const handleHideCharacterActivity = useCallback(() => {
|
||||
// Update local state to hide the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showCharacterActivity: false,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.hideActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle showing the character activity dialog
|
||||
*/
|
||||
const handleShowActivity = useCallback(() => {
|
||||
// Update local state to show the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.showActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle updating character activity data
|
||||
*/
|
||||
const handleUpdateActivity = useCallback(
|
||||
(activityData: { activity: ActivitySummary[] }) => {
|
||||
if (!activityData || !activityData.activity) {
|
||||
console.error('Invalid activity data received:', activityData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state with the activity data
|
||||
update(state => ({
|
||||
...state,
|
||||
characterActivityData: activityData.activity,
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
return {
|
||||
handleHideCharacterActivity,
|
||||
handleShowActivity,
|
||||
handleUpdateActivity,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, CommandData, Commands } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import type { TrackingCharacter } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/types';
|
||||
|
||||
/**
|
||||
* Hook for track and follow related handlers
|
||||
*/
|
||||
export const useTrackAndFollowHandlers = () => {
|
||||
const { outCommand, update } = useMapRootState();
|
||||
|
||||
/**
|
||||
* Handle hiding the track and follow dialog
|
||||
*/
|
||||
const handleHideTracking = useCallback(() => {
|
||||
// Update local state to hide the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: false,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.hideTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle showing the track and follow dialog
|
||||
*/
|
||||
const handleShowTracking = useCallback(() => {
|
||||
// Update local state to show the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle updating tracking data
|
||||
*/
|
||||
const handleUpdateTracking = useCallback(
|
||||
(trackingData: { characters: TrackingCharacter[] }) => {
|
||||
if (!trackingData || !trackingData.characters) {
|
||||
console.error('Invalid tracking data received:', trackingData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state with the tracking data
|
||||
update(state => ({
|
||||
...state,
|
||||
trackingCharactersData: trackingData.characters,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle toggling character tracking
|
||||
*/
|
||||
const handleToggleTrack = useCallback(
|
||||
(characterId: string) => {
|
||||
if (!characterId) return;
|
||||
|
||||
// Send the toggle track command to the server
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
|
||||
// Note: The local state is now updated in the TrackAndFollow component
|
||||
// for immediate UI feedback, while we wait for the server response
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle toggling character following
|
||||
*/
|
||||
const handleToggleFollow = useCallback(
|
||||
(characterId: string) => {
|
||||
if (!characterId) return;
|
||||
|
||||
// Send the toggle follow command to the server
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
|
||||
// Note: The local state is now updated in the TrackAndFollow component
|
||||
// for immediate UI feedback, while we wait for the server response
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Handle user settings updates
|
||||
*/
|
||||
const handleUserSettingsUpdated = useCallback((settingsData: CommandData[Commands.userSettingsUpdated]) => {
|
||||
if (!settingsData || !settingsData.settings) {
|
||||
console.error('Invalid settings data received:', settingsData);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleHideTracking,
|
||||
handleShowTracking,
|
||||
handleUpdateTracking,
|
||||
handleToggleTrack,
|
||||
handleToggleFollow,
|
||||
handleUserSettingsUpdated,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user