Initial commit
This commit is contained in:
6
lib/main.dart
Normal file
6
lib/main.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/app.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const App());
|
||||
}
|
147
lib/src/app.dart
Normal file
147
lib/src/app.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/screens/events/index.dart';
|
||||
import 'package:furman_now/src/screens/home/index.dart';
|
||||
import 'package:furman_now/src/screens/info/index.dart';
|
||||
import 'package:furman_now/src/screens/map/index.dart';
|
||||
import 'package:furman_now/src/screens/student_id/index.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:navbar_router/navbar_router.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Furman Now!',
|
||||
home: const MainPage(),
|
||||
theme: myFurmanTheme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MainPage> createState() => _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> {
|
||||
List<NavbarItem> items = [
|
||||
NavbarItem(Icons.home_outlined, 'Home', backgroundColor: colors[0]),
|
||||
NavbarItem(Icons.map_outlined, 'Map', backgroundColor: colors[0]),
|
||||
NavbarItem(Icons.person_outline, 'Meal ID', backgroundColor: colors[0]),
|
||||
NavbarItem(Icons.calendar_month_outlined, 'Events', backgroundColor: colors[0]),
|
||||
NavbarItem(Icons.info_outline, 'Info', backgroundColor: colors[0]),
|
||||
];
|
||||
|
||||
final Map<int, Map<String, Widget>> _routes = const {
|
||||
0: {
|
||||
'/': HomeScreen(),
|
||||
// FeedDetail.route: FeedDetail(),
|
||||
},
|
||||
1: {
|
||||
'/': MapScreen(),
|
||||
// ProductDetail.route: ProductDetail(),
|
||||
// ProductComments.route: ProductComments(),
|
||||
},
|
||||
2: {
|
||||
'/': StudentIdScreen(),
|
||||
// ProductDetail.route: ProductDetail(),
|
||||
// ProductComments.route: ProductComments(),
|
||||
},
|
||||
3: {
|
||||
'/': EventsScreen(),
|
||||
// ProfileEdit.route: ProfileEdit(),
|
||||
},
|
||||
4: {
|
||||
'/': InfoScreen(),
|
||||
// ProfileEdit.route: ProfileEdit(),
|
||||
},
|
||||
};
|
||||
|
||||
void showSnackBar() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: Duration(milliseconds: 600),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: kBottomNavigationBarHeight + 2, right: 2, left: 2),
|
||||
content: Text('Tap back button again to exit'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void hideSnackBar() {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
}
|
||||
|
||||
DateTime oldTime = DateTime.now();
|
||||
DateTime newTime = DateTime.now();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: NavbarRouter(
|
||||
errorBuilder: (context) {
|
||||
return const Center(child: Text('Error 404'));
|
||||
},
|
||||
isDesktop: size.width > 600 ? true : false,
|
||||
onBackButtonPressed: (isExitingApp) {
|
||||
if (isExitingApp) {
|
||||
newTime = DateTime.now();
|
||||
int difference = newTime.difference(oldTime).inMilliseconds;
|
||||
oldTime = newTime;
|
||||
if (difference < 1000) {
|
||||
hideSnackBar();
|
||||
return isExitingApp;
|
||||
} else {
|
||||
showSnackBar();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return isExitingApp;
|
||||
}
|
||||
},
|
||||
destinationAnimationCurve: Curves.fastOutSlowIn,
|
||||
destinationAnimationDuration: 600,
|
||||
decoration: NavbarDecoration(
|
||||
selectedLabelTextStyle: const TextStyle(color: Colors.deepPurple),
|
||||
showUnselectedLabels: true,
|
||||
unselectedLabelTextStyle:
|
||||
const TextStyle(color: Colors.black, fontSize: 10),
|
||||
selectedIconTheme: const IconThemeData(color: Colors.deepPurple),
|
||||
isExtended: size.width > 800 ? true : false,
|
||||
navbarType: BottomNavigationBarType.fixed),
|
||||
// onChanged: (x) {
|
||||
// debugPrint('index changed $x');
|
||||
// },
|
||||
backButtonBehavior: BackButtonBehavior.rememberHistory,
|
||||
destinations: [
|
||||
for (int i = 0; i < items.length; i++)
|
||||
DestinationRouter(
|
||||
navbarItem: items[i],
|
||||
destinations: [
|
||||
for (int j = 0; j < _routes[i]!.keys.length; j++)
|
||||
Destination(
|
||||
route: _routes[i]!.keys.elementAt(j),
|
||||
widget: _routes[i]!.values.elementAt(j),
|
||||
),
|
||||
],
|
||||
initialRoute: _routes[i]!.keys.first,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> navigate(BuildContext context, String route,
|
||||
{bool isDialog = false,
|
||||
bool isRootNavigator = true,
|
||||
Map<String, dynamic>? arguments}) =>
|
||||
Navigator.of(context, rootNavigator: isRootNavigator)
|
||||
.pushNamed(route, arguments: arguments);
|
49
lib/src/layouts/main/index.dart
Normal file
49
lib/src/layouts/main/index.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MainLayout extends StatelessWidget {
|
||||
const MainLayout({
|
||||
Key? key,
|
||||
this.body,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget? body;
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
// Navigate to the second screen using a named route.
|
||||
// Navigator.pushNamed(context, '/second');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: body,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.map),
|
||||
label: 'Map',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.perm_identity),
|
||||
label: 'Meal Card',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.calendar_month),
|
||||
label: 'Events',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.info_outline),
|
||||
label: 'Info',
|
||||
),
|
||||
],
|
||||
currentIndex: 0,
|
||||
selectedItemColor: Colors.grey[700],
|
||||
onTap: _onItemTapped,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
15
lib/src/routes/index.dart
Normal file
15
lib/src/routes/index.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/screens/home/index.dart';
|
||||
|
||||
Route routes(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case '/':
|
||||
return MaterialPageRoute(builder: (_) => const HomeScreen());
|
||||
// case '/home':
|
||||
// return MaterialPageRoute(builder: (_) => HomeScreen());
|
||||
// case '/auth':
|
||||
// return MaterialPageRoute(builder: (_) => AuthenticationScreen());
|
||||
default:
|
||||
return MaterialPageRoute(builder: (_) => const HomeScreen());
|
||||
}
|
||||
}
|
113
lib/src/screens/events/index.dart
Normal file
113
lib/src/screens/events/index.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/utils/date_range.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:furman_now/src/widgets/header.dart';
|
||||
import 'package:furman_now/src/widgets/home/events/events_list.dart';
|
||||
import 'package:furman_now/src/widgets/scroll_view_height.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class EventsScreen extends StatelessWidget {
|
||||
const EventsScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: Colors.grey[100],
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: kBottomNavigationBarHeight),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
width: double.infinity,
|
||||
height: 100,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.calendar_month_outlined, size: 35, color: Colors.grey[700]),
|
||||
const SizedBox(width: 12),
|
||||
Text("Events", style: furmanTextStyle(TextStyle(color: Colors.grey[900], fontSize: 28, fontWeight: FontWeight.w700))),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ScrollViewWithHeight(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
margin: const EdgeInsets.only(top: 100),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const HeaderWidget(title: "Today"),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: EventsList()
|
||||
),
|
||||
const HeaderWidget(title: "Tomorrow"),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: EventsList(dateRange: constructDateRange(
|
||||
DateTime.now().add(const Duration(days: 1)),
|
||||
DateTime.now().add(const Duration(days: 1)),
|
||||
)),
|
||||
),
|
||||
...[for(var i=2; i<7; i+=1) i].map((i) {
|
||||
var date = DateTime.now().add(Duration(days: i));
|
||||
var dayName = DateFormat('EEEE').format(date);
|
||||
return Wrap(
|
||||
children: [
|
||||
HeaderWidget(title: dayName),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: EventsList(dateRange: constructDateRange(
|
||||
date,
|
||||
date,
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
Center(child:
|
||||
Wrap(
|
||||
direction: Axis.vertical,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: const [
|
||||
Text("Need more events?"),
|
||||
Text("Syncdin"),
|
||||
Text("Athletics"),
|
||||
Text("CLPs"),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
96
lib/src/screens/home/index.dart
Normal file
96
lib/src/screens/home/index.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/utils/greeting.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:furman_now/src/widgets/header.dart';
|
||||
import 'package:furman_now/src/widgets/home/events/events_list.dart';
|
||||
import 'package:furman_now/src/widgets/home/restaurants/restaurants_list.dart';
|
||||
import 'package:furman_now/src/widgets/home/transportation/transportation_card.dart';
|
||||
import 'package:furman_now/src/widgets/scroll_view_height.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: const Color(0xffb7acc9),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
color: Colors.grey[100],
|
||||
padding: const EdgeInsets.only(bottom: kBottomNavigationBarHeight),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Color(0xffb7acc9),
|
||||
Color(0xffb7acc9),
|
||||
], // Gradient from https://learnui.design/tools/gradient-generator.html
|
||||
tileMode: TileMode.mirror,
|
||||
),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(40),
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("${greeting()},\nMichael", style: furmanTextStyle(const TextStyle(color: Color(0xff26183d), fontSize: 36, fontWeight: FontWeight.w800))),
|
||||
const SizedBox(height: 5),
|
||||
Text("It's 76º and partly cloudy", style: furmanTextStyle(const TextStyle(color: Color(0xff26183d), fontSize: 16, fontWeight: FontWeight.w500))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ScrollViewWithHeight(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
margin: const EdgeInsets.only(top: 200),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const HeaderWidget(
|
||||
title: "Today's Events",
|
||||
link: HeaderLink(text: "View more", href: ""),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: EventsList(),
|
||||
),
|
||||
const HeaderWidget(title: "Food & Dining"),
|
||||
const RestaurantsList(),
|
||||
const HeaderWidget(title: "Transportation"),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: TransportationCard(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
89
lib/src/screens/info/index.dart
Normal file
89
lib/src/screens/info/index.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:furman_now/src/widgets/info/info_card.dart';
|
||||
import 'package:furman_now/src/widgets/scroll_view_height.dart';
|
||||
|
||||
class InfoScreen extends StatelessWidget {
|
||||
const InfoScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: Colors.grey[100],
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: kBottomNavigationBarHeight),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
width: double.infinity,
|
||||
height: 100,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 35, color: Colors.grey[700]),
|
||||
const SizedBox(width: 12),
|
||||
Text("Info", style: furmanTextStyle(TextStyle(color: Colors.grey[900], fontSize: 28, fontWeight: FontWeight.w700))),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ScrollViewWithHeight(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
margin: const EdgeInsets.only(top: 100),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoCard(
|
||||
color: Colors.red.shade50,
|
||||
icon: Icons.local_hospital,
|
||||
title: "Health and Safety",
|
||||
description: "Important contact information and links regarding student health and safety.",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
InfoCard(
|
||||
color: Colors.deepPurple.shade50,
|
||||
icon: Icons.phone,
|
||||
title: "Contacts",
|
||||
description: "Important contact information and links regarding student health and safety.",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
InfoCard(
|
||||
color: Colors.blue.shade50,
|
||||
icon: Icons.access_time,
|
||||
title: "Hours",
|
||||
description: "Important contact information and links regarding student health and safety.",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
187
lib/src/screens/map/index.dart
Normal file
187
lib/src/screens/map/index.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:furman_now/src/widgets/map/filter_chip.dart';
|
||||
import 'package:furman_now/src/widgets/map/rotate_compass.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class MapScreen extends StatefulWidget {
|
||||
const MapScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MapScreen> createState() => _MapScreenState();
|
||||
}
|
||||
|
||||
class _MapScreenState extends State<MapScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
late final AnimationController _animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
var _rotation = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mapController.mapEventStream.listen((event) {
|
||||
if (event is MapEventRotate) {
|
||||
setState(() {
|
||||
_rotation = _mapController.rotation * (2 * pi) / 360;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_animationController.dispose();
|
||||
}
|
||||
|
||||
void resetRotation() async {
|
||||
// take the shortest rotation path
|
||||
var end = _mapController.rotation > 180 ? 360.0 : 0.0;
|
||||
var animation = Tween<double>(
|
||||
begin: _mapController.rotation,
|
||||
end: end,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
animationListener() {
|
||||
_mapController.rotate(animation.value);
|
||||
}
|
||||
|
||||
animation.addListener(animationListener);
|
||||
|
||||
await _animationController.forward();
|
||||
|
||||
animation.removeListener(animationListener);
|
||||
_animationController.reset();
|
||||
|
||||
_mapController.rotate(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: const Color(0xffb7acc9),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: kBottomNavigationBarHeight),
|
||||
child: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
center: LatLng(34.925926, -82.439397),
|
||||
enableMultiFingerGestureRace: true,
|
||||
rotationWinGestures: MultiFingerGesture.all,
|
||||
pinchZoomThreshold: 0.2,
|
||||
rotationThreshold: 8,
|
||||
zoom: 15,
|
||||
minZoom: 12,
|
||||
maxZoom: 18,
|
||||
),
|
||||
layers: [
|
||||
TileLayerOptions(
|
||||
urlTemplate:
|
||||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
userAgentPackageName: 'edu.furman.now',
|
||||
),
|
||||
],
|
||||
nonRotatedChildren: [
|
||||
AttributionWidget(
|
||||
attributionBuilder: (BuildContext context) {
|
||||
return const ColoredBox(
|
||||
color: Color(0xCCFFFFFF),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Text("©️ OpenStreetMap contributors"),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(left: 10, right: 20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(60)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x33000000),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset("assets/images/bell-tower.svg", color: Theme.of(context).primaryColor, height: 32),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"Search locations",
|
||||
style: furmanTextStyle(TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade500,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// const SizedBox(height: 12),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: const [
|
||||
MapFilterChip(icon: Icons.restaurant, text: "Restaurants"),
|
||||
MapFilterChip(icon: Icons.train, text: "Transportation"),
|
||||
MapFilterChip(icon: Icons.school, text: "Campus Buildings"),
|
||||
],
|
||||
),
|
||||
),
|
||||
MapRotateCompass(rotation: _rotation, resetRotation: resetRotation),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
72
lib/src/screens/student_id/index.dart
Normal file
72
lib/src/screens/student_id/index.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:barcode_widget/barcode_widget.dart';
|
||||
import 'package:furman_now/src/services/get_app/barcode/barcode_service.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
|
||||
class StudentIdScreen extends StatefulWidget {
|
||||
const StudentIdScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StudentIdScreen> createState() => _StudentIdScreenState();
|
||||
}
|
||||
|
||||
class _StudentIdScreenState extends State<StudentIdScreen> {
|
||||
String barcodeNumber = BarcodeService.generateGetBarcode();
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
timer = Timer.periodic(
|
||||
const Duration(seconds: 10),
|
||||
updateBarcode,
|
||||
);
|
||||
}
|
||||
|
||||
void updateBarcode(Timer timer) {
|
||||
setState(() {
|
||||
barcodeNumber = BarcodeService.generateGetBarcode();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: const Color(0xffb7acc9),
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(40),
|
||||
children: [
|
||||
Text(
|
||||
"Furman ID",
|
||||
style: furmanTextStyle(const TextStyle(color: Color(0xff26183d), fontSize: 36, fontWeight: FontWeight.w800)),
|
||||
),
|
||||
const SizedBox(height: 200),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
// hack since the barcode has a weird intrinsic size for some reason
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return BarcodeWidget(
|
||||
barcode: Barcode.pdf417(moduleHeight: 4),
|
||||
data: barcodeNumber,
|
||||
margin: const EdgeInsets.all(10),
|
||||
height: constraints.maxWidth / 3,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
13
lib/src/services/events/event.dart
Normal file
13
lib/src/services/events/event.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class Event {
|
||||
Event({
|
||||
required this.title,
|
||||
required this.time,
|
||||
required this.location,
|
||||
required this.category
|
||||
});
|
||||
|
||||
final String title;
|
||||
final DateTime time;
|
||||
final String location;
|
||||
final String category;
|
||||
}
|
174
lib/src/services/events/events_service.dart
Normal file
174
lib/src/services/events/events_service.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:furman_now/src/services/events/event.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class EventsService {
|
||||
static Future<List<Event>> fetchEvents() async {
|
||||
var athleticsEvents = await fetchAthleticsEvents();
|
||||
var clpEvents = await fetchClpEvents();
|
||||
List<Event> eventsList = List.from(athleticsEvents)..addAll(clpEvents);
|
||||
eventsList.sort((Event a, Event b) => a.time.compareTo(b.time));
|
||||
return eventsList;
|
||||
}
|
||||
|
||||
static Future<Iterable<AthleticsEvent>> fetchAthleticsEvents() async {
|
||||
final response = await http
|
||||
.get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/athleticsGet.php"));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// If the server did return a 200 OK response,
|
||||
// then parse the JSON.
|
||||
final eventsJson = jsonDecode(response.body);
|
||||
return (eventsJson["results"] as List<dynamic>).map((event) =>
|
||||
AthleticsEvent.fromJson(event)
|
||||
);
|
||||
} else {
|
||||
// If the server did not return a 200 OK response,
|
||||
// then throw an exception.
|
||||
throw Exception('Failed to load athletics events.');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Iterable<ClpEvent>> fetchClpEvents() async {
|
||||
final response = await http
|
||||
.get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/clpGet.php"));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// If the server did return a 200 OK response,
|
||||
// then parse the JSON.
|
||||
final eventsJson = jsonDecode(response.body);
|
||||
return (eventsJson["results"] as List<dynamic>).map((event) =>
|
||||
ClpEvent.fromJson(event)
|
||||
);
|
||||
} else {
|
||||
// If the server did not return a 200 OK response,
|
||||
// then throw an exception.
|
||||
throw Exception('Failed to load athletics events.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AthleticsEvent implements Event {
|
||||
@override
|
||||
final String title;
|
||||
@override
|
||||
final DateTime time;
|
||||
@override
|
||||
final String location;
|
||||
@override
|
||||
final String category = "Athletics";
|
||||
|
||||
final String sportTitle;
|
||||
final String sportShort;
|
||||
final AthleticsEventLocation locationIndicator;
|
||||
final String opponent;
|
||||
|
||||
AthleticsEvent._({
|
||||
required this.title,
|
||||
required this.time,
|
||||
required this.location,
|
||||
|
||||
required this.sportTitle,
|
||||
required this.sportShort,
|
||||
required this.locationIndicator,
|
||||
required this.opponent,
|
||||
});
|
||||
|
||||
factory AthleticsEvent({
|
||||
required time,
|
||||
required location,
|
||||
required sportTitle,
|
||||
required sportShort,
|
||||
required locationIndicator,
|
||||
required opponent,
|
||||
}) {
|
||||
// Determine AthleticsEventLocation from string
|
||||
AthleticsEventLocation eventLocationIndicator;
|
||||
switch(locationIndicator) {
|
||||
case ("H"):
|
||||
eventLocationIndicator = AthleticsEventLocation.home;
|
||||
break;
|
||||
case ("A"):
|
||||
eventLocationIndicator = AthleticsEventLocation.away;
|
||||
break;
|
||||
case ("N"):
|
||||
default:
|
||||
eventLocationIndicator = AthleticsEventLocation.neither;
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate event title
|
||||
var eventTitleSeparator =
|
||||
eventLocationIndicator == AthleticsEventLocation.away
|
||||
? "at"
|
||||
: "vs";
|
||||
|
||||
return AthleticsEvent._(
|
||||
title: '$sportTitle $eventTitleSeparator $opponent',
|
||||
time: time,
|
||||
location: location,
|
||||
sportTitle: sportTitle,
|
||||
sportShort: sportShort,
|
||||
locationIndicator: eventLocationIndicator,
|
||||
opponent: opponent,
|
||||
);
|
||||
}
|
||||
|
||||
factory AthleticsEvent.fromJson(Map<String, dynamic> json) {
|
||||
return AthleticsEvent(
|
||||
sportTitle: json["sportTitle"],
|
||||
sportShort: json["sportShort"],
|
||||
time: DateTime.parse(json["eventdate"]),
|
||||
location: json["location"],
|
||||
locationIndicator: json["location_indicator"],
|
||||
opponent: json["opponent"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AthleticsEventLocation {
|
||||
home,
|
||||
away,
|
||||
neither,
|
||||
}
|
||||
|
||||
class ClpEvent implements Event {
|
||||
@override
|
||||
final String title;
|
||||
@override
|
||||
DateTime get time {
|
||||
return startTime;
|
||||
}
|
||||
@override
|
||||
final String location;
|
||||
@override
|
||||
final String category;
|
||||
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final String organization;
|
||||
final String description;
|
||||
|
||||
ClpEvent({
|
||||
required this.title,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.location,
|
||||
required this.category,
|
||||
required this.organization,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
factory ClpEvent.fromJson(Map<String, dynamic> json) {
|
||||
return ClpEvent(
|
||||
title: json["title"],
|
||||
startTime: DateTime.parse(json["date"] + " " + json["start"]),
|
||||
endTime: DateTime.parse(json["date"] + " " + json["end"]),
|
||||
location: json["location"],
|
||||
category: json["eventType"],
|
||||
organization: json["organization"],
|
||||
description: json["description"],
|
||||
);
|
||||
}
|
||||
}
|
90
lib/src/services/get_app/barcode/barcode_service.dart
Normal file
90
lib/src/services/get_app/barcode/barcode_service.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:furman_now/secrets.dart';
|
||||
|
||||
class BarcodeService {
|
||||
static generateGetBarcode({ int? timestamp }) {
|
||||
// seconds since epoch
|
||||
timestamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
// final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
// const timestamp = 1661567550;
|
||||
|
||||
final timestampBytes = _int64BigEndianBytes(timestamp);
|
||||
|
||||
const cbordKey = Secrets.cbordKey;
|
||||
const institutionKey = Secrets.institutionKey;
|
||||
const patronKey = Secrets.testPatronKey;
|
||||
|
||||
final cbordKeyBytes = Uint8List.fromList(hex.decode(utf8.decode(base64Decode(cbordKey))));
|
||||
final institutionKeyBytes = Uint8List.fromList(hex.decode(institutionKey));
|
||||
final patronKeyBytes = _int32LittleEndianBytes(patronKey);
|
||||
final sharedKeyBytes = _xorEncrypt(cbordKeyBytes, institutionKeyBytes);
|
||||
|
||||
final checksumDigit = _luhnGenerate(patronKey.toString());
|
||||
|
||||
// print(hex.encode(_int32BigEndianBytes(timestamp)));
|
||||
final barcodeOtc = int.parse(_hotp(sharedKeyBytes, timestampBytes, 9, hash: sha256));
|
||||
final barcodeOtcBytes = _int32LittleEndianBytes(barcodeOtc);
|
||||
|
||||
final encryptedBytes = _xorEncrypt(patronKeyBytes, barcodeOtcBytes);
|
||||
final encrypted = ByteData.view(encryptedBytes.buffer).getInt32(0, Endian.little);
|
||||
|
||||
final barcodeData = "${timestamp.toString().padLeft(10, "0")}1${encrypted.toString().padLeft(10, "0")}$checksumDigit";
|
||||
|
||||
return barcodeData;
|
||||
}
|
||||
|
||||
static int _luhnGenerate(String code) {
|
||||
const computed = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9];
|
||||
var sum = 0,
|
||||
digit = 0,
|
||||
i = code.length,
|
||||
even = false;
|
||||
|
||||
while (i-- > 0) {
|
||||
digit = int.parse(code[i]);
|
||||
sum += (even = !even) ? computed[digit] : digit;
|
||||
}
|
||||
|
||||
return (sum * 9) % 10;
|
||||
}
|
||||
|
||||
static Uint8List _xorEncrypt(List<int> subject, List<int> key) {
|
||||
var out = Uint8List(subject.length);
|
||||
for (int i = 0; i < subject.length; i++) {
|
||||
out[i] = subject[i] ^ key[i % key.length];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static _hotp(Uint8List key, Uint8List counter, int digits, { Hash hash = sha256 }) {
|
||||
final mac = Uint8List.fromList(Hmac(hash, key).convert(counter).bytes);
|
||||
final offset = mac[mac.length - 1] & 0x0f;
|
||||
final binary = ByteData.view(mac.buffer).getInt32(offset, Endian.big) & 0x7fffffff;
|
||||
var binaryString = binary.toString();
|
||||
if (binaryString.length - digits > 0) {
|
||||
binaryString = binaryString.substring(binaryString.length - digits);
|
||||
}
|
||||
binaryString = binaryString.padLeft(digits, "0");
|
||||
return binaryString;
|
||||
}
|
||||
|
||||
static Uint8List _int32LittleEndianBytes(int value) =>
|
||||
Uint8List(4)..buffer.asByteData().setInt32(0, value, Endian.little);
|
||||
|
||||
static Uint8List _int64BigEndianBytes(int value) {
|
||||
var sendValueBytes = ByteData(8);
|
||||
// setUint64 not implemented on some systems so use setUint32 in
|
||||
// those cases. Leading zeros to pad to equal 64 bit.
|
||||
// Epoch as 32-bit good until 2038 Jan 19 @ 03:14:07
|
||||
try {
|
||||
sendValueBytes.setUint64(0, value, Endian.big);
|
||||
} on UnsupportedError {
|
||||
sendValueBytes.setUint32(0, value, Endian.big);
|
||||
}
|
||||
return sendValueBytes.buffer.asUint8List();
|
||||
}
|
||||
}
|
12
lib/src/services/get_app/barcode/barcode_test.dart
Normal file
12
lib/src/services/get_app/barcode/barcode_test.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import "barcode_service.dart";
|
||||
|
||||
void main(List<String> args) {
|
||||
// while(true) {
|
||||
// print(BarcodeService.generateGetBarcode());
|
||||
// sleep(const Duration(seconds:1));
|
||||
// }
|
||||
|
||||
print(BarcodeService.generateGetBarcode(timestamp: int.parse(args[0])));
|
||||
}
|
66
lib/src/services/get_app/barcode/results.txt
Normal file
66
lib/src/services/get_app/barcode/results.txt
Normal file
@@ -0,0 +1,66 @@
|
||||
1661796958104839794376
|
||||
1661796959104317426886
|
||||
1661796960108255460226
|
||||
1661796961106920860666
|
||||
1661796962106199730706
|
||||
1661796963100729423146
|
||||
1661796964100826751576
|
||||
1661796965105980662356
|
||||
1661796966100792147766
|
||||
1661796967103886508706
|
||||
1661796968101562397906
|
||||
1661796969109347522246
|
||||
1661796970104476755206
|
||||
1661796971106085404376
|
||||
1661796972100553491616
|
||||
1661796973101404143146
|
||||
1661796974105377734796
|
||||
1661796975107065989056
|
||||
1661796976105135326536
|
||||
1661796977100360805266
|
||||
1661796978108364147506
|
||||
1661796979106002091456
|
||||
1661796980100164117926
|
||||
1661796981109290599786
|
||||
1661796982107498877976
|
||||
1661796983100444791306
|
||||
1661796984103380694806
|
||||
1661796985105701206076
|
||||
1661796986103876556676
|
||||
1661796987102846575686
|
||||
1661796988109894924956
|
||||
1661796989109869297986
|
||||
1661796990105289985306
|
||||
1661796991107971540556
|
||||
1661796992109255791256
|
||||
1661796993104515591796
|
||||
1661796994105087857776
|
||||
1661796995100628463616
|
||||
1661796996109645970966
|
||||
1661796997102192728376
|
||||
1661796998101976381966
|
||||
1661796999100836277786
|
||||
1661797000107503534346
|
||||
1661797001100639859056
|
||||
1661797002101528598866
|
||||
1661797003105623778966
|
||||
1661797004107907863076
|
||||
1661797005109315398796
|
||||
1661797006100768643546
|
||||
1661797007101212204946
|
||||
1661797008103273524436
|
||||
1661797009101866267696
|
||||
1661797010102857038416
|
||||
1661797011102685511796
|
||||
1661797012100193444876
|
||||
1661797013103722518116
|
||||
1661797014108411880946
|
||||
1661797015101560642386
|
||||
1661797016109488069706
|
||||
1661797017100723908746
|
||||
1661797018103949619536
|
||||
1661797019109992965496
|
||||
1661797020106962817906
|
||||
1661797021108226414706
|
||||
1661797022106594105606
|
||||
1661797023100233692336
|
3
lib/src/services/get_app/user/user_service.dart
Normal file
3
lib/src/services/get_app/user/user_service.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class UserService {
|
||||
|
||||
}
|
227
lib/src/services/restaurants/restaurant_service.dart
Normal file
227
lib/src/services/restaurants/restaurant_service.dart
Normal file
@@ -0,0 +1,227 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class RestaurantService {
|
||||
static Future<List<Restaurant>> fetchRestaurants() async {
|
||||
var restaurants = (await _fetchRestaurants()).toList();
|
||||
await _fetchRestaurantHours(restaurants);
|
||||
return restaurants;
|
||||
}
|
||||
|
||||
static Future<Iterable<Restaurant>> _fetchRestaurants() async {
|
||||
final response = await http
|
||||
.get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/restaurantGet.php"));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// If the server did return a 200 OK response,
|
||||
// then parse the JSON.
|
||||
final restaurantsJson = jsonDecode(response.body);
|
||||
return (restaurantsJson["results"] as List<dynamic>).map((restaurant) =>
|
||||
Restaurant.fromJson(restaurant)
|
||||
);
|
||||
} else {
|
||||
// If the server did not return a 200 OK response,
|
||||
// then throw an exception.
|
||||
throw Exception('Failed to load athletics events.');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _fetchRestaurantHours(List<Restaurant> restaurants) async {
|
||||
final response = await http
|
||||
.get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/restaurantHoursGet.php"));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// If the server did return a 200 OK response,
|
||||
// then parse the JSON.
|
||||
final hoursListJson = jsonDecode(response.body);
|
||||
for (var hoursJson in (hoursListJson["results"] as List<dynamic>)) {
|
||||
var hours = Hours.fromJson(hoursJson);
|
||||
restaurants.firstWhere((restaurant) => restaurant.id == hours.id).hoursList.add(hours);
|
||||
}
|
||||
} else {
|
||||
// If the server did not return a 200 OK response,
|
||||
// then throw an exception.
|
||||
throw Exception('Failed to load athletics events.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Restaurant {
|
||||
int id;
|
||||
String name;
|
||||
String? shortName;
|
||||
String location;
|
||||
LatLng mapLocation;
|
||||
int frequency;
|
||||
int busyness;
|
||||
String? url;
|
||||
List<Hours> hoursList = <Hours>[];
|
||||
|
||||
Restaurant({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.shortName,
|
||||
required this.location,
|
||||
required this.mapLocation,
|
||||
required this.frequency,
|
||||
required this.busyness,
|
||||
this.url,
|
||||
});
|
||||
|
||||
bool get isOpen {
|
||||
for(var hours in hoursList) {
|
||||
var now = DateTime.now();
|
||||
var today = Weekday.values[now.weekday - 1];
|
||||
if (hours.daysOfWeek.days[today] == true) {
|
||||
var currentTime = Duration(hours: now.hour, minutes: now.minute, seconds: now.second);
|
||||
if(hours.startTime != null && hours.endTime != null) {
|
||||
if (hours.startTime! < currentTime && hours.endTime! > currentTime) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: this is the absolute worst
|
||||
String get imageUri {
|
||||
return "https://cs.furman.edu/~csdaemon/FUNow/appIcons/$name Icon.png";
|
||||
}
|
||||
|
||||
ImageProvider get image {
|
||||
return NetworkImage(imageUri);
|
||||
}
|
||||
|
||||
factory Restaurant.fromJson(Map<String, dynamic> json) {
|
||||
return Restaurant(
|
||||
id: int.parse(json["id"]),
|
||||
name: json["fullname"],
|
||||
shortName: json["name"],
|
||||
location: json["location"],
|
||||
mapLocation: LatLng(double.parse(json["latitude"]), double.parse(json["longitude"])),
|
||||
frequency: int.parse(json["frequency"]),
|
||||
busyness: int.parse(json["busyness"]),
|
||||
url: (json["url"] != null && (json["url"] as String).isNotEmpty) ? json["url"] : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Hours {
|
||||
int id;
|
||||
String? meal;
|
||||
Duration? startTime;
|
||||
Duration? endTime;
|
||||
DaysOfWeek daysOfWeek;
|
||||
int dayOrder;
|
||||
|
||||
Hours({
|
||||
required this.id,
|
||||
this.meal,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
required this.daysOfWeek,
|
||||
required this.dayOrder,
|
||||
});
|
||||
|
||||
static Duration _getDurationFromString(String s) {
|
||||
var timeComponents = s.split(":");
|
||||
var hours = int.parse(timeComponents[0]);
|
||||
var minutes = int.parse(timeComponents[1]);
|
||||
var seconds = int.parse(timeComponents[2]);
|
||||
|
||||
return Duration(hours: hours, minutes: minutes, seconds: seconds);
|
||||
}
|
||||
|
||||
factory Hours.fromJson(Map<String, dynamic> json) {
|
||||
return Hours(
|
||||
id: int.parse(json["id"]),
|
||||
meal: json["meal"],
|
||||
startTime: (json["start"] != null) ? _getDurationFromString(json["start"]) : null,
|
||||
endTime: (json["end"] != null) ? _getDurationFromString(json["end"]) : null,
|
||||
daysOfWeek: DaysOfWeek.parse(json["dayOfWeek"]),
|
||||
dayOrder: int.parse(json["dayOrder"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DaysOfWeek {
|
||||
final Weekday startDay;
|
||||
final Weekday endDay;
|
||||
final Map<Weekday, bool> days;
|
||||
|
||||
DaysOfWeek({
|
||||
required this.startDay,
|
||||
required this.endDay,
|
||||
required this.days,
|
||||
});
|
||||
|
||||
static Weekday _getDayFromShortName(String shortName) {
|
||||
switch(shortName.toLowerCase()) {
|
||||
case "mon":
|
||||
return Weekday.monday;
|
||||
case "tue":
|
||||
return Weekday.tuesday;
|
||||
case "wed":
|
||||
return Weekday.wednesday;
|
||||
case "thu":
|
||||
return Weekday.thursday;
|
||||
case "fri":
|
||||
return Weekday.friday;
|
||||
case "sat":
|
||||
return Weekday.saturday;
|
||||
case "sun":
|
||||
return Weekday.sunday;
|
||||
default:
|
||||
throw "Invalid date short name.";
|
||||
}
|
||||
}
|
||||
|
||||
factory DaysOfWeek.parse(String s) {
|
||||
final Map<Weekday, bool> days = { for (var e in Weekday.values) e : false };
|
||||
|
||||
// single day
|
||||
if (!s.contains("-")) {
|
||||
var day = _getDayFromShortName(s);
|
||||
days.update(day, (val) => true);
|
||||
return DaysOfWeek(startDay: day, endDay: day, days: days);
|
||||
}
|
||||
|
||||
// date range
|
||||
var dayNames = s.split("-");
|
||||
final startDay = _getDayFromShortName(dayNames[0]);
|
||||
final endDay = _getDayFromShortName(dayNames[1]);
|
||||
int i = startDay.index;
|
||||
|
||||
// loop through to add all dates in date range to list
|
||||
while(i != endDay.index) {
|
||||
days.update(Weekday.values[i], (val) => true);
|
||||
|
||||
if (i < Weekday.values.length - 1) {
|
||||
i++;
|
||||
} else {
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
days.update(endDay, (val) => true);
|
||||
|
||||
return DaysOfWeek(
|
||||
startDay: startDay,
|
||||
endDay: endDay,
|
||||
days: days,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum Weekday {
|
||||
monday,
|
||||
tuesday,
|
||||
wednesday,
|
||||
thursday,
|
||||
friday,
|
||||
saturday,
|
||||
sunday,
|
||||
}
|
29
lib/src/services/restaurants/restaurant_test.dart
Normal file
29
lib/src/services/restaurants/restaurant_test.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import "restaurant_service.dart";
|
||||
|
||||
void main() async {
|
||||
var restaurants = await RestaurantService.fetchRestaurants();
|
||||
for (var restaurant in restaurants) {
|
||||
print(restaurant.name + " " + (restaurant.isOpen ? "is open" : "is closed"));
|
||||
}
|
||||
// print("Start day: ${restaurants.firstWhere((restaurant) => restaurant.id == 22).hoursList[0].daysOfWeek.startDay}");
|
||||
// print("End day: ${restaurants.firstWhere((restaurant) => restaurant.id == 22).hoursList[0].daysOfWeek.endDay}");
|
||||
// print("Days of week: ${restaurants.firstWhere((restaurant) => restaurant.id == 22).hoursList[0].daysOfWeek.days}");
|
||||
|
||||
// var testRestaurant = Restaurant.fromJson(jsonDecode('''
|
||||
// {
|
||||
// "id":"10",
|
||||
// "name":null,
|
||||
// "fullname":"Papa John's Pizza",
|
||||
// "location":"Off Campus: 1507 Poinsett Hwy",
|
||||
// "latitude":"34.887977","longitude":"-82.405793",
|
||||
// "frequency":"10",
|
||||
// "busyness":"0",
|
||||
// "url":"https:\/\/www.papajohns.com\/order\/stores-near-me"
|
||||
// }
|
||||
// '''));
|
||||
|
||||
// var testDaysOfWeek = DaysOfWeek.parse("Sat-Mon");
|
||||
// print(testDaysOfWeek.days);
|
||||
}
|
3
lib/src/services/transportation/bus_503.dart
Normal file
3
lib/src/services/transportation/bus_503.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class TransportationBus503Service {
|
||||
|
||||
}
|
3
lib/src/services/transportation/saferide_shuttle.dart
Normal file
3
lib/src/services/transportation/saferide_shuttle.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class TransportationSafeRideShuttleService {
|
||||
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
class TransportationService {
|
||||
|
||||
}
|
19
lib/src/utils/date_range.dart
Normal file
19
lib/src/utils/date_range.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
DateTimeRange constructDateRange(DateTime startDateTime, DateTime endDateTime) {
|
||||
var startDate = DateTime(
|
||||
startDateTime.year,
|
||||
startDateTime.month,
|
||||
startDateTime.day,
|
||||
);
|
||||
var endDate = DateTime(
|
||||
endDateTime.year,
|
||||
endDateTime.month,
|
||||
endDateTime.day,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
|
||||
return DateTimeRange(start: startDate, end: endDate);
|
||||
}
|
12
lib/src/utils/greeting.dart
Normal file
12
lib/src/utils/greeting.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
String greeting() {
|
||||
var currentHour = DateTime.now().hour;
|
||||
if (currentHour > 4 && currentHour < 11) {
|
||||
return "Good morning";
|
||||
} else if (currentHour >= 11 && currentHour < 17) {
|
||||
return "Good afternoon";
|
||||
} else if (currentHour >= 17 && currentHour < 21) {
|
||||
return "Good evening";
|
||||
} else {
|
||||
return "Good night";
|
||||
}
|
||||
}
|
23
lib/src/utils/theme.dart
Normal file
23
lib/src/utils/theme.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
ThemeData _baseTheme = ThemeData(
|
||||
primarySwatch: Colors.deepPurple,
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: Colors.grey[100],
|
||||
unselectedItemColor: Colors.grey[500],
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
subtitle2: TextStyle(
|
||||
color: Colors.grey[500],
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
ThemeData myFurmanTheme = _baseTheme.copyWith(
|
||||
textTheme: GoogleFonts.interTextTheme(_baseTheme.textTheme),
|
||||
);
|
||||
|
||||
var furmanTextStyle = (TextStyle baseStyle) => GoogleFonts.inter(textStyle: baseStyle);
|
54
lib/src/widgets/header.dart
Normal file
54
lib/src/widgets/header.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
|
||||
class HeaderWidget extends StatelessWidget {
|
||||
const HeaderWidget({
|
||||
required this.title,
|
||||
this.link,
|
||||
Key? key
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final HeaderLink? link;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 40, right: 40, top: 20, bottom: 15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: furmanTextStyle(TextStyle(
|
||||
color: Colors.grey[800],
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
)),
|
||||
),
|
||||
if (link != null)
|
||||
Text(
|
||||
link!.text,
|
||||
style: furmanTextStyle(const TextStyle(
|
||||
color: Color(0xff755898),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class HeaderLink {
|
||||
final String text;
|
||||
final String href;
|
||||
|
||||
const HeaderLink({
|
||||
required this.text,
|
||||
required this.href,
|
||||
});
|
||||
}
|
97
lib/src/widgets/home/events/event_card.dart
Normal file
97
lib/src/widgets/home/events/event_card.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/services/events/event.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class EventCard extends StatelessWidget {
|
||||
const EventCard
|
||||
(
|
||||
this.event,
|
||||
{
|
||||
Key? key
|
||||
}
|
||||
) : super(key: key);
|
||||
|
||||
final Event event;
|
||||
|
||||
String get eventHour {
|
||||
var formatter = DateFormat('hh:mm');
|
||||
return formatter.format(event.time);
|
||||
}
|
||||
|
||||
String get eventAmPm {
|
||||
var formatter = DateFormat('a');
|
||||
return formatter.format(event.time);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xfff9f9fb),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(eventHour, style: furmanTextStyle(const TextStyle(fontWeight: FontWeight.w700))),
|
||||
Text(eventAmPm, style: Theme.of(context).textTheme.subtitle2),
|
||||
],
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 2,
|
||||
thickness: 2,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(event.title, style: furmanTextStyle(const TextStyle(fontWeight: FontWeight.w600))),
|
||||
const SizedBox(height: 6),
|
||||
RichText(text: TextSpan(
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 5.0),
|
||||
child: Icon(Icons.place_outlined, size: 20, color: Colors.grey[500])
|
||||
),
|
||||
),
|
||||
TextSpan(text: event.location),
|
||||
],
|
||||
)),
|
||||
const SizedBox(height: 2),
|
||||
RichText(text: TextSpan(
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 1.0, right: 6.0),
|
||||
child: Icon(Icons.sell_outlined, size: 18, color: Colors.grey[500])
|
||||
),
|
||||
),
|
||||
TextSpan(text: event.category),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
95
lib/src/widgets/home/events/events_list.dart
Normal file
95
lib/src/widgets/home/events/events_list.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/services/events/event.dart';
|
||||
import 'package:furman_now/src/services/events/events_service.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
|
||||
import 'event_card.dart';
|
||||
|
||||
class EventsList extends StatefulWidget {
|
||||
final DateTimeRange dateRange;
|
||||
|
||||
const EventsList._({
|
||||
required this.dateRange,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
factory EventsList({
|
||||
DateTimeRange? dateRange,
|
||||
Key? key,
|
||||
}) {
|
||||
if (dateRange == null) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final tonight = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
dateRange = DateTimeRange(start: today, end: tonight);
|
||||
}
|
||||
return EventsList._(
|
||||
dateRange: dateRange,
|
||||
key: key,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<EventsList> createState() => _EventsListState();
|
||||
}
|
||||
|
||||
class _EventsListState extends State<EventsList> {
|
||||
late Future<Iterable<Event>> futureEventList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
futureEventList = EventsService.fetchEvents();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Iterable<Event>>(
|
||||
future: futureEventList,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var events = snapshot.data!.where((event) {
|
||||
return event.time.isAfter(widget.dateRange.start) && event.time.isBefore(widget.dateRange.end);
|
||||
});
|
||||
if (events.isNotEmpty) {
|
||||
return Column(
|
||||
children: events.map((event) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 15),
|
||||
child: EventCard(event),
|
||||
);
|
||||
}).toList());
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 15),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No events today :(",
|
||||
style: furmanTextStyle(TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (snapshot.hasError) {
|
||||
return Text('${snapshot.error}', style: const TextStyle(color: Colors.red),);
|
||||
}
|
||||
|
||||
// By default, show a loading spinner.
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 25),
|
||||
child: CircularProgressIndicator()
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
110
lib/src/widgets/home/restaurants/restaurant_card.dart
Normal file
110
lib/src/widgets/home/restaurants/restaurant_card.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:furman_now/src/services/restaurants/restaurant_service.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
|
||||
class RestaurantCard extends StatefulWidget {
|
||||
const RestaurantCard({
|
||||
required this.restaurant,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final Restaurant restaurant;
|
||||
|
||||
@override
|
||||
State<RestaurantCard> createState() => _RestaurantCardState();
|
||||
}
|
||||
|
||||
class _RestaurantCardState extends State<RestaurantCard> {
|
||||
PaletteGenerator? paletteGenerator;
|
||||
Color? backgroundColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updatePaletteGenerator();
|
||||
}
|
||||
|
||||
Future<void> _updatePaletteGenerator() async {
|
||||
paletteGenerator = await PaletteGenerator.fromImageProvider(
|
||||
widget.restaurant.image,
|
||||
maximumColorCount: 1,
|
||||
);
|
||||
if (paletteGenerator != null && paletteGenerator!.colors.toList().isNotEmpty) {
|
||||
var hslColor = HSLColor.fromColor(paletteGenerator!.colors.toList()[0]);
|
||||
var newColor = HSLColor.fromAHSL(1, hslColor.hue, 40/100, 90/100);
|
||||
backgroundColor = newColor.toColor();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 150,
|
||||
height: 175,
|
||||
decoration: BoxDecoration(
|
||||
// color: Color(0xfff9f9fb),
|
||||
color: backgroundColor ?? const Color(0xfff9f9fb),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: furmanTextStyle(TextStyle(
|
||||
color: Colors.grey[800],
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
)),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Container(
|
||||
width: 9,
|
||||
height: 9,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.restaurant.isOpen ? Colors.green : Colors.red.shade700,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: widget.restaurant.isOpen ? "Open" : "Closed"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
child: Image.network(widget.restaurant.imageUri, height: 90),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
widget.restaurant.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: furmanTextStyle(TextStyle(
|
||||
color: Colors.grey[800],
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
63
lib/src/widgets/home/restaurants/restaurants_list.dart
Normal file
63
lib/src/widgets/home/restaurants/restaurants_list.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/services/restaurants/restaurant_service.dart';
|
||||
|
||||
import 'restaurant_card.dart';
|
||||
|
||||
class RestaurantsList extends StatefulWidget {
|
||||
const RestaurantsList({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RestaurantsList> createState() => _RestaurantsListState();
|
||||
}
|
||||
|
||||
class _RestaurantsListState extends State<RestaurantsList> {
|
||||
late Future<List<Restaurant>> futureEventList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
futureEventList = RestaurantService.fetchRestaurants();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<Restaurant>>(
|
||||
future: futureEventList,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var restaurants = snapshot.data!..sort((r1, r2) => (r1.isOpen ? 0 : 1) - (r2.isOpen ? 0 : 1));
|
||||
return SizedBox(
|
||||
height: 175,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
itemCount: restaurants.length,
|
||||
cacheExtent: 10000,
|
||||
scrollDirection: Axis.horizontal,
|
||||
prototypeItem: Padding(
|
||||
padding: const EdgeInsets.only(right: 15),
|
||||
child: RestaurantCard(restaurant: restaurants.first),
|
||||
),
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(right: 15),
|
||||
child: RestaurantCard(restaurant: restaurants[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text('${snapshot.error}', style: const TextStyle(color: Colors.red))
|
||||
);
|
||||
}
|
||||
|
||||
// By default, show a loading spinner.
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 25),
|
||||
child: CircularProgressIndicator()
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
78
lib/src/widgets/home/transportation/transportation_card.dart
Normal file
78
lib/src/widgets/home/transportation/transportation_card.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
|
||||
class TransportationCard extends StatelessWidget {
|
||||
const TransportationCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xfff9f9fb),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: Icon(Icons.directions_bus, size: 50, color: Colors.grey[800]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: furmanTextStyle(TextStyle(
|
||||
color: Colors.grey[800],
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
)),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Container(
|
||||
width: 9,
|
||||
height: 9,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const TextSpan(text: "Running"),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"503 Bus",
|
||||
style: furmanTextStyle(TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
fontSize: 18,
|
||||
)),
|
||||
),
|
||||
Text(
|
||||
"Next stop: Pointsett Hwy & Crestwood Dr",
|
||||
style: furmanTextStyle(TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[500],
|
||||
fontSize: 12,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
46
lib/src/widgets/info/info_card.dart
Normal file
46
lib/src/widgets/info/info_card.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InfoCard extends StatelessWidget {
|
||||
final Color color;
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
const InfoCard({
|
||||
required this.color,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.description,
|
||||
Key? key
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 15, right: 5),
|
||||
child: Icon(icon, size: 40,)
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge,),
|
||||
Text(description),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
55
lib/src/widgets/map/filter_chip.dart
Normal file
55
lib/src/widgets/map/filter_chip.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
|
||||
class MapFilterChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
const MapFilterChip({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
Key? key
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6, bottom: 6, left: 8, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x33000000),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Icon(icon,
|
||||
size: 18,
|
||||
color: Colors.grey.shade800),
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
const WidgetSpan(child: SizedBox(width: 6)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Text(
|
||||
text,
|
||||
style: furmanTextStyle(TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
37
lib/src/widgets/map/rotate_compass.dart
Normal file
37
lib/src/widgets/map/rotate_compass.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class MapRotateCompass extends StatelessWidget {
|
||||
final double rotation;
|
||||
final void Function()? resetRotation;
|
||||
|
||||
@override
|
||||
const MapRotateCompass({
|
||||
required this.rotation,
|
||||
required this.resetRotation,
|
||||
Key? key
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: rotation != 0.0 ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, right: 12),
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.white,
|
||||
mini: true,
|
||||
onPressed: resetRotation,
|
||||
child: Transform.rotate(
|
||||
angle: rotation,
|
||||
child: SvgPicture.asset("assets/images/compass.svg", height: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
22
lib/src/widgets/scroll_view_height.dart
Normal file
22
lib/src/widgets/scroll_view_height.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScrollViewWithHeight extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const ScrollViewWithHeight({
|
||||
required this.child,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: constraints.copyWith(minHeight: constraints.maxHeight, maxHeight: double.infinity),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user