refactor index.ts
This commit is contained in:
@@ -4,14 +4,13 @@ import { DrawingCard } from '../components/DrawingCard';
|
||||
import { Plus, Search, Loader2, Inbox, Trash2, Folder, ArrowRight, Copy, Upload, CheckSquare, Square, ArrowUp, ArrowDown, ChevronDown, FileText, Calendar, Clock } from 'lucide-react';
|
||||
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom';
|
||||
import * as api from '../api';
|
||||
import type { DrawingSummary, Collection } from '../types';
|
||||
import type { DrawingSortField, SortDirection } from '../api';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
import clsx from 'clsx';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import { useUpload } from '../context/UploadContext';
|
||||
import { DragOverlayPortal, getSelectionBounds, type Point, type SelectionBounds } from './dashboard/shared';
|
||||
import { isLatestRequest, mergeUniqueDrawings } from './dashboard/pagination';
|
||||
import { useDashboardData } from './dashboard/useDashboardData';
|
||||
|
||||
const PAGE_SIZE = 24;
|
||||
|
||||
@@ -19,10 +18,6 @@ export const Dashboard: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [drawings, setDrawings] = useState<DrawingSummary[]>([]);
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
||||
|
||||
const selectedCollectionId = React.useMemo(() => {
|
||||
if (location.pathname === '/') return undefined;
|
||||
@@ -73,73 +68,29 @@ export const Dashboard: React.FC = () => {
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const listRequestVersionRef = useRef(0);
|
||||
|
||||
const { uploadFiles } = useUpload();
|
||||
|
||||
const hasMore = drawings.length < totalCount;
|
||||
|
||||
const refreshData = useCallback(async () => {
|
||||
const requestVersion = ++listRequestVersionRef.current;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [drawingsRes, collectionsData] = await Promise.all([
|
||||
api.getDrawings(debouncedSearch, selectedCollectionId, {
|
||||
limit: PAGE_SIZE,
|
||||
offset: 0,
|
||||
sortField: sortConfig.field,
|
||||
sortDirection: sortConfig.direction,
|
||||
}),
|
||||
api.getCollections()
|
||||
]);
|
||||
if (!isLatestRequest(requestVersion, listRequestVersionRef.current)) return;
|
||||
setDrawings(drawingsRes.drawings);
|
||||
setTotalCount(drawingsRes.totalCount);
|
||||
setCollections(collectionsData);
|
||||
setSelectedIds(new Set());
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch data:', err);
|
||||
} finally {
|
||||
if (isLatestRequest(requestVersion, listRequestVersionRef.current)) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [debouncedSearch, selectedCollectionId, sortConfig.field, sortConfig.direction]);
|
||||
|
||||
const fetchMore = useCallback(async () => {
|
||||
if (isFetchingMore || !hasMore || isLoading) return;
|
||||
const requestVersion = listRequestVersionRef.current;
|
||||
setIsFetchingMore(true);
|
||||
try {
|
||||
const drawingsRes = await api.getDrawings(debouncedSearch, selectedCollectionId, {
|
||||
limit: PAGE_SIZE,
|
||||
offset: drawings.length,
|
||||
sortField: sortConfig.field,
|
||||
sortDirection: sortConfig.direction,
|
||||
});
|
||||
if (!isLatestRequest(requestVersion, listRequestVersionRef.current)) return;
|
||||
setDrawings(prev => mergeUniqueDrawings(prev, drawingsRes.drawings));
|
||||
setTotalCount(drawingsRes.totalCount);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch more data:', err);
|
||||
} finally {
|
||||
setIsFetchingMore(false);
|
||||
}
|
||||
}, [
|
||||
const resetSelection = useCallback(() => {
|
||||
setSelectedIds(new Set());
|
||||
}, []);
|
||||
const {
|
||||
drawings,
|
||||
setDrawings,
|
||||
collections,
|
||||
setCollections,
|
||||
setTotalCount,
|
||||
isFetchingMore,
|
||||
hasMore,
|
||||
isLoading,
|
||||
hasMore,
|
||||
refreshData,
|
||||
fetchMore,
|
||||
} = useDashboardData({
|
||||
debouncedSearch,
|
||||
selectedCollectionId,
|
||||
drawings.length,
|
||||
sortConfig.field,
|
||||
sortConfig.direction,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, [refreshData]);
|
||||
sortField: sortConfig.field,
|
||||
sortDirection: sortConfig.direction,
|
||||
pageSize: PAGE_SIZE,
|
||||
onRefreshSuccess: resetSelection,
|
||||
});
|
||||
|
||||
// Infinite scroll observer
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import debounce from 'lodash/debounce';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { Toaster, toast } from 'sonner';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { getUserIdentity, type UserIdentity } from '../utils/identity';
|
||||
import type { UserIdentity } from '../utils/identity';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { reconcileElements } from '../utils/sync';
|
||||
import { exportFromEditor } from '../utils/exportUtils';
|
||||
@@ -15,9 +15,7 @@ import * as api from '../api';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import {
|
||||
UIOptions,
|
||||
getColorFromString,
|
||||
getFilesDelta,
|
||||
getInitialsFromName,
|
||||
hasRenderableElements,
|
||||
haveSameElements,
|
||||
isSuspiciousEmptySnapshot,
|
||||
@@ -25,6 +23,8 @@ import {
|
||||
isStaleNonRenderableSnapshot,
|
||||
} from './editor/shared';
|
||||
import type { ElementVersionInfo } from './editor/shared';
|
||||
import { useEditorChrome } from './editor/useEditorChrome';
|
||||
import { useEditorIdentity } from './editor/useEditorIdentity';
|
||||
|
||||
interface Peer extends UserIdentity {
|
||||
isActive: boolean;
|
||||
@@ -49,75 +49,13 @@ export const Editor: React.FC = () => {
|
||||
const [isSceneLoading, setIsSceneLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [isSavingOnLeave, setIsSavingOnLeave] = useState(false);
|
||||
const [isHeaderVisible, setIsHeaderVisible] = useState(true);
|
||||
const [autoHideEnabled, setAutoHideEnabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${drawingName} - ExcaliDash`;
|
||||
return () => {
|
||||
document.title = 'ExcaliDash';
|
||||
};
|
||||
}, [drawingName]);
|
||||
|
||||
// Auto-hide header based on mouse movement
|
||||
useEffect(() => {
|
||||
if (!autoHideEnabled || isRenaming) {
|
||||
setIsHeaderVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let isInTriggerZone = false;
|
||||
|
||||
const handleMouseMove = throttle((e: MouseEvent) => {
|
||||
const wasInTriggerZone = isInTriggerZone;
|
||||
isInTriggerZone = e.clientY < 5;
|
||||
|
||||
if (isInTriggerZone) {
|
||||
// Mouse is in trigger zone - show header
|
||||
setIsHeaderVisible(true);
|
||||
if (hideTimeout !== null) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = null;
|
||||
}
|
||||
} else if (wasInTriggerZone) {
|
||||
// Mouse just left trigger zone - start hide timer
|
||||
if (hideTimeout !== null) clearTimeout(hideTimeout);
|
||||
hideTimeout = setTimeout(() => {
|
||||
setIsHeaderVisible(false);
|
||||
}, 2000);
|
||||
}
|
||||
// If mouse is already out of trigger zone and moving, don't reset timer
|
||||
}, 100);
|
||||
|
||||
// Show header initially
|
||||
setIsHeaderVisible(true);
|
||||
|
||||
// Hide after initial delay if mouse doesn't move to top
|
||||
hideTimeout = setTimeout(() => {
|
||||
setIsHeaderVisible(false);
|
||||
}, 3000);
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
if (hideTimeout !== null) clearTimeout(hideTimeout);
|
||||
};
|
||||
}, [autoHideEnabled, isRenaming]);
|
||||
|
||||
// Use authenticated user identity or fallback to generated identity
|
||||
const [me] = useState<UserIdentity>(() => {
|
||||
if (user) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
initials: getInitialsFromName(user.name),
|
||||
color: getColorFromString(user.id),
|
||||
};
|
||||
}
|
||||
return getUserIdentity();
|
||||
const { isHeaderVisible, setIsHeaderVisible } = useEditorChrome({
|
||||
drawingName,
|
||||
autoHideEnabled,
|
||||
isRenaming,
|
||||
});
|
||||
const me: UserIdentity = useEditorIdentity(user);
|
||||
|
||||
const [peers, setPeers] = useState<Peer[]>([]);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams, useNavigate, Link } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { Logo } from '../components/Logo';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || "/api";
|
||||
import { authPasswordResetConfirm, isAxiosError } from '../api';
|
||||
|
||||
export const PasswordResetConfirm: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -44,17 +42,14 @@ export const PasswordResetConfirm: React.FC = () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await axios.post(`${API_URL}/auth/password-reset-confirm`, {
|
||||
token,
|
||||
password,
|
||||
});
|
||||
await authPasswordResetConfirm(token, password);
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
navigate('/login');
|
||||
}, 3000);
|
||||
} catch (err: unknown) {
|
||||
let message = 'Failed to reset password';
|
||||
if (axios.isAxiosError(err)) {
|
||||
if (isAxiosError(err)) {
|
||||
if (err.response?.status === 404) {
|
||||
message = 'Password reset feature is not enabled on this server';
|
||||
} else if (err.response?.data?.message) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as api from '../../api';
|
||||
import type { DrawingSortField, SortDirection } from '../../api';
|
||||
import type { Collection, DrawingSummary } from '../../types';
|
||||
import { isLatestRequest, mergeUniqueDrawings } from './pagination';
|
||||
|
||||
type SelectedCollectionId = string | null | undefined;
|
||||
|
||||
type UseDashboardDataOptions = {
|
||||
debouncedSearch: string;
|
||||
selectedCollectionId: SelectedCollectionId;
|
||||
sortField: DrawingSortField;
|
||||
sortDirection: SortDirection;
|
||||
pageSize: number;
|
||||
onRefreshSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const useDashboardData = ({
|
||||
debouncedSearch,
|
||||
selectedCollectionId,
|
||||
sortField,
|
||||
sortDirection,
|
||||
pageSize,
|
||||
onRefreshSuccess,
|
||||
}: UseDashboardDataOptions) => {
|
||||
const [drawings, setDrawings] = useState<DrawingSummary[]>([]);
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const listRequestVersionRef = useRef(0);
|
||||
|
||||
const hasMore = drawings.length < totalCount;
|
||||
|
||||
const refreshData = useCallback(async () => {
|
||||
const requestVersion = ++listRequestVersionRef.current;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [drawingsRes, collectionsData] = await Promise.all([
|
||||
api.getDrawings(debouncedSearch, selectedCollectionId, {
|
||||
limit: pageSize,
|
||||
offset: 0,
|
||||
sortField,
|
||||
sortDirection,
|
||||
}),
|
||||
api.getCollections(),
|
||||
]);
|
||||
if (!isLatestRequest(requestVersion, listRequestVersionRef.current)) return;
|
||||
setDrawings(drawingsRes.drawings);
|
||||
setTotalCount(drawingsRes.totalCount);
|
||||
setCollections(collectionsData);
|
||||
onRefreshSuccess?.();
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch data:', err);
|
||||
} finally {
|
||||
if (isLatestRequest(requestVersion, listRequestVersionRef.current)) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
debouncedSearch,
|
||||
selectedCollectionId,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
onRefreshSuccess,
|
||||
]);
|
||||
|
||||
const fetchMore = useCallback(async () => {
|
||||
if (isFetchingMore || !hasMore || isLoading) return;
|
||||
const requestVersion = listRequestVersionRef.current;
|
||||
setIsFetchingMore(true);
|
||||
try {
|
||||
const drawingsRes = await api.getDrawings(debouncedSearch, selectedCollectionId, {
|
||||
limit: pageSize,
|
||||
offset: drawings.length,
|
||||
sortField,
|
||||
sortDirection,
|
||||
});
|
||||
if (!isLatestRequest(requestVersion, listRequestVersionRef.current)) return;
|
||||
setDrawings((prev) => mergeUniqueDrawings(prev, drawingsRes.drawings));
|
||||
setTotalCount(drawingsRes.totalCount);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch more data:', err);
|
||||
} finally {
|
||||
setIsFetchingMore(false);
|
||||
}
|
||||
}, [
|
||||
isFetchingMore,
|
||||
hasMore,
|
||||
isLoading,
|
||||
debouncedSearch,
|
||||
selectedCollectionId,
|
||||
pageSize,
|
||||
drawings.length,
|
||||
sortField,
|
||||
sortDirection,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, [refreshData]);
|
||||
|
||||
return {
|
||||
drawings,
|
||||
setDrawings,
|
||||
collections,
|
||||
setCollections,
|
||||
totalCount,
|
||||
setTotalCount,
|
||||
isFetchingMore,
|
||||
isLoading,
|
||||
hasMore,
|
||||
refreshData,
|
||||
fetchMore,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
type UseEditorChromeOptions = {
|
||||
drawingName: string;
|
||||
autoHideEnabled: boolean;
|
||||
isRenaming: boolean;
|
||||
};
|
||||
|
||||
export const useEditorChrome = ({
|
||||
drawingName,
|
||||
autoHideEnabled,
|
||||
isRenaming,
|
||||
}: UseEditorChromeOptions) => {
|
||||
const [isHeaderVisible, setIsHeaderVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${drawingName} - ExcaliDash`;
|
||||
return () => {
|
||||
document.title = 'ExcaliDash';
|
||||
};
|
||||
}, [drawingName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoHideEnabled || isRenaming) {
|
||||
setIsHeaderVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let isInTriggerZone = false;
|
||||
|
||||
const handleMouseMove = throttle((e: MouseEvent) => {
|
||||
const wasInTriggerZone = isInTriggerZone;
|
||||
isInTriggerZone = e.clientY < 5;
|
||||
|
||||
if (isInTriggerZone) {
|
||||
setIsHeaderVisible(true);
|
||||
if (hideTimeout !== null) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = null;
|
||||
}
|
||||
} else if (wasInTriggerZone) {
|
||||
if (hideTimeout !== null) clearTimeout(hideTimeout);
|
||||
hideTimeout = setTimeout(() => {
|
||||
setIsHeaderVisible(false);
|
||||
}, 2000);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
setIsHeaderVisible(true);
|
||||
hideTimeout = setTimeout(() => {
|
||||
setIsHeaderVisible(false);
|
||||
}, 3000);
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
if (hideTimeout !== null) clearTimeout(hideTimeout);
|
||||
};
|
||||
}, [autoHideEnabled, isRenaming]);
|
||||
|
||||
return {
|
||||
isHeaderVisible,
|
||||
setIsHeaderVisible,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getUserIdentity, type UserIdentity } from '../../utils/identity';
|
||||
import {
|
||||
getColorFromString,
|
||||
getInitialsFromName,
|
||||
} from './shared';
|
||||
|
||||
type AuthUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null | undefined;
|
||||
|
||||
export const useEditorIdentity = (user: AuthUser): UserIdentity => {
|
||||
return useMemo(() => {
|
||||
if (user) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
initials: getInitialsFromName(user.name),
|
||||
color: getColorFromString(user.id),
|
||||
};
|
||||
}
|
||||
return getUserIdentity();
|
||||
}, [user]);
|
||||
};
|
||||
Reference in New Issue
Block a user