0.2 fixes

This commit is contained in:
2024-06-23 14:36:18 -05:00
parent 4517ec3078
commit 03abc1191d
15 changed files with 1135 additions and 766 deletions
+156 -136
View File
@@ -105,154 +105,174 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
});
}
ListView _detailBody(Business business) {
Widget _detailBody(Business business) {
return ListView(
children: [
// Title, logo, desc, website
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Card(
clipBehavior: Clip.antiAlias,
Center(
child: SizedBox(
width: 800,
child: Column(
children: [
ListTile(
title: Text(business.name!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text(
business.description!,
textAlign: TextAlign.left,
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!),
size: 48);
}),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
titleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(business.name!,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text(
business.description!,
),
contentPadding:
const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${business.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromBusinessType(
business.type ?? BusinessType.other),
size: 48);
}),
),
),
if (business.website != null)
ListTile(
leading: const Icon(Icons.link),
title: const Text('Website'),
subtitle: Text(
business.website!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(
Uri.parse('https://${business.website}'));
},
),
],
),
),
),
ListTile(
leading: const Icon(Icons.link),
title: const Text('Website'),
subtitle: Text(
business.website!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse(business.website!));
},
// Available positions
if (business.listings != null)
Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, top: 4),
child: _GetListingsTitle(business)),
_JobList(business: business)
]),
),
// Contact info
Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Row(
children: [
Padding(
padding:
const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
business.contactName!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
if (business.contactPhone != null)
ListTile(
leading: const Icon(Icons.phone),
title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title:
Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(
Uri.parse('mailto:${business.contactEmail}'));
},
),
],
),
),
// Location
Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: const Icon(Icons.location_on),
title: Text(business.locationName),
subtitle: Text(business.locationAddress!),
onTap: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName} ${business.locationAddress}')));
},
),
),
// Notes
if (business.notes != null && business.notes != '')
Card(
child: ListTile(
leading: const Icon(Icons.notes),
title: const Text(
'Additional Notes',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(business.notes!),
),
),
],
),
),
),
// Available positions
if (business.listings != null)
Card(
clipBehavior: Clip.antiAlias,
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(
padding: const EdgeInsets.only(left: 16, top: 4),
child: _GetListingsTitle(business)),
_JobList(business: business)
]),
),
// Contact info
Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
business.contactName!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
if (business.contactPhone != null)
ListTile(
leading: const Icon(Icons.phone),
title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title: Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
],
),
),
// Location
Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: const Icon(Icons.location_on),
title: Text(business.locationName),
subtitle: Text(business.locationAddress!),
onTap: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
),
// Notes
if (business.notes != null && business.notes != '')
Card(
child: ListTile(
leading: const Icon(Icons.notes),
title: const Text(
'Additional Notes',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(business.notes!),
),
),
],
);
}
+17 -15
View File
@@ -213,6 +213,7 @@ class _BusinessesOverviewState extends State<BusinessesOverview> {
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
selectedChips = Set.from(businessTypeFilters);
showDialog(
context: context,
builder: (BuildContext context) {
@@ -338,9 +339,9 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
);
}
List<BusinessHeader> headers = [];
List<_BusinessHeader> headers = [];
for (BusinessType businessType in widget.groupedBusinesses.keys) {
headers.add(BusinessHeader(
headers.add(_BusinessHeader(
businessType: businessType,
widescreen: widget.widescreen,
businesses: widget.groupedBusinesses[businessType]!));
@@ -351,23 +352,22 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
}
}
class BusinessHeader extends StatefulWidget {
class _BusinessHeader extends StatefulWidget {
final BusinessType businessType;
final List<Business> businesses;
final bool widescreen;
const BusinessHeader({
super.key,
const _BusinessHeader({
required this.businessType,
required this.businesses,
required this.widescreen,
});
@override
State<BusinessHeader> createState() => _BusinessHeaderState();
State<_BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
class _BusinessHeaderState extends State<_BusinessHeader> {
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
@@ -391,7 +391,8 @@ class _BusinessHeaderState extends State<BusinessHeader> {
getIconFromBusinessType(widget.businessType),
color: Theme.of(context).colorScheme.onPrimary,
)),
Text(getNameFromBusinessType(widget.businessType)),
Text(getNameFromBusinessType(widget.businessType),
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)),
],
);
}
@@ -491,17 +492,18 @@ class _BusinessHeaderState extends State<BusinessHeader> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(Uri.parse('https://${business.website}'));
},
),
if (business.website != null)
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(Uri.parse('https://${business.website}'));
},
),
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
'https://www.google.com/maps/search/?api=1&query=${business.locationName} ${business.locationAddress}')));
},
),
if (business.contactPhone != null)
+151 -121
View File
@@ -4,6 +4,8 @@ import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../shared/global_vars.dart';
class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness;
@@ -23,15 +25,14 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
late TextEditingController _notesController;
late TextEditingController _locationNameController;
late TextEditingController _locationAddressController;
// late TextEditingController _businessTypeController;
late bool widescreen;
Business business = Business(
id: 0,
name: 'Business',
description: 'Add details about the business below.',
type: null,
website: '',
website: null,
contactName: null,
contactEmail: null,
contactPhone: null,
@@ -56,8 +57,8 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
_descriptionController = TextEditingController();
}
_websiteController = TextEditingController(
text: business.website!
.replaceAll('https://', '')
text: business.website
?.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
_contactNameController = TextEditingController(text: business.contactName);
@@ -76,6 +77,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
@override
Widget build(BuildContext context) {
widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return PopScope(
canPop: !_isLoading,
onPopInvoked: _handlePop,
@@ -87,65 +89,23 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
? Text('Edit ${widget.inputBusiness?.name}', maxLines: 1)
: const Text('Add New Business'),
),
floatingActionButton: FloatingActionButton(
child: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
if (business.type == null) {
setState(() {
dropDownErrorText = 'Business type is required';
});
formKey.currentState!.validate();
} else {
setState(() {
dropDownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputBusiness != null) {
result = await editBusiness(business);
} else {
result = await createBusiness(business);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp()));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
);
}
}
},
),
floatingActionButton: !widescreen
? FloatingActionButton.extended(
label: const Text('Save'),
icon: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
await _saveBusiness(context);
},
)
: null,
body: ListView(
children: [
Center(
@@ -154,26 +114,25 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
child: Column(
children: [
ListTile(
titleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(business.name!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text(
business.description!,
textAlign: TextAlign.left,
),
contentPadding:
const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${business.id}',
width: 48,
height: 48,
'https://logo.clearbit.com/${business.website}',
errorBuilder: (BuildContext context,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromBusinessType(business.type != null
? business.type!
: BusinessType.other),
getIconFromBusinessType(
business.type ?? BusinessType.other),
size: 48);
}),
),
@@ -183,7 +142,10 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
children: [
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
top: 8.0,
bottom: 8.0,
left: 8.0,
right: 8.0),
child: TextFormField(
controller: _nameController,
autovalidateMode:
@@ -210,7 +172,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
bottom: 8.0, left: 8.0, right: 8.0),
child: TextFormField(
controller: _descriptionController,
autovalidateMode:
@@ -247,62 +209,59 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
business.website = Uri.encodeFull(inputUrl);
if (!business.website!
.contains('http://') &&
!business.website!
.contains('https://')) {
business.website =
'https://${business.website}';
if (inputUrl.trim().isEmpty) {
business.website = null;
} else {
if (!business.website!
.contains('http://') &&
!business.website!
.contains('https://')) {
business.website =
'https://${business.website!.trim()}';
}
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website (required)',
labelText: 'Website',
),
validator: (value) {
if (value != null &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:\/[^\/\s]*)*')
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:/[^/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
if (value != null && value.trim().isEmpty) {
return 'Website is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text('Type of Business',
style: TextStyle(fontSize: 16)),
DropdownMenu<BusinessType>(
initialSelection: business.type,
label: const Text('Business Type'),
errorText: dropDownErrorText,
dropdownMenuEntries: [
for (BusinessType type
in BusinessType.values)
DropdownMenuEntry(
value: type,
label: getNameFromBusinessType(
type)),
],
onSelected: (inputType) {
setState(() {
business.type = inputType!;
dropDownErrorText = null;
});
},
),
],
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: DropdownMenu<BusinessType>(
initialSelection: business.type,
// width: 776,
label: const Text('Business Type'),
errorText: dropDownErrorText,
dropdownMenuEntries: [
for (BusinessType type
in BusinessType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromBusinessType(type)),
],
onSelected: (inputType) {
setState(() {
business.type = inputType!;
dropDownErrorText = null;
});
},
),
),
),
@@ -380,7 +339,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
// ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactNameController,
onSaved: (inputText) {
@@ -405,7 +364,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactPhoneController,
inputFormatters: [PhoneFormatter()],
@@ -439,7 +398,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactEmailController,
keyboardType: TextInputType.emailAddress,
@@ -482,7 +441,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _locationNameController,
onChanged: (inputName) {
@@ -558,9 +517,33 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
],
),
),
SizedBox(
height: 75,
)
if (!widescreen)
const SizedBox(
height: 75,
)
else
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(
top: 8.0, right: 8.0, bottom: 8.0),
child: Icon(Icons.save),
),
Text('Save'),
],
),
onPressed: () async {
await _saveBusiness(context);
},
),
),
)
],
),
),
@@ -582,6 +565,53 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
);
}
}
Future<void> _saveBusiness(BuildContext context) async {
if (business.type == null) {
setState(() {
dropDownErrorText = 'Business type is required';
});
formKey.currentState!.validate();
} else {
setState(() {
dropDownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputBusiness != null) {
result = await editBusiness(business);
} else {
result = await createBusiness(business);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const MainApp()));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
}
}
class PhoneFormatter extends TextInputFormatter {
+254 -161
View File
@@ -1,9 +1,11 @@
import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import '../main.dart';
class CreateEditJobListing extends StatefulWidget {
final JobListing? inputJobListing;
final Business? inputBusiness;
@@ -21,9 +23,10 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
late TextEditingController _descriptionController;
late TextEditingController _wageController;
late TextEditingController _linkController;
List nameMapping = [];
List<Map<String, dynamic>> nameMapping = [];
String? typeDropdownErrorText;
String? businessDropdownErrorText;
late bool widescreen;
JobListing listing = JobListing(
id: null,
@@ -32,7 +35,8 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
description: 'Add details about the business below.',
type: null,
wage: null,
link: null);
link: null,
offerType: null);
bool _isLoading = false;
@override
@@ -59,6 +63,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
@override
Widget build(BuildContext context) {
widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
if (widget.inputBusiness != null) {
listing.businessId = widget.inputBusiness!.id;
}
@@ -73,75 +78,22 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
? Text('Edit ${widget.inputJobListing?.name}', maxLines: 1)
: const Text('Add New Job Listing'),
),
floatingActionButton: FloatingActionButton(
child: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
if (listing.type == null || listing.businessId == null) {
if (listing.type == null) {
setState(() {
typeDropdownErrorText = 'Job type is required';
});
formKey.currentState!.validate();
}
if (listing.businessId == null) {
setState(() {
businessDropdownErrorText = 'Business is required';
});
formKey.currentState!.validate();
}
} else {
setState(() {
typeDropdownErrorText = null;
businessDropdownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputJobListing != null) {
result = await editListing(listing);
} else {
result = await createListing(listing);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp(
initialPage: 1,
)));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
);
}
}
}),
floatingActionButton: !widescreen
? FloatingActionButton.extended(
label: const Text('Save'),
icon: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
await _saveListing(context);
})
: null,
body: FutureBuilder(
future: getBusinessNameMapping,
builder: (context, snapshot) {
@@ -172,6 +124,8 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
}
nameMapping = snapshot.data;
nameMapping.sort((a, b) =>
a['name'].toString().compareTo(b['name'].toString()));
return ListView(
children: [
@@ -181,30 +135,29 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
child: Column(
children: [
ListTile(
titleAlignment:
ListTileTitleAlignment.titleHeight,
title: Text(listing.name,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold)),
subtitle: Text(
listing.description,
textAlign: TextAlign.left,
),
contentPadding: const EdgeInsets.only(
bottom: 8, left: 16),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${listing.businessId}',
width: 48,
height: 48,
listing.businessId != null
? '$apiAddress/logos/${listing.businessId}'
: '',
errorBuilder: (BuildContext context,
Object exception,
StackTrace? stackTrace) {
height: 48, errorBuilder:
(BuildContext context,
Object exception,
StackTrace? stackTrace) {
return Icon(
getIconFromJobType(
listing.type ?? JobType.other,
),
listing.type ?? JobType.other),
size: 48);
}),
),
@@ -213,86 +166,126 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
Card(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: [
const Text('Type of Job',
style:
TextStyle(fontSize: 16)),
DropdownMenu<JobType>(
initialSelection: listing.type,
label: const Text('Job Type'),
errorText:
typeDropdownErrorText,
dropdownMenuEntries: [
for (JobType type
in JobType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromJobType(
type))
],
onSelected: (inputType) {
setState(() {
listing.type = inputType!;
typeDropdownErrorText =
null;
});
},
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8.0),
child: DropdownMenu<JobType>(
initialSelection:
listing.type,
label: const Text('Job Type'),
errorText:
typeDropdownErrorText,
width: calculateDropdownWidth(
context),
dropdownMenuEntries: [
for (JobType type
in JobType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromJobType(
type))
],
onSelected: (inputType) {
setState(() {
listing.type = inputType!;
typeDropdownErrorText =
null;
});
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8.0),
child: DropdownMenu<OfferType>(
initialSelection:
listing.offerType,
label:
const Text('Offer Type'),
errorText:
typeDropdownErrorText,
width: calculateDropdownWidth(
context),
dropdownMenuEntries: [
for (OfferType type
in OfferType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromOfferType(
type))
],
onSelected: (inputType) {
setState(() {
listing.offerType =
inputType!;
typeDropdownErrorText =
null;
});
},
),
),
],
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0,
top: 8.0),
child: DropdownMenu<int>(
menuHeight: 300,
width: (MediaQuery.sizeOf(context)
.width -
24) <
776
? MediaQuery.sizeOf(context)
.width -
24
: 776,
errorText:
businessDropdownErrorText,
initialSelection:
widget.inputBusiness?.id,
label: const Text(
'Offering Business'),
dropdownMenuEntries: [
for (Map<String, dynamic> map
in nameMapping)
DropdownMenuEntry(
value: map['id']!,
label: map['name'])
],
onSelected: (inputType) {
setState(() {
listing.businessId =
inputType!;
businessDropdownErrorText =
null;
});
},
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'Business that has the job',
style:
TextStyle(fontSize: 16)),
DropdownMenu<int>(
errorText:
businessDropdownErrorText,
initialSelection:
widget.inputBusiness?.id,
label: const Text('Business'),
dropdownMenuEntries: [
for (Map<String, dynamic> map
in nameMapping)
DropdownMenuEntry(
value: map['id']!,
label: map['name'])
],
onSelected: (inputType) {
setState(() {
listing.businessId =
inputType!;
businessDropdownErrorText =
null;
});
},
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
bottom: 8.0),
child: TextFormField(
controller: _nameController,
autovalidateMode: AutovalidateMode
@@ -322,7 +315,9 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
left: 8.0,
right: 8.0,
bottom: 8.0),
child: TextFormField(
controller: _descriptionController,
autovalidateMode: AutovalidateMode
@@ -355,7 +350,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0),
bottom: 16.0),
child: TextFormField(
controller: _wageController,
onChanged: (input) {
@@ -376,7 +371,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0),
bottom: 16.0),
child: TextFormField(
controller: _linkController,
autovalidateMode: AutovalidateMode
@@ -399,7 +394,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
validator: (value) {
if (value != null &&
value.isNotEmpty &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:\/[^\/\s]*)*')
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:/[^/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
@@ -418,9 +413,35 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
],
),
),
const SizedBox(
height: 75,
)
if (!widescreen)
const SizedBox(
height: 75,
)
else
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(
top: 8.0,
right: 8.0,
bottom: 8.0),
child: Icon(Icons.save),
),
Text('Save'),
],
),
onPressed: () async {
await _saveListing(context);
},
),
),
)
],
),
),
@@ -456,6 +477,18 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
);
}
double calculateDropdownWidth(BuildContext context) {
double screenWidth = MediaQuery.sizeOf(context).width;
if ((screenWidth - 40) / 2 < 200) {
return screenWidth - 24;
} else if ((screenWidth - 40) / 2 < 380) {
return (screenWidth - 40) / 2;
} else {
return 380;
}
}
void _handlePop(bool didPop) {
if (!didPop) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -467,4 +500,64 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
);
}
}
Future<void> _saveListing(BuildContext context) async {
if (listing.type == null || listing.businessId == null) {
if (listing.type == null) {
setState(() {
typeDropdownErrorText = 'Job type is required';
});
formKey.currentState!.validate();
}
if (listing.businessId == null) {
setState(() {
businessDropdownErrorText = 'Business is required';
});
formKey.currentState!.validate();
}
} else {
setState(() {
typeDropdownErrorText = null;
businessDropdownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputJobListing != null) {
result = await editListing(listing);
} else {
result = await createListing(listing);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp(
initialPage: 1,
)));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
}
}
+119 -120
View File
@@ -29,145 +29,144 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
);
}
ListView _detailBody(JobListing listing) {
Widget _detailBody(JobListing listing) {
return ListView(
children: [
// Title, logo, desc, website
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Card(
clipBehavior: Clip.antiAlias,
Center(
child: SizedBox(
width: 800,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${listing.businessId}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromJobType(listing.type!),
size: 48);
}),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(listing.name,
padding: const EdgeInsets.only(top: 4.0),
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
titleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(listing.name,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
Text(widget.fromBusiness.name!,
style: const TextStyle(fontSize: 16)),
Text(
subtitle: Text(
listing.description,
),
contentPadding:
const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${listing.businessId}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromJobType(
listing.type ?? JobType.other),
size: 48);
}),
),
),
if (listing.link != null && listing.link != '')
ListTile(
leading: const Icon(Icons.link),
title: const Text('More Information'),
subtitle: Text(
listing.link!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse('https://${listing.link!}'));
},
),
],
),
),
),
// Wage
Visibility(
visible: listing.wage != null && listing.wage != '',
child: Card(
child: ListTile(
leading: const Icon(Icons.attach_money),
subtitle: Text(listing.wage!),
title: const Text('Wage Information'),
),
),
),
Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Row(
children: [
Padding(
padding:
const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
widget.fromBusiness.contactName!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
if (widget.fromBusiness.contactPhone != null)
ListTile(
leading: const Icon(Icons.phone),
title: Text(widget.fromBusiness.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title: Text(
'Contact ${widget.fromBusiness.contactName}'),
content: Text(
'Would you like to call or text ${widget.fromBusiness.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${widget.fromBusiness.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${widget.fromBusiness.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (widget.fromBusiness.contactEmail != null)
ListTile(
leading: const Icon(Icons.email),
title: Text(widget.fromBusiness.contactEmail!),
onTap: () {
launchUrl(Uri.parse(
'mailto:${widget.fromBusiness.contactEmail}'));
},
),
],
),
),
if (listing.link != null && listing.link != '')
ListTile(
leading: const Icon(Icons.link),
title: const Text('More Information'),
subtitle: Text(
listing.link!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse(listing.link!));
},
),
],
),
),
),
// Wage
Visibility(
visible: listing.wage != null && listing.wage != '',
child: Card(
child: ListTile(
leading: const Icon(Icons.attach_money),
subtitle: Text(listing.wage!),
title: const Text('Wage Information'),
),
),
),
Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
widget.fromBusiness.contactName!,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
if (widget.fromBusiness.contactPhone != null)
ListTile(
leading: const Icon(Icons.phone),
title: Text(widget.fromBusiness.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title: Text(
'Contact ${widget.fromBusiness.contactName}'),
content: Text(
'Would you like to call or text ${widget.fromBusiness.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${widget.fromBusiness.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${widget.fromBusiness.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (widget.fromBusiness.contactEmail != null)
ListTile(
leading: const Icon(Icons.email),
title: Text(widget.fromBusiness.contactEmail!),
onTap: () {
launchUrl(Uri.parse(
'mailto:${widget.fromBusiness.contactEmail}'));
},
),
],
),
),
],
);
}
+132 -54
View File
@@ -15,7 +15,8 @@ import 'package:url_launcher/url_launcher.dart';
class JobsOverview extends StatefulWidget {
final String searchQuery;
final Future refreshJobDataOverviewFuture;
final Future<void> Function(Set<JobType>) updateBusinessesCallback;
final Future<void> Function(Set<JobType>?, Set<OfferType>?)
updateBusinessesCallback;
final void Function() themeCallback;
final void Function(bool) updateLoggedIn;
@@ -36,6 +37,7 @@ class _JobsOverviewState extends State<JobsOverview> {
bool _isPreviousData = false;
late Map<JobType, List<Business>> overviewBusinesses;
Set<JobType> jobTypeFilters = <JobType>{};
Set<OfferType> offerTypeFilters = <OfferType>{};
String searchQuery = '';
ScrollController controller = ScrollController();
bool _extended = true;
@@ -66,9 +68,15 @@ class _JobsOverviewState extends State<JobsOverview> {
});
}
void _setFilters(Set<JobType> filters) async {
jobTypeFilters = Set.from(filters);
widget.updateBusinessesCallback(jobTypeFilters);
void _setFilters(Set<JobType>? newJobTypeFilters,
Set<OfferType>? newOfferTypeFilters) async {
if (newJobTypeFilters != null) {
jobTypeFilters = Set.from(newJobTypeFilters);
}
if (newOfferTypeFilters != null) {
offerTypeFilters = Set.from(newOfferTypeFilters);
}
widget.updateBusinessesCallback(jobTypeFilters, offerTypeFilters);
}
void _scrollListener() {
@@ -114,9 +122,8 @@ class _JobsOverviewState extends State<JobsOverview> {
setSearch: _setSearch,
searchHintText: 'Search Job Listings',
themeCallback: widget.themeCallback,
filterIconButton: _filterIconButton(
jobTypeFilters,
),
filterIconButton:
_filterIconButton(jobTypeFilters, offerTypeFilters),
updateLoggedIn: widget.updateLoggedIn,
generatePDF: _generatePDF,
),
@@ -139,7 +146,7 @@ class _JobsOverviewState extends State<JobsOverview> {
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
widget.updateBusinessesCallback(jobTypeFilters);
widget.updateBusinessesCallback(null, null);
},
),
),
@@ -201,45 +208,78 @@ class _JobsOverviewState extends State<JobsOverview> {
);
}
Widget _filterIconButton(Set<JobType> filters) {
Set<JobType> selectedChips = Set.from(filters);
Widget _filterIconButton(
Set<JobType> jobTypeFilters, Set<OfferType> offerTypeFilters) {
Set<JobType> selectedJobTypeChips = Set.from(jobTypeFilters);
Set<OfferType> selectedOfferTypeChips = Set.from(offerTypeFilters);
return IconButton(
icon: Icon(
Icons.filter_list,
color: filters.isNotEmpty
color: jobTypeFilters.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
selectedJobTypeChips = Set.from(jobTypeFilters);
selectedOfferTypeChips = Set.from(offerTypeFilters);
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
void setDialogState(Set<JobType> newFilters) {
setState(() {
filters = newFilters;
});
void setDialogState(Set<JobType>? newJobTypeFilters,
Set<OfferType>? newOfferTypeFilters) {
if (newJobTypeFilters != null) {
setState(() {
selectedJobTypeChips = newJobTypeFilters;
});
}
if (newOfferTypeFilters != null) {
setState(() {
selectedOfferTypeChips = newOfferTypeFilters;
});
}
}
List<Padding> chips = [];
for (var type in JobType.values) {
chips.add(Padding(
List<Padding> jobTypeChips = [];
for (JobType type in JobType.values) {
jobTypeChips.add(Padding(
padding: const EdgeInsets.all(4),
child: FilterChip(
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
label: Text(getNameFromJobType(type)),
selected: selectedChips.contains(type),
selected: selectedJobTypeChips.contains(type),
onSelected: (bool selected) {
if (selected) {
selectedChips.add(type);
selectedJobTypeChips.add(type);
} else {
selectedChips.remove(type);
selectedJobTypeChips.remove(type);
}
setDialogState(filters);
setDialogState(selectedJobTypeChips, null);
}),
));
}
List<Padding> offerTypeChips = [];
for (OfferType type in OfferType.values) {
offerTypeChips.add(Padding(
padding: const EdgeInsets.all(4),
child: FilterChip(
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
label: Text(getNameFromOfferType(type)),
selected: selectedOfferTypeChips.contains(type),
onSelected: (bool selected) {
if (selected) {
selectedOfferTypeChips.add(type);
} else {
selectedOfferTypeChips.remove(type);
}
setDialogState(null, selectedOfferTypeChips);
}),
));
}
@@ -248,30 +288,46 @@ class _JobsOverviewState extends State<JobsOverview> {
title: const Text('Filter Options'),
content: SizedBox(
width: 400,
child: Wrap(
children: chips,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Job Type Filters:'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
children: jobTypeChips,
),
),
const Text('Offer Type Filters:'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
children: offerTypeChips,
),
),
],
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
_setFilters(<JobType>{});
// selectedChips = <BusinessType>{};
_setFilters(<JobType>{}, <OfferType>{});
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Cancel'),
onPressed: () {
// selectedChips = Set.from(filters);
// setDialogState(jobTypeFilters, offerTypeFilters);
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Apply'),
onPressed: () {
_setFilters(selectedChips);
_setFilters(
selectedJobTypeChips, selectedOfferTypeChips);
Navigator.of(context).pop();
},
)
@@ -337,9 +393,9 @@ class _JobDisplayPanelState extends State<JobDisplayPanel> {
);
}
List<BusinessHeader> headers = [];
List<_JobHeader> headers = [];
for (JobType jobType in widget.jobGroupedBusinesses.keys) {
headers.add(BusinessHeader(
headers.add(_JobHeader(
jobType: jobType,
widescreen: widget.widescreen,
businesses: widget.jobGroupedBusinesses[jobType]!));
@@ -349,23 +405,22 @@ class _JobDisplayPanelState extends State<JobDisplayPanel> {
}
}
class BusinessHeader extends StatefulWidget {
class _JobHeader extends StatefulWidget {
final JobType jobType;
final List<Business> businesses;
final bool widescreen;
const BusinessHeader({
super.key,
const _JobHeader({
required this.jobType,
required this.businesses,
required this.widescreen,
});
@override
State<BusinessHeader> createState() => _BusinessHeaderState();
State<_JobHeader> createState() => _JobHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
class _JobHeaderState extends State<_JobHeader> {
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
@@ -389,7 +444,8 @@ class _BusinessHeaderState extends State<BusinessHeader> {
getIconFromJobType(widget.jobType),
color: Theme.of(context).colorScheme.onPrimary,
)),
Text(getNameFromJobType(widget.jobType)),
Text(getNameFromJobType(widget.jobType),
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)),
],
);
}
@@ -451,21 +507,34 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48,
width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!),
size: 48);
}),
child: Badge(
label: Text(
getLetterFromOfferType(
business.listings![0].offerType!),
style: const TextStyle(fontSize: 16),
),
largeSize: 26,
offset: const Offset(15, -5),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${business.id}',
height: 48,
width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromJobType(business.listings![0].type!),
size: 48);
}),
),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.listings![0].name,
'${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
@@ -559,14 +628,23 @@ class _BusinessHeaderState extends State<BusinessHeader> {
Widget _businessListItem(Business business, JobType? jobType) {
return Card(
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!));
})),
title: Text(business.listings![0].name),
leading: Badge(
label: Text(getLetterFromOfferType(business.listings![0].offerType!)),
textColor: Theme.of(context).colorScheme.onPrimary,
isLabelVisible: true,
backgroundColor: Theme.of(context).colorScheme.primary,
child: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(
getIconFromJobType(business.listings![0].type!),
);
})),
),
title: Text(
'${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})'),
subtitle: Text(business.listings![0].description,
maxLines: 2, overflow: TextOverflow.ellipsis),
onTap: () {