import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:manager_app/Components/fetch_resource_icon.dart'; import 'package:manager_app/Components/multi_select_container.dart'; import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; import 'package:diacritic/diacritic.dart'; class ResourceBodyGrid extends StatefulWidget { final List resources; final Function onSelect; final bool isAddButton; final bool isSelectModal; final List resourceTypesIn; const ResourceBodyGrid({ Key? key, required this.resources, required this.onSelect, required this.isAddButton, required this.resourceTypesIn, this.isSelectModal = false, }) : super(key: key); @override _ResourceBodyGridState createState() => _ResourceBodyGridState(); } class _ResourceBodyGridState extends State { late List filterTypes; late List currentFilterTypes; String filterSearch = ''; late List selectedTypes; late List resourcesToShow; @override void initState() { resourcesToShow = widget.resources; currentFilterTypes = resource_types .where((rt) => widget.resourceTypesIn.contains(rt.type)) .map((rt) => rt.label) .toList(); filterTypes = resource_types .where((rt) => widget.resourceTypesIn.contains(rt.type)) .map((rt) => rt.label) .toList(); selectedTypes = resource_types .where((rt) => widget.resourceTypesIn.contains(rt.type)) .map((rt) => rt.label) .toList(); super.initState(); } @override Widget build(BuildContext context) { final appContext = Provider.of(context); return bodyGrid(resourcesToShow, appContext); } Widget bodyGrid(List data, AppContext appContext) { final canEdit = (appContext.getContext() as ManagerAppContext).canEdit; return Column( children: [ // Header: search + filter + add button — Wrap pour mobile Padding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 4), child: Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ StringInputContainer( label: 'Recherche:', onChanged: (String value) { setState(() { filterSearch = value; filterResource(); }); }, ), MultiSelectContainer( label: 'Type:', initialValue: filterTypes, values: currentFilterTypes, isMultiple: true, onChanged: (result) { setState(() { selectedTypes = result as List; filterResource(); }); }, ), if (widget.isAddButton && canEdit) _AddButton(onTap: () { widget.onSelect( ResourceDTO(id: widget.isSelectModal ? '-1' : null)); }), ], ), ), // Grid Expanded( child: GridView.builder( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 160, childAspectRatio: 1.0, mainAxisSpacing: 10, crossAxisSpacing: 10, ), itemCount: data.length, itemBuilder: (BuildContext context, int index) { return _ResourceCard( resource: resourcesToShow[index], onTap: () => widget.onSelect(resourcesToShow[index]), ); }, ), ), ], ); } void filterResource() { resourcesToShow = filterSearch.isEmpty ? widget.resources : widget.resources .where((ResourceDTO resource) => resource.id == null || removeDiacritics(resource.label!.toUpperCase()) .contains(removeDiacritics(filterSearch.toUpperCase()))) .toList(); var getTypesInSelected = resource_types .where((ft) => selectedTypes.contains(ft.label)) .map((rt) => rt.type) .toList(); resourcesToShow = resourcesToShow .where((resource) => resource.id == null || getTypesInSelected.contains(resource.type)) .toList(); } } // ── Add button ──────────────────────────────────────────────────────────────── class _AddButton extends StatelessWidget { final VoidCallback onTap; const _AddButton({required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 56, height: 56, decoration: BoxDecoration( color: kSuccess, borderRadius: BorderRadius.circular(16), boxShadow: const [kDefaultShadow], ), child: const Icon(Icons.add, color: kTextLightColor, size: 28), ), ); } } // ── Resource card ───────────────────────────────────────────────────────────── class _ResourceCard extends StatefulWidget { final ResourceDTO resource; final VoidCallback onTap; const _ResourceCard({required this.resource, required this.onTap}); @override State<_ResourceCard> createState() => _ResourceCardState(); } class _ResourceCardState extends State<_ResourceCard> { bool _hovered = false; @override Widget build(BuildContext context) { final resource = widget.resource; final isRemove = resource.id == null; final hasImage = !isRemove && (resource.type == ResourceType.Image || resource.type == ResourceType.ImageUrl) && resource.url != null; return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( onTap: widget.onTap, child: AnimatedScale( scale: _hovered ? 1.03 : 1.0, duration: const Duration(milliseconds: 150), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: kSecond, spreadRadius: _hovered ? 1 : 0.5, blurRadius: _hovered ? 10 : 5, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Stack( fit: StackFit.expand, children: [ // Background if (hasImage) Image.network(resource.url!, fit: BoxFit.cover) else Container( decoration: BoxDecoration( gradient: LinearGradient( colors: isRemove ? [kSecond, kSecond.withValues(alpha: 0.7)] : [kPrimaryColor, kPrimaryColor.withValues(alpha: 0.65)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), ), // Gradient scrim (only when there's an image) if (hasImage) DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.35, 1.0], colors: [ Colors.transparent, Colors.black.withValues(alpha: 0.65), ], ), ), ), // Type icon — top right badge if (!isRemove) Positioned( top: 8, right: 8, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(6), ), child: Icon( getResourceIcon(resource.type), color: kTextLightColor, size: 14, ), ), ), // Label — bottom left if (!isRemove) Positioned( left: 8, right: 8, bottom: 8, child: AutoSizeText( resource.label ?? '', style: kCardTitleStyle.copyWith(fontSize: 13), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // Remove icon if (isRemove) const Center( child: Icon(Icons.close, color: kTextLightColor, size: 40), ), ], ), ), ), ), ), ); } }