matches: add animation, add suspense for table, add no-image image

This commit is contained in:
smanylov
2026-04-08 13:41:56 +07:00
parent eb2615c303
commit 0c3ccb2806
10 changed files with 267 additions and 26 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+2 -2
View File
@@ -96,11 +96,11 @@ export const createCaseSchema = z
.object({
textArea: z
.string()
.min(6, { error: 'text-area-error-fill-complaint-text' })
.min(6, { error: 'fill-text-of-the-case' })
.trim(),
caseTitle: z
.string()
.min(6, { error: 'text-area-error-fill-complaint-text' })
.min(6, { error: 'fill-title-of-the-case' })
.trim(),
})
+19 -2
View File
@@ -4457,9 +4457,10 @@
padding: 15px;
height: calc(16vh - 12px);
cursor: pointer;
display: flex;
justify-content: space-between;
opacity: 0.75;
display: flex;
gap: 10px;
&:hover {
transform: translateY(-2px);
@@ -4497,10 +4498,12 @@
max-height: 80%;
min-height: 50px;
height: 100%;
position: relative;
img {
height: 100%;
height: 100% !important;
border-radius: 8px;
width: auto !important;
}
}
@@ -4662,6 +4665,7 @@
&-image {
width: 300px;
height: 200px;
position: relative;
img {
width: 100%;
@@ -4727,6 +4731,7 @@
&-case {
padding: 10px;
position: relative;
&:first-child {
border-bottom: 1px solid rgb(226, 232, 240);
@@ -4763,6 +4768,12 @@
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
&-wrapper {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
}
.note-form-flex {
@@ -4789,6 +4800,10 @@
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
&::placeholder {
font-size: 12px;
}
}
.note-case-label {
@@ -5389,6 +5404,7 @@
border-radius: 10px;
box-shadow: 0 1px 2px #0000000d;
color: v.$p-color;
cursor: pointer;
.icon {
width: 18px;
@@ -5414,6 +5430,7 @@
padding: 0.25rem 0.75rem;
border-radius: 10px;
color: v.$p-color;
cursor: pointer;
&:disabled {
opacity: 0.5;
@@ -143,7 +143,14 @@ export default function CaseComplaint({ selectedViolation, updateStatusHandler }
if (isLoadingComplain) {
return (
<div className="violation-info-case">
<div>{t('loading')}...</div>
<div
className="loading-animation"
>
<div
className="global-spinner large"
>
</div>
</div>
</div>
);
}
@@ -86,7 +86,7 @@ export default function CaseViolation({ selectedViolation, updateStatusHandler }
className="btn-action btn-case"
onClick={toggleForm}
>
Создание дела
{t('create-case')}
</button>
) : (
<div>
@@ -111,12 +111,13 @@ export default function CaseViolation({ selectedViolation, updateStatusHandler }
className="note-case-label"
htmlFor="caseTitle"
>
Название дела
{t('case-name')}
</label>
<input
type="text"
name="caseTitle"
className="note-case-input"
placeholder={t('fill-title-of-the-case') + '...'}
/>
{state?.errorMessage?.caseTitle && (
<p
@@ -142,12 +143,13 @@ export default function CaseViolation({ selectedViolation, updateStatusHandler }
className="note-case-label"
htmlFor="amount"
>
Сумма ущерба
{t('amount-of-damage')}
</label>
<input
type="number"
name="amount"
className="note-case-input"
placeholder={t('enter-the-amount-of-damage') + '...'}
/>
{state?.errorMessage?.amount && (
<p
@@ -168,11 +170,14 @@ export default function CaseViolation({ selectedViolation, updateStatusHandler }
</section>
</div>
<section>
<section
className="note-textarea-wrapper"
>
<textarea
name="note"
className="note-textarea"
placeholder={`Текст дела...`}>
placeholder={t('fill-text-of-the-case') + '...'}
>
</textarea>
{state?.errorMessage?.textArea && (
<p
@@ -212,7 +217,15 @@ export default function CaseViolation({ selectedViolation, updateStatusHandler }
if (isLoadingCase) {
return (
<div className="violation-info-case">
<div>{t('loading')}...</div>
<div
className="loading-animation"
>
<div
className="global-spinner large"
>
</div>
</div>
</div>
);
}
@@ -12,6 +12,7 @@ import { useCallback, useEffect, useState } from 'react';
import { MatchStatus } from '@/app/actions/violationActions';
import { toast } from 'sonner';
import FilePageViolationEpmtyScreen from '@/app/ui/file-page/violation-table/file-page-violation-epmty-screen';
import Image from 'next/image';
export default function FilePageViolationInfo({ selectedViolation }: { selectedViolation: ViolationFileDetail | null }) {
const t = useTranslations('Global');
@@ -88,7 +89,17 @@ export default function FilePageViolationInfo({ selectedViolation }: { selectedV
<div
className="violation-info-image"
>
<img src={selectedViolation?.url} alt={selectedViolation?.page_title} />
<Image
src={selectedViolation?.url}
alt={selectedViolation?.page_title}
unoptimized
fill
sizes="100px"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = '/images/no-image.png';
}}
/>
</div>
<div
@@ -3,12 +3,14 @@
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import FilePageViolationInfo from '@/app/ui/file-page/violation-table/file-page-violation-info';
import { useFileViolations } from '@/app/hooks/react-query/useFileViolations';
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
import DropDownList from '@/app/components/DropDownList';
import { IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons';
import FilePageViolationsLoadingList from '@/app/ui/file-page/violation-table/file-page-violations-loading-list';
import Image from 'next/image';
export default function FilePageViolationsList({
currentPage,
@@ -26,12 +28,36 @@ export default function FilePageViolationsList({
const searchParams = useSearchParams();
const [selectedViolation, setSelectedViolation] = useState<ViolationFileDetail | null>(null);
const [cachedPagination, setCachedPagination] = useState<{
total_pages: number;
total_elements: number;
current_page: number;
} | null>(null);
const { data: fileViolations, error, isPending } = useFileViolations(fileId, currentPage, status);
if (!fileViolations?.violations) {
return null;
}
useEffect(() => {
if (fileViolations?.violations) {
setCachedPagination({
total_pages: fileViolations.total_pages,
total_elements: fileViolations.total_elements,
current_page: fileViolations.current_page
});
}
}, [fileViolations]);
const paginationData = fileViolations?.violations ? fileViolations : cachedPagination;
if (!fileViolations?.violations) {
return <FilePageViolationsLoadingList
currentPage={currentPage}
fileId={fileId}
status={status}
cachedTotalPages={paginationData?.total_pages}
cachedTotalElements={paginationData?.total_elements}
cachedCurrentPage={paginationData?.current_page}
/>;
}
const handlePageChange = (page: number) => {
const params = new URLSearchParams(searchParams);
params.set('page', page.toString());
@@ -105,7 +131,7 @@ export default function FilePageViolationsList({
className="sources-list-wrapper"
>
<div className="sources-list">
{fileViolations.violations.map(item => {
{fileViolations?.violations.map(item => {
return (
<div
className={`source-card ${selectedViolation?.id === item.id ? 'selected' : ''}`}
@@ -118,7 +144,17 @@ export default function FilePageViolationsList({
className="source-header-left"
>
<div className="source-image">
<img src={item.url} alt="" />
<Image
src={item.url}
alt={item.page_title}
unoptimized
fill
sizes="100px"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = '/images/no-image.png';
}}
/>
</div>
<div
className="source-url-block"
@@ -177,9 +213,9 @@ export default function FilePageViolationsList({
className="sources-list-pagination"
>
<span>
{t('total-violations')}: {fileViolations.total_elements} | {t('page')} {fileViolations.current_page} {t('out-of')} {fileViolations.total_pages}
{t('total-violations')}: {fileViolations?.total_elements} | {t('page')} {fileViolations?.current_page} {t('out-of')} {fileViolations?.total_pages}
</span>
{fileViolations.total_pages > 1 && (
{fileViolations?.total_pages > 1 && (
<div className="pagination-controls">
<button
className="arrow"
@@ -191,7 +227,7 @@ export default function FilePageViolationsList({
<button
onClick={() => handlePageChange(currentPage - 1)}
className="arrow"
disabled={!fileViolations.has_previous || isPending}
disabled={!fileViolations?.has_previous || isPending}
>
<IconArrowLeft />
</button>
@@ -215,14 +251,14 @@ export default function FilePageViolationsList({
<button
onClick={() => handlePageChange(currentPage + 1)}
className="arrow"
disabled={!fileViolations.has_next}
disabled={!fileViolations?.has_next}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => handlePageChange(fileViolations.total_pages)}
disabled={currentPage === fileViolations.total_pages || isPending}
onClick={() => handlePageChange(fileViolations?.total_pages)}
disabled={currentPage === fileViolations?.total_pages || isPending}
>
<IconDoubleArrowRight />
</button>
@@ -0,0 +1,143 @@
'use client'
import Link from 'next/link';
import FilePageViolationInfo from '@/app/ui/file-page/violation-table/file-page-violation-info';
import DropDownList from '@/app/components/DropDownList';
import { IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons';
import { useTranslations } from 'next-intl';
export default function FilePageViolationsLoadingList({
currentPage,
fileId,
status,
cachedTotalPages,
cachedTotalElements,
cachedCurrentPage,
}: {
currentPage: number,
fileId: string,
status: string | undefined,
cachedTotalPages: number | undefined,
cachedTotalElements: number | undefined,
cachedCurrentPage: number | undefined
}) {
const t = useTranslations('Global');
const tStatus = useTranslations('Match-status');
const getPageNumbers = () => {
const pages = [];
const maxVisible = 5;
const total = cachedTotalPages;
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let end = Math.min(total ? total : 0, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
};
return (
<div className="block-wrapper">
<div className="card-header">
<h3 className="card-title">
{t('all-file-violations')}
</h3>
<div className="flex items-center gap-4">
<div
className="status-filter-select"
>
<DropDownList
value={status ? tStatus(status) : t('all-statuses')}
callBack={() => { }}
>
<li value=''>{t('all-statuses')}</li>
<li value="NEW">{tStatus('NEW')}</li>
<li value="SHOWED">{tStatus('SHOWED')}</li>
<li value="LEGAL_IN_WORK">{tStatus('LEGAL_IN_WORK')}</li>
<li value="COMPLAINT_IN_WORK">{tStatus('COMPLAINT_IN_WORK')}</li>
<li value="COMPLAINT_AND_LEGAL_IN_WORK">{tStatus('COMPLAINT_AND_LEGAL_IN_WORK')}</li>
<li value="AUTHORIZED_USE">{tStatus('AUTHORIZED_USE')}</li>
</DropDownList>
</div>
</div>
</div>
<div
className="sources-list-wrapper relative"
>
<div
className="loading-animation"
>
<div
className="global-spinner large"
>
</div>
</div>
</div>
<div
className="sources-list-pagination"
>
<span>
{t('total-violations')}: {cachedTotalElements} | {t('page')} {cachedCurrentPage} {t('out-of')} {cachedTotalPages}
</span>
<div className="pagination-controls">
<button
className="arrow"
onClick={() => { }}
disabled={true}
>
<IconDoubleArrowLeft />
</button>
<button
onClick={() => { }}
className="arrow"
disabled={true}
>
<IconArrowLeft />
</button>
<div
className="pagination-controls-pages"
>
{getPageNumbers().map(pageNum => (
<button
key={pageNum}
onClick={() => { }}
className={currentPage === pageNum ? 'current' : 'other'}
disabled={true}
>
{pageNum}
</button>
))}
</div>
<button
onClick={() => { }}
className="arrow"
disabled={true}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => { }}
disabled={true}
>
<IconDoubleArrowRight />
</button>
</div>
</div>
</div>
)
}
+8 -1
View File
@@ -407,7 +407,14 @@
"priority": "Priority",
"type": "Type",
"lawyer": "Lawyer",
"not-assigned": "Not assigned"
"not-assigned": "Not assigned",
"create-case": "Create case",
"case-name": "Case name",
"amount-of-damage": "Amount of damage",
"text-of-the-case": "Text of the case",
"fill-text-of-the-case": "Fill the case text",
"fill-title-of-the-case": "Fill the case title",
"enter-the-amount-of-damage": "Enter the amount of damage"
},
"Login-register-form": {
"and": "and",
+8 -1
View File
@@ -407,7 +407,14 @@
"priority": "Приоритет",
"type": "Тип",
"lawyer": "Адвокат",
"not-assigned": "Не назначено"
"not-assigned": "Не назначено",
"create-case": "Создать дело",
"case-name": "Название дела",
"amount-of-damage": "Сумма ущерба",
"text-of-the-case": "Текст дела",
"fill-text-of-the-case": "Заполните текст дела",
"fill-title-of-the-case": "Заполните название дела",
"enter-the-amount-of-damage": "Введите сумму ущерба"
},
"Login-register-form": {
"and": "и",