Avatar image loading
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-25 20:38:17 -05:00
parent e4af76cfd4
commit e5277c05a1
6 changed files with 157 additions and 63 deletions

View File

@@ -2,11 +2,13 @@ import {
Box,
Avatar,
Typography,
Tooltip
Tooltip,
Skeleton
} from "@mui/material";
import PersonIcon from '@mui/icons-material/Person';
import { formatDistanceToNow, format } from "date-fns";
import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent";
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
interface AuditEventItemProps {
event: AuditEvent;
@@ -14,17 +16,39 @@ interface AuditEventItemProps {
}
export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) {
const isValidator = event.User === validatorUser;
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
return (
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
>
<PersonIcon />
</Avatar>
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
<Skeleton
variant="circular"
sx={{
position: 'absolute',
width: 40,
height: 40,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={isValidator ? undefined : (thumbnailUrl || undefined)}
sx={{
width: 40,
height: 40,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
>
<PersonIcon />
</Avatar>
</Box>
<Box sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="subtitle2">
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
{isValidator ? "Validator" : event.Username || "Unknown"}
</Typography>
<DateDisplay date={event.Date} />
</Box>

View File

@@ -21,18 +21,21 @@ export default function AuditEventsTabPanel({
);
return (
<Box role="tabpanel" hidden={activeTab !== 1}>
{activeTab === 1 && (
<Stack spacing={2}>
{filteredEvents.map((event, index) => (
<AuditEventItem
key={index}
event={event}
validatorUser={validatorUser}
/>
))}
</Stack>
)}
<Box
role="tabpanel"
sx={{
display: activeTab === 1 ? 'block' : 'none'
}}
>
<Stack spacing={2}>
{filteredEvents.map((event, index) => (
<AuditEventItem
key={index}
event={event}
validatorUser={validatorUser}
/>
))}
</Stack>
</Box>
);
}

View File

@@ -2,11 +2,13 @@ import {
Box,
Avatar,
Typography,
Tooltip
Tooltip,
Skeleton
} from "@mui/material";
import PersonIcon from '@mui/icons-material/Person';
import { formatDistanceToNow, format } from "date-fns";
import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent";
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
interface CommentItemProps {
event: AuditEvent;
@@ -14,17 +16,39 @@ interface CommentItemProps {
}
export default function CommentItem({ event, validatorUser }: CommentItemProps) {
const isValidator = event.User === validatorUser;
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
return (
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
>
<PersonIcon />
</Avatar>
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
<Skeleton
variant="circular"
sx={{
position: 'absolute',
width: 40,
height: 40,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={isValidator ? undefined : (thumbnailUrl || undefined)}
sx={{
width: 40,
height: 40,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
>
<PersonIcon />
</Avatar>
</Box>
<Box sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="subtitle2">
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
{isValidator ? "Validator" : event.Username || "Unknown"}
</Typography>
<DateDisplay date={event.Date} />
</Box>

View File

@@ -3,11 +3,13 @@ import {
Stack,
Avatar,
TextField,
IconButton
IconButton,
Skeleton
} from "@mui/material";
import SendIcon from '@mui/icons-material/Send';
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
import CommentItem from './CommentItem';
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
interface CommentsTabPanelProps {
activeTab: number;
@@ -33,34 +35,35 @@ export default function CommentsTabPanel({
);
return (
<Box role="tabpanel" hidden={activeTab !== 0}>
{activeTab === 0 && (
<>
<Stack spacing={2} sx={{ mb: 3 }}>
{commentEvents.length > 0 ? (
commentEvents.map((event, index) => (
<CommentItem
key={index}
event={event}
validatorUser={validatorUser}
/>
))
) : (
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
No Comments
</Box>
)}
</Stack>
{userId !== null && (
<CommentInput
newComment={newComment}
setNewComment={setNewComment}
handleCommentSubmit={handleCommentSubmit}
userId={userId}
<Box
role="tabpanel"
sx={{
display: activeTab === 0 ? 'block' : 'none'
}}
>
<Stack spacing={2} sx={{ mb: 3 }}>
{commentEvents.length > 0 ? (
commentEvents.map((event, index) => (
<CommentItem
key={index}
event={event}
validatorUser={validatorUser}
/>
)}
</>
))
) : (
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
No Comments
</Box>
)}
</Stack>
{userId !== null && (
<CommentInput
newComment={newComment}
setNewComment={setNewComment}
handleCommentSubmit={handleCommentSubmit}
userId={userId}
/>
)}
</Box>
);
@@ -74,11 +77,32 @@ interface CommentInputProps {
}
function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) {
const { thumbnailUrl, isLoading } = useUserThumbnail(userId || undefined, '150x150');
return (
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
<Avatar
src={`/thumbnails/user/${userId}`}
/>
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
<Skeleton
variant="circular"
sx={{
position: 'absolute',
width: 40,
height: 40,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={thumbnailUrl || undefined}
sx={{
width: 40,
height: 40,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</Box>
<TextField
fullWidth
multiline

View File

@@ -1,4 +1,4 @@
import {Typography, Box, Avatar, keyframes} from "@mui/material";
import {Typography, Box, Avatar, keyframes, Skeleton} from "@mui/material";
import { StatusChip } from "@/app/_components/statusChip";
import { SubmissionStatus } from "@/app/ts/Submission";
import { MapfixStatus } from "@/app/ts/Mapfix";
@@ -6,6 +6,7 @@ import {Status, StatusMatches} from "@/app/ts/Status";
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import LaunchIcon from '@mui/icons-material/Launch';
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
function SubmitterName({ submitterId }: { submitterId: number }) {
const [name, setName] = useState<string | null>(null);
@@ -50,6 +51,7 @@ interface ReviewItemHeaderProps {
export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]);
const { thumbnailUrl, isLoading } = useUserThumbnail(submitterId, '150x150');
const pulse = keyframes`
0%, 100% { opacity: 0.2; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1); }
@@ -111,10 +113,28 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Avatar
src={`/thumbnails/user/${submitterId}`}
sx={{ mr: 1, width: 24, height: 24 }}
/>
<Box sx={{ position: 'relative', mr: 1, width: 24, height: 24 }}>
<Skeleton
variant="circular"
sx={{
position: 'absolute',
width: 24,
height: 24,
opacity: isLoading ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
animation="wave"
/>
<Avatar
src={thumbnailUrl || undefined}
sx={{
width: 24,
height: 24,
opacity: isLoading ? 0 : 1,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</Box>
<SubmitterName submitterId={submitterId} />
</Box>
</>

View File

@@ -15,7 +15,6 @@ export default defineConfig({
'/v1': {
target: process.env.VITE_API_HOST || 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/v1/, ''),
},
},
},