ae08b946f6
- Updated TendersRepository and TendersService to support cursor-based pagination. - Modified TendersViewModel to manage pagination state and handle API responses more effectively. - Replaced existing pagination UI components with a new WindowedPagination widget across desktop, mobile, and tablet views. - Improved notification handling in the UI to reflect the presence of notifications dynamically. - Adjusted main tenders slider to ensure proper rendering based on the current page index.
251 lines
6.7 KiB
Dart
251 lines
6.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:tm_app/core/theme/colors.dart';
|
|
|
|
/// A windowed numbered pagination widget that mirrors the Next.js
|
|
/// implementation: ±10 page-button window around the current page,
|
|
/// 1-based labels, prev/next chevrons.
|
|
class WindowedPagination extends StatelessWidget {
|
|
const WindowedPagination({
|
|
super.key,
|
|
required this.currentPage,
|
|
required this.totalPages,
|
|
required this.onPageChanged,
|
|
this.maxPageJump = 10,
|
|
this.compact = false,
|
|
});
|
|
|
|
/// 0-based absolute current page index.
|
|
final int currentPage;
|
|
|
|
/// Total number of pages (1-based count).
|
|
final int totalPages;
|
|
|
|
/// Called with the 0-based target page index.
|
|
final ValueChanged<int> onPageChanged;
|
|
|
|
/// Half-width of the page-button window (web uses 10).
|
|
final int maxPageJump;
|
|
|
|
/// If true, renders smaller buttons (for mobile).
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (totalPages <= 1) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final clampedCurrent = currentPage.clamp(0, totalPages - 1);
|
|
final windowStart = (clampedCurrent - maxPageJump).clamp(0, totalPages - 1);
|
|
final windowEnd = (clampedCurrent + maxPageJump).clamp(0, totalPages - 1);
|
|
|
|
final buttons = <Widget>[];
|
|
|
|
buttons.add(
|
|
_NavButton(
|
|
icon: Icons.chevron_left,
|
|
enabled: clampedCurrent > 0,
|
|
compact: compact,
|
|
onTap: () => onPageChanged(clampedCurrent - 1),
|
|
),
|
|
);
|
|
|
|
if (windowStart > 0) {
|
|
buttons.add(_buildPageButton(0, clampedCurrent));
|
|
if (windowStart > 1) {
|
|
buttons.add(const _Ellipsis());
|
|
}
|
|
}
|
|
|
|
for (var i = windowStart; i <= windowEnd; i++) {
|
|
buttons.add(_buildPageButton(i, clampedCurrent));
|
|
}
|
|
|
|
if (windowEnd < totalPages - 1) {
|
|
if (windowEnd < totalPages - 2) {
|
|
buttons.add(const _Ellipsis());
|
|
}
|
|
buttons.add(_buildPageButton(totalPages - 1, clampedCurrent));
|
|
}
|
|
|
|
buttons.add(
|
|
_NavButton(
|
|
icon: Icons.chevron_right,
|
|
enabled: clampedCurrent < totalPages - 1,
|
|
compact: compact,
|
|
onTap: () => onPageChanged(clampedCurrent + 1),
|
|
),
|
|
);
|
|
|
|
final gap = compact ? 4.0 : 6.0;
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(vertical: compact ? 8 : 12),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: _withSpacing(buttons, gap),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPageButton(int pageIndex, int currentPageIndex) {
|
|
final isActive = pageIndex == currentPageIndex;
|
|
return _PageButton(
|
|
label: '${pageIndex + 1}',
|
|
isActive: isActive,
|
|
compact: compact,
|
|
onTap: isActive ? null : () => onPageChanged(pageIndex),
|
|
);
|
|
}
|
|
|
|
static List<Widget> _withSpacing(List<Widget> items, double gap) {
|
|
final result = <Widget>[];
|
|
for (var i = 0; i < items.length; i++) {
|
|
result.add(items[i]);
|
|
if (i != items.length - 1) {
|
|
result.add(SizedBox(width: gap));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class _PageButton extends StatelessWidget {
|
|
const _PageButton({
|
|
required this.label,
|
|
required this.isActive,
|
|
required this.compact,
|
|
this.onTap,
|
|
});
|
|
|
|
final String label;
|
|
final bool isActive;
|
|
final bool compact;
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = compact ? 32.0 : 38.0;
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: onTap,
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
gradient: isActive
|
|
? const LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [AppColors.mainBlue, AppColors.primaryColor],
|
|
)
|
|
: null,
|
|
color: isActive ? null : AppColors.grey0,
|
|
border: Border.all(
|
|
color: isActive ? Colors.transparent : AppColors.grey30,
|
|
width: 1,
|
|
),
|
|
boxShadow: isActive
|
|
? [
|
|
BoxShadow(
|
|
color: AppColors.mainBlue.withValues(alpha: 0.25),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: compact ? 12 : 13,
|
|
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
|
|
color: isActive ? Colors.white : AppColors.grey70,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavButton extends StatelessWidget {
|
|
const _NavButton({
|
|
required this.icon,
|
|
required this.enabled,
|
|
required this.compact,
|
|
required this.onTap,
|
|
});
|
|
|
|
final IconData icon;
|
|
final bool enabled;
|
|
final bool compact;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = compact ? 32.0 : 38.0;
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: enabled ? onTap : null,
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
color: AppColors.grey0,
|
|
border: Border.all(
|
|
color: enabled ? AppColors.grey30 : AppColors.grey20,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
size: compact ? 18 : 20,
|
|
color: enabled ? AppColors.grey70 : AppColors.grey40,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Ellipsis extends StatelessWidget {
|
|
const _Ellipsis();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
child: Text(
|
|
'…',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: AppColors.grey60,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|