450 lines
18 KiB
Dart
450 lines
18 KiB
Dart
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
|
import 'package:fbla_ui/api_logic.dart';
|
|
import 'package:fbla_ui/main.dart';
|
|
import 'package:fbla_ui/pages/create_edit_business.dart';
|
|
import 'package:fbla_ui/pages/export_data.dart';
|
|
import 'package:fbla_ui/pages/signin_page.dart';
|
|
import 'package:fbla_ui/shared.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:rive/rive.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
typedef Callback = void Function();
|
|
|
|
class Home extends StatefulWidget {
|
|
final Callback themeCallback;
|
|
|
|
const Home({super.key, required this.themeCallback});
|
|
|
|
@override
|
|
State<Home> createState() => _HomeState();
|
|
}
|
|
|
|
class _HomeState extends State<Home> {
|
|
late Future refreshBusinessDataFuture;
|
|
bool _isPreviousData = false;
|
|
late List<Business> businesses;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
refreshBusinessDataFuture = fetchBusinessData();
|
|
initialLogin();
|
|
}
|
|
|
|
Future<void> initialLogin() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
bool? rememberMe = prefs.getBool('rememberMe');
|
|
if (rememberMe != null && rememberMe) {
|
|
String? username = prefs.getString('username');
|
|
String? password = prefs.getString('password');
|
|
|
|
jwt = await signIn(username!, password!);
|
|
if (!jwt.contains('Error:')) {
|
|
setState(() {
|
|
loggedIn = true;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void setStateCallback() {
|
|
setState(() {
|
|
loggedIn = loggedIn;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool widescreen = MediaQuery.sizeOf(context).width >= 1000;
|
|
return Scaffold(
|
|
floatingActionButton: _getFAB(),
|
|
body: RefreshIndicator(
|
|
edgeOffset: 120,
|
|
onRefresh: () async {
|
|
var refreshedData = fetchBusinessData();
|
|
await refreshedData;
|
|
setState(() {
|
|
refreshBusinessDataFuture = refreshedData;
|
|
});
|
|
},
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
title: widescreen ? _searchBar() : const Text('Job Link'),
|
|
toolbarHeight: 70,
|
|
pinned: true,
|
|
scrolledUnderElevation: 0,
|
|
centerTitle: true,
|
|
expandedHeight: widescreen ? 70 : 120,
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
bottom: _getBottom(),
|
|
leading: IconButton(
|
|
icon: getIconFromThemeMode(themeMode),
|
|
onPressed: () {
|
|
setState(() {
|
|
widget.themeCallback();
|
|
});
|
|
},
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.help),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('About'),
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.background,
|
|
content: SizedBox(
|
|
width: 500,
|
|
child: IntrinsicHeight(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Welcome to my FBLA 2024 Coding and Programming submission!\n\n'
|
|
'MarinoDev Job Link aims to provide comprehensive details of businesses and community partners'
|
|
' for Waukesha West High School\'s Career and Technical Education Department.\n\n'),
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
child: const Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Git Repo:'),
|
|
Text(
|
|
'https://git.marinodev.com/MarinoDev/FBLA24\n',
|
|
style: TextStyle(
|
|
color: Colors.blue)),
|
|
],
|
|
),
|
|
onTap: () {
|
|
launchUrl(Uri.https(
|
|
'git.marinodev.com',
|
|
'/MarinoDev/FBLA24'));
|
|
},
|
|
),
|
|
),
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
child: const Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Please direct any questions to'),
|
|
Text('drake@marinodev.com',
|
|
style: TextStyle(
|
|
color: Colors.blue)),
|
|
],
|
|
),
|
|
onTap: () {
|
|
launchUrl(Uri.parse(
|
|
'mailto:drake@marinodev.com'));
|
|
},
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('OK'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
}),
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.picture_as_pdf),
|
|
onPressed: () async {
|
|
if (!_isPreviousData) {
|
|
ScaffoldMessenger.of(context).clearSnackBars();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
width: 300,
|
|
behavior: SnackBarBehavior.floating,
|
|
content: Text('There is no data!'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
} else {
|
|
selectedDataTypes = <DataType>{};
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
ExportData(businesses: businesses)));
|
|
}
|
|
},
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: IconButton(
|
|
icon: loggedIn
|
|
? const Icon(Icons.account_circle)
|
|
: const Icon(Icons.login),
|
|
onPressed: () {
|
|
if (loggedIn) {
|
|
var payload = JWT.decode(jwt).payload;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.background,
|
|
title: Text('Hi, ${payload['username']}!'),
|
|
content: Text(
|
|
'You are logged in as an admin with username ${payload['username']}.'),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('Cancel'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Logout'),
|
|
onPressed: () async {
|
|
final prefs = await SharedPreferences
|
|
.getInstance();
|
|
prefs.setBool('rememberMe', false);
|
|
prefs.setString('username', '');
|
|
prefs.setString('password', '');
|
|
|
|
setState(() {
|
|
loggedIn = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
}),
|
|
],
|
|
);
|
|
});
|
|
} else {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => SignInPage(
|
|
refreshAccount: setStateCallback)));
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
FutureBuilder(
|
|
future: refreshBusinessDataFuture,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.done) {
|
|
if (snapshot.hasData) {
|
|
if (snapshot.data.runtimeType == String) {
|
|
_isPreviousData = false;
|
|
return SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(children: [
|
|
Center(
|
|
child: Text(snapshot.data,
|
|
textAlign: TextAlign.center)),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: FilledButton(
|
|
child: const Text('Retry'),
|
|
onPressed: () {
|
|
var refreshedData = fetchBusinessData();
|
|
setState(() {
|
|
refreshBusinessDataFuture = refreshedData;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
]),
|
|
));
|
|
}
|
|
|
|
businesses = snapshot.data;
|
|
_isPreviousData = true;
|
|
|
|
return BusinessDisplayPanel(
|
|
businesses: businesses,
|
|
widescreen: widescreen,
|
|
selectable: false);
|
|
} else if (snapshot.hasError) {
|
|
return SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
|
child: Text(
|
|
'Error when loading data! Error: ${snapshot.error}'),
|
|
));
|
|
}
|
|
} else if (snapshot.connectionState ==
|
|
ConnectionState.waiting) {
|
|
if (_isPreviousData) {
|
|
return BusinessDisplayPanel(
|
|
businesses: businesses,
|
|
widescreen: widescreen,
|
|
selectable: false);
|
|
} else {
|
|
return SliverToBoxAdapter(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8.0),
|
|
alignment: Alignment.center,
|
|
// child: const CircularProgressIndicator(),
|
|
child: const SizedBox(
|
|
width: 75,
|
|
height: 75,
|
|
child: RiveAnimation.asset(
|
|
'assets/mdev_triangle_loading.riv'),
|
|
// child: RiveAnimation.file(
|
|
// 'assets/mdev_triangle_loading.riv',
|
|
// ),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
return SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'\nError: ${snapshot.error}',
|
|
style: const TextStyle(fontSize: 18),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
const SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 80,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget? _getFAB() {
|
|
if (loggedIn) {
|
|
return FloatingActionButton(
|
|
child: const Icon(Icons.add),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const CreateEditBusiness()));
|
|
},
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Widget _searchBar() {
|
|
return SizedBox(
|
|
width: 800,
|
|
height: 50,
|
|
child: TextField(
|
|
onChanged: (query) {
|
|
setState(() {
|
|
searchFilter = query;
|
|
});
|
|
},
|
|
decoration: InputDecoration(
|
|
labelText: 'Search',
|
|
hintText: 'Search',
|
|
prefixIcon: const Padding(
|
|
padding: EdgeInsets.only(left: 8.0),
|
|
child: Icon(Icons.search),
|
|
),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(25.0)),
|
|
),
|
|
suffixIcon: Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: IconButton(
|
|
tooltip: 'Filters',
|
|
icon: Icon(Icons.filter_list,
|
|
color: isFiltered
|
|
? Theme.of(context).colorScheme.primary
|
|
: Theme.of(context).colorScheme.onBackground),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.background,
|
|
title: const Text('Filter Options'),
|
|
content: const FilterChips(),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('Reset'),
|
|
onPressed: () {
|
|
setState(() {
|
|
filters = <BusinessType>{};
|
|
selectedChips = <BusinessType>{};
|
|
isFiltered = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Cancel'),
|
|
onPressed: () {
|
|
selectedChips = Set.from(filters);
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Apply'),
|
|
onPressed: () {
|
|
setState(() {
|
|
filters = Set.from(selectedChips);
|
|
if (filters.isNotEmpty) {
|
|
isFiltered = true;
|
|
} else {
|
|
isFiltered = false;
|
|
}
|
|
});
|
|
Navigator.of(context).pop();
|
|
}),
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget? _getBottom() {
|
|
if (MediaQuery.sizeOf(context).width <= 1000) {
|
|
return PreferredSize(
|
|
preferredSize: const Size.fromHeight(0),
|
|
child: SizedBox(
|
|
// color: Theme.of(context).colorScheme.background,
|
|
height: 70,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: _searchBar(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|