refactor(frontend): remove requests image warnings
This commit is contained in:
parent
2cfa1099f3
commit
ba41dd2b0f
@ -162,6 +162,7 @@
|
||||
Файлы:
|
||||
|
||||
- `requests/RequestAttachmentPreviewModal.tsx`
|
||||
- `requests/RequestAvatar.tsx`
|
||||
- `requests/RequestComposeModal.tsx`
|
||||
- `requests/RequestDetailModal.tsx`
|
||||
- `requests/RequestListControls.tsx`
|
||||
@ -183,6 +184,7 @@
|
||||
- feature container для list shell, который собирает rows + empty state + load more
|
||||
- отдельный mode-panel для alternate flow вроде swipe-review
|
||||
- attachment preview modal как отдельный feature block, а не хвост page-level JSX
|
||||
- локальный avatar/media wrapper вокруг `next/image`, когда feature уже использует несколько однотипных аватаров и preview
|
||||
- participant badge pattern для request-like доменов
|
||||
- participant-only UI decisions
|
||||
- conditional action rendering по данным API
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
| Зона | Статус | Что уже хорошо | Основные проблемы | Рекомендуемый следующий шаг |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `requests` | `mixed` | compose-настроение, participant-only логика, UI вокруг `can_decide`, compose/list/detail/controls/preview разведены по feature components, screen orchestration вынесена в hook, list/swipe flow собраны в feature containers, dense row приведён к явной anatomy с отдельными detail/comments panels | visual debt сузился, но ещё остаются image warnings и контракт controls block | использовать как референс для разделения orchestration/UI, participant-heavy request flows и anatomy плотной строки, затем добить image/controls cleanup |
|
||||
| `requests` | `mixed` | compose-настроение, participant-only логика, UI вокруг `can_decide`, compose/list/detail/controls/preview разведены по feature components, screen orchestration вынесена в hook, list/swipe flow собраны в feature containers, dense row приведён к явной anatomy с отдельными detail/comments panels, avatar/preview media contract выровнен через local wrapper | основная незавершённость теперь в контракте controls block | использовать как референс для разделения orchestration/UI, participant-heavy request flows, anatomy плотной строки и локального media-wrapper паттерна, затем добить controls cleanup |
|
||||
| `settings` | `mixed` | сильная surface hierarchy, хорошие primary sections | встречаются custom radii и локальная геометрическая неоднородность | рефакторить карточки и кастомные радиусы без смены общего layout |
|
||||
| `documents` | `needs refactor` | богатый набор сценариев, detail/upload/comment patterns уже выделены в компоненты | смешение `rounded-md`, `rounded-lg`, `rounded-xl`, разные уровни visual density | сделать отдельный pass по карточкам и metadata/upload flows |
|
||||
| `equipment` | `needs refactor` | понятные формы и вторичные блоки | много локальных card/form решений, геометрия плавает | после `documents` пройтись по secondary cards и action forms |
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import Image from "next/image";
|
||||
import { Modal } from "@/components/ui";
|
||||
import type { RequestAttachmentPreview } from "@/hooks/useRequestsPage";
|
||||
import { resolveMediaUrl } from "@/lib/url";
|
||||
import { FileSignature } from "lucide-react";
|
||||
|
||||
type RequestAttachmentPreviewModalProps = {
|
||||
@ -14,6 +16,7 @@ export function RequestAttachmentPreviewModal({
|
||||
if (!preview) return null;
|
||||
|
||||
const extension = preview.url.split(".").pop()?.toLowerCase() || "";
|
||||
const previewUrl = resolveMediaUrl(preview.url);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -23,7 +26,7 @@ export function RequestAttachmentPreviewModal({
|
||||
size="lg"
|
||||
footer={(
|
||||
<a
|
||||
href={preview.url}
|
||||
href={previewUrl}
|
||||
download
|
||||
className="app-action-secondary rounded-lg px-3 py-1.5 text-xs font-medium"
|
||||
>
|
||||
@ -33,21 +36,26 @@ export function RequestAttachmentPreviewModal({
|
||||
>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].includes(extension) ? (
|
||||
<img
|
||||
src={preview.url}
|
||||
alt={preview.name}
|
||||
className="mx-auto max-h-[70vh] rounded-lg object-contain"
|
||||
/>
|
||||
<div className="relative mx-auto h-[70vh] overflow-hidden rounded-lg bg-[var(--surface-secondary)]">
|
||||
<Image
|
||||
src={previewUrl}
|
||||
alt={preview.name}
|
||||
fill
|
||||
unoptimized
|
||||
sizes="(max-width: 1024px) 100vw, 960px"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : ["mp4", "webm", "ogg", "mov"].includes(extension) ? (
|
||||
<video src={preview.url} controls className="mx-auto max-h-[70vh] rounded-lg" />
|
||||
<video src={previewUrl} controls className="mx-auto max-h-[70vh] rounded-lg" />
|
||||
) : ["mp3", "wav", "aac"].includes(extension) ? (
|
||||
<audio src={preview.url} controls className="mx-auto mt-8" />
|
||||
<audio src={previewUrl} controls className="mx-auto mt-8" />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 py-12 text-center">
|
||||
<FileSignature size={40} className="app-text-muted" />
|
||||
<p className="app-text-muted text-sm">{preview.name}</p>
|
||||
<a
|
||||
href={preview.url}
|
||||
href={previewUrl}
|
||||
download
|
||||
className="app-action-primary rounded-lg px-4 py-2 text-sm font-medium"
|
||||
>
|
||||
|
||||
48
frontend/src/components/requests/RequestAvatar.tsx
Normal file
48
frontend/src/components/requests/RequestAvatar.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import Image from "next/image";
|
||||
import { resolveMediaUrl } from "@/lib/url";
|
||||
|
||||
const avatarSizeMap = {
|
||||
sm: { className: "h-6 w-6 text-[11px]", px: 24 },
|
||||
md: { className: "h-7 w-7 text-xs", px: 28 },
|
||||
lg: { className: "h-8 w-8 text-xs", px: 32 },
|
||||
} as const;
|
||||
|
||||
type RequestAvatarProps = {
|
||||
alt: string;
|
||||
fallback: string;
|
||||
size?: keyof typeof avatarSizeMap;
|
||||
src?: string | null;
|
||||
};
|
||||
|
||||
export function RequestAvatar({
|
||||
alt,
|
||||
fallback,
|
||||
size = "md",
|
||||
src,
|
||||
}: RequestAvatarProps) {
|
||||
const config = avatarSizeMap[size];
|
||||
const resolvedSrc = resolveMediaUrl(src);
|
||||
|
||||
if (resolvedSrc) {
|
||||
return (
|
||||
<span className={`app-avatar-frame relative shrink-0 overflow-hidden rounded-full ${config.className}`}>
|
||||
<Image
|
||||
src={resolvedSrc}
|
||||
alt={alt}
|
||||
width={config.px}
|
||||
height={config.px}
|
||||
unoptimized
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`app-avatar-fallback flex shrink-0 items-center justify-center rounded-full font-semibold ${config.className}`}
|
||||
>
|
||||
{fallback}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { displayUserName, formatDate, formatDateTime, userProfileLink } from "@/
|
||||
import type { Request, RequestComment, User } from "@/types/api";
|
||||
import { Ban, ChevronDown, ChevronRight, MessageSquare, Paperclip, Pencil, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react";
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import { RequestAvatar } from "./RequestAvatar";
|
||||
import { RequestUserBadge } from "./RequestUserBadge";
|
||||
|
||||
type RequestListItemProps = {
|
||||
@ -137,6 +138,7 @@ export function RequestListItem({
|
||||
}: RequestListItemProps) {
|
||||
const requestAuthor = request.employee || request.created_by;
|
||||
const authorName = displayUserName(requestAuthor);
|
||||
const authorFallback = (requestAuthor?.first_name?.[0] || requestAuthor?.last_name?.[0] || "?").toUpperCase();
|
||||
const authorLink = requestAuthor ? userProfileLink(requestAuthor, currentUserId) : null;
|
||||
const statusKey = String(request.status || "").toLowerCase();
|
||||
const status = statusMeta[statusKey] ?? defaultStatusMeta;
|
||||
@ -204,24 +206,23 @@ export function RequestListItem({
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{authorLink ? (
|
||||
<Link href={authorLink} className="group flex min-w-0 items-center gap-2">
|
||||
{requestAuthor?.avatar ? (
|
||||
<img
|
||||
src={requestAuthor.avatar}
|
||||
alt={authorName}
|
||||
className="app-avatar-frame h-8 w-8 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="app-avatar-fallback flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
|
||||
{(requestAuthor?.first_name?.[0] || requestAuthor?.last_name?.[0] || "?").toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<RequestAvatar
|
||||
alt={authorName}
|
||||
fallback={authorFallback}
|
||||
size="lg"
|
||||
src={requestAuthor?.avatar}
|
||||
/>
|
||||
<span className="truncate text-sm font-medium text-[var(--foreground)] group-hover:text-[var(--accent-primary-strong)]">
|
||||
{authorName}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="app-badge flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold">?</span>
|
||||
<RequestAvatar
|
||||
alt={authorName}
|
||||
fallback={authorFallback}
|
||||
size="lg"
|
||||
/>
|
||||
<span className="truncate text-sm font-medium text-[var(--foreground)]">{authorName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { displayUserName, userProfileLink } from "@/lib/shared";
|
||||
import type { User } from "@/types/api";
|
||||
import { RequestAvatar } from "./RequestAvatar";
|
||||
|
||||
type RequestUserBadgeProps = {
|
||||
currentUserId?: number | null;
|
||||
@ -15,21 +16,15 @@ export function RequestUserBadge({
|
||||
}: RequestUserBadgeProps) {
|
||||
const personLink = userProfileLink(person, currentUserId);
|
||||
const personName = displayUserName(person);
|
||||
const fallback = (person.first_name?.[0] || person.last_name?.[0] || "?").toUpperCase();
|
||||
const chip = (
|
||||
<>
|
||||
{person.avatar ? (
|
||||
<img
|
||||
src={person.avatar}
|
||||
alt={personName}
|
||||
className={`app-avatar-frame ${large ? "h-7 w-7" : "h-6 w-6"} shrink-0 rounded-full object-cover`}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`app-avatar-fallback ${large ? "h-7 w-7 text-xs" : "h-6 w-6 text-[11px]"} flex shrink-0 items-center justify-center rounded-full font-semibold`}
|
||||
>
|
||||
{(person.first_name?.[0] || person.last_name?.[0] || "?").toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<RequestAvatar
|
||||
alt={personName}
|
||||
fallback={fallback}
|
||||
size={large ? "md" : "sm"}
|
||||
src={person.avatar}
|
||||
/>
|
||||
<span className="break-words">{personName}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user