Created
February 21, 2020 18:09
-
-
Save Levi-Lesches/55b6fc50b4592365e9929a1a01115d71 to your computer and use it in GitHub Desktop.
Ram Life admin sports builder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import "package:flutter/material.dart"; | |
enum Sport {baseball, basketball, hockey, tennis, volleyball, soccer} | |
const Map<String, Sport> stringToSports = { | |
"baseball": Sport.baseball, | |
"basketball": Sport.basketball, | |
"hockey": Sport.hockey, | |
"tennis": Sport.tennis, | |
"volleyball": Sport.volleyball, | |
"soccer": Sport.soccer, | |
}; | |
/// The hour and minute representation of a time. | |
/// | |
/// This is used instead of [Flutter's TimeOfDay](https://api.flutter.dev/flutter/material/TimeOfDay-class.html) | |
/// to provide the `>` and `<` operators. | |
@immutable | |
class Time { | |
/// The hour in 24-hour format. | |
final int hour; | |
/// The minutes. | |
final int minutes; | |
/// A const constructor. | |
const Time (this.hour, this.minutes); | |
/// Simplifies a [DateTime] object to a [Time]. | |
Time.fromDateTime (DateTime date) : | |
hour = date.hour, | |
minutes = date.minute; | |
/// Returns a new [Time] object from JSON data. | |
/// | |
/// The json must have `hour` and `minutes` fields that map to integers. | |
Time.fromJson(Map<String, dynamic> json) : | |
hour = json ["hour"], | |
minutes = json ["minutes"]; | |
/// Returns this obect in JSON form | |
Map<String, dynamic> toJson() => { | |
"hour": hour, | |
"minutes": minutes, | |
}; | |
@override | |
int get hashCode => toString().hashCode; | |
@override | |
bool operator == (dynamic other) => other.runtimeType == Time && | |
other.hour == hour && | |
other.minutes == minutes; | |
/// Returns whether this [Time] is before another [Time]. | |
bool operator < (Time other) => hour < other.hour || | |
(hour == other.hour && minutes < other.minutes); | |
/// Returns whether this [Time] is at or before another [Time]. | |
bool operator <= (Time other) => this < other || this == other; | |
/// Returns whether this [Time] is after another [Time]. | |
bool operator > (Time other) => hour > other.hour || | |
(hour == other.hour && minutes > other.minutes); | |
/// Returns whether this [Time] is at or after another [Time]. | |
bool operator >= (Time other) => this > other || this == other; | |
@override | |
String toString() => | |
"${hour > 12 ? hour - 12 : hour}:${minutes.toString().padLeft(2, '0')}"; | |
} | |
/// A range of times. | |
@immutable | |
class Range { | |
/// When this range starts. | |
final Time start; | |
/// When this range ends. | |
final Time end; | |
/// Provides a const constructor. | |
const Range (this.start, this.end); | |
/// Convenience method for manually creating a range by hand. | |
Range.nums ( | |
int startHour, | |
int startMinute, | |
int endHour, | |
int endMinute | |
) : | |
start = Time (startHour, startMinute), | |
end = Time (endHour, endMinute); | |
/// Returns a new [Range] from JSON data | |
/// | |
/// The json must have `start` and `end` fields | |
/// that map to [Time] JSON objects. | |
/// See [Time.fromJson] for more details. | |
Range.fromJson(Map<String, dynamic> json) : | |
start = Time.fromJson(Map<String, dynamic>.from(json ["start"])), | |
end = Time.fromJson(Map<String, dynamic>.from(json ["end"])); | |
/// Returns a JSON representation of this range. | |
Map<String, dynamic> toJson() => { | |
"start": start.toJson(), | |
"end": end.toJson(), | |
}; | |
/// Returns whether [other] is in this range. | |
bool contains (Time other) => start <= other && other <= end; | |
@override String toString() => "$start-$end"; | |
/// Returns whether this range is before another range. | |
bool operator < (Time other) => end.hour < other.hour || | |
( | |
end.hour == other.hour && | |
end.minutes < other.minutes | |
); | |
/// Returns whether this range is after another range. | |
bool operator > (Time other) => start.hour > other.hour || | |
( | |
start.hour == other.hour && | |
start.minutes > other.minutes | |
); | |
} | |
@immutable | |
class Scores { | |
final int ramazScore, otherScore; | |
final bool isHome; | |
const Scores(this.ramazScore, this.otherScore, {@required this.isHome}); | |
Scores.fromJson(Map<String, dynamic> json) : | |
isHome = json ["isHome"], | |
ramazScore = json ["ramaz"], | |
otherScore = json ["other"]; | |
@override | |
String toString() => "Ramaz: $ramazScore, Other: $otherScore"; | |
bool get didDraw => ramazScore == otherScore; | |
bool get didWin => ramazScore > otherScore; | |
int getScore({bool home}) => home == isHome | |
? ramazScore : otherScore; | |
} | |
@immutable | |
class SportsGame { | |
static List<SportsGame> fromList(List<Map<String, dynamic>> listJson) => [ | |
for (final Map<String, dynamic> json in listJson) | |
SportsGame.fromJson(json) | |
]; | |
final Sport sport; | |
final DateTime date; | |
final Range times; | |
final String team, opponent; | |
final bool home; | |
final Scores scores; | |
const SportsGame({ | |
@required this.sport, | |
@required this.date, | |
@required this.times, | |
@required this.team, | |
@required this.opponent, | |
@required this.home, | |
this.scores, | |
}); | |
SportsGame.fromJson(Map<String, dynamic> json) : | |
sport = stringToSports [json ["sport"]], | |
date = DateTime.parse(json ["date"]), | |
times = Range.fromJson(json ["times"]), | |
team = json ["team"], | |
home = json ["home"], | |
opponent = json ["opponent"], | |
scores = Scores.fromJson( | |
Map<String, dynamic>.from(json ["scores"]) | |
); | |
String get homeTeam => home ? "Ramaz" : opponent; | |
String get awayTeam => home ? opponent : "Ramaz"; | |
String get description => "$awayTeam @ $homeTeam"; | |
} | |
class SportsStats extends StatelessWidget { | |
final String team, dateTime; | |
final int score; | |
const SportsStats({ | |
@required this.team, | |
@required this.dateTime, | |
@required this.score, | |
}); | |
@override | |
Widget build(BuildContext context) => Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Text(team), | |
const Spacer(flex: 2), | |
Text(score?.toString() ?? ""), | |
const Spacer(flex: 3), | |
Text(dateTime), | |
const Spacer(), | |
] | |
); | |
} | |
class SportsScoreUpdater extends StatefulWidget { | |
static Future<Scores> updateGame( | |
BuildContext context, | |
SportsGame game | |
) => showDialog<Scores>( | |
context: context, | |
builder: (_) => SportsScoreUpdater(game), | |
); | |
final SportsGame game; | |
const SportsScoreUpdater(this.game); | |
@override | |
ScoreUpdaterState createState() => ScoreUpdaterState(); | |
} | |
class ScoreUpdaterState extends State<SportsScoreUpdater> { | |
TextEditingController ramazController, otherController; | |
Scores get scores => Scores( | |
int.parse(ramazController.text), | |
int.parse(otherController.text), | |
isHome: widget.game.home, | |
); | |
bool get ready => ramazController.text.isNotEmpty && | |
otherController.text.isNotEmpty; | |
@override | |
void initState() { | |
super.initState(); | |
ramazController = TextEditingController(); | |
otherController = TextEditingController(); | |
} | |
@override | |
Widget build(BuildContext context) => AlertDialog( | |
title: const Text("Update Scores"), | |
content: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
const Text("Ramaz"), | |
const SizedBox(width: 50), | |
Text(widget.game.opponent), | |
] | |
), | |
const SizedBox(height: 20), | |
Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
SizedBox( | |
width: 20, | |
child: TextField( | |
controller: ramazController, | |
onChanged: (_) => setState(() {}), | |
) | |
), | |
const SizedBox(width: 50), | |
SizedBox( | |
width: 20, | |
child: TextField( | |
controller: otherController, | |
onChanged: (_) => setState(() {}), | |
) | |
), | |
] | |
) | |
] | |
), | |
actions: [ | |
FlatButton( | |
onPressed: () => Navigator.of(context).pop(), | |
child: const Text("Cancel"), | |
), | |
RaisedButton( | |
onPressed: !ready ? null : () => Navigator.of(context).pop(scores), | |
child: const Text("Save"), | |
) | |
] | |
); | |
} | |
class SportsTile extends StatelessWidget { | |
final SportsGame game; | |
final void Function(Scores) updateScores; | |
const SportsTile(this.game, [this.updateScores]); | |
String get icon { | |
switch (game.sport) { | |
case Sport.baseball: return "B"; | |
case Sport.basketball: return "B"; | |
case Sport.soccer: return "S"; | |
case Sport.hockey: return "H"; | |
case Sport.tennis: return "T"; | |
case Sport.volleyball: return "V"; | |
} | |
return ""; // no default to keep static analysis | |
} | |
Color get cardColor => game.scores != null | |
? (game.scores.didDraw | |
? Colors.blueGrey | |
: (game.scores.didWin ? Colors.lightGreen : Colors.red [400]) | |
) : null; | |
String formatDate(DateTime date) => | |
"${date.month}-${date.day}-${date.year}"; | |
int get padLength => game.opponent.length > "Ramaz".length | |
? game.opponent.length : "Ramaz".length; | |
TimeOfDay getTime(Time time) => time == null ? null : TimeOfDay(hour: time.hour, minute: time.minutes); | |
@override | |
Widget build(BuildContext context) => SizedBox( | |
height: 170, | |
child: Card( | |
color: cardColor, | |
child: InkWell( | |
onTap: updateScores == null ? null : () async => updateScores( | |
await SportsScoreUpdater.updateGame(context, game) | |
), | |
child: Padding( | |
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
ListTile( | |
leading: CircleAvatar(child: Text(icon)), | |
title: Text(game.team ?? ""), | |
subtitle: Text(game.home | |
? "${game.opponent} @ Ramaz" | |
: "Ramaz @ ${game.opponent}" | |
), | |
), | |
const SizedBox(height: 20), | |
SportsStats( | |
team: game.awayTeam?.padRight(padLength), | |
score: game.scores?.getScore(home: false), | |
dateTime: game.date == null ? "" : formatDate(game.date), | |
), | |
const SizedBox(height: 10), | |
SportsStats( | |
team: game.homeTeam?.padRight(padLength), | |
score: game.scores?.getScore(home: true), | |
dateTime: | |
"${getTime(game.times.start)?.format(context) ?? ''} - " | |
"${getTime(game.times.end)?.format(context) ?? ''}" | |
), | |
] | |
) | |
), | |
) | |
) | |
); | |
} | |
class FormRow extends StatelessWidget { | |
final Widget a, b; | |
final bool spaced; | |
const FormRow(this.a, this.b, {this.spaced = false}); | |
@override | |
Widget build(BuildContext context) => Column( | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: [ | |
a, | |
Spacer(), | |
spaced ? SizedBox(width: 300, child: b) : b | |
] | |
), | |
SizedBox(height: 20), | |
] | |
); | |
} | |
class EditableField<T> extends StatelessWidget { | |
final String label; | |
final String value; | |
final IconData whenNull; | |
final void Function() setNewValue; | |
const EditableField({ | |
@required this.label, | |
@required this.value, | |
@required this.whenNull, | |
@required this.setNewValue | |
}); | |
@override | |
Widget build(BuildContext context) => FormRow( | |
Text(label), | |
value == null | |
? IconButton( | |
icon: Icon(whenNull), | |
onPressed: setNewValue | |
) | |
: InkWell( | |
onTap: setNewValue, | |
child: Text( | |
value, | |
style: TextStyle(color: Colors.blue), | |
), | |
) | |
); | |
} | |
class SportGameForm extends StatefulWidget { | |
@override | |
SportGameFormState createState() => SportGameFormState(); | |
} | |
class SportGameFormState extends State<SportGameForm> { | |
Sport sport; | |
String team; | |
bool away = false; | |
DateTime date; | |
TimeOfDay start, end; | |
final TextEditingController opponentController = TextEditingController(); | |
Time getTime(TimeOfDay time) => time == null ? null : Time(time.hour, time.minute); | |
bool get ready => sport != null && | |
team != null && | |
away != null && | |
date != null && | |
start != null && | |
end != null && | |
opponentController.text.isNotEmpty; | |
SportsGame get game => SportsGame( | |
date: date, | |
home: !away, | |
times: Range(getTime(start), getTime(end)), | |
team: team ?? "", | |
opponent: opponentController.text, | |
sport: sport, | |
); | |
void setDate() async { | |
final DateTime selected = await showDatePicker( | |
firstDate: DateTime(2019, 09, 01), | |
lastDate: DateTime(2020, 06, 30), | |
initialDate: DateTime.now(), | |
context: context | |
); | |
setState(() => date = selected); | |
} | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar( | |
title: Text("Add game"), | |
), | |
body: Form( | |
child: ListView( | |
padding: EdgeInsets.all(20), | |
children: [ | |
FormRow( | |
Text("Sport"), | |
DropdownButtonFormField<Sport>( | |
hint: Text("Choose a sport"), | |
value: sport, | |
onChanged: (Sport value) => setState(() => sport = value), | |
items: [ | |
for (final Sport sport in Sport.values) | |
DropdownMenuItem<Sport>( | |
value: sport, | |
child: Text(sport.toString().split(".") [1]) | |
) | |
], | |
), | |
spaced: true, | |
), | |
const SizedBox(height: 10), | |
FormRow( | |
Text("Team"), | |
DropdownButtonFormField<String>( | |
itemHeight: 70, | |
hint: Text("Choose a Team"), | |
value: team, | |
onChanged: (String value) => setState(() => team = value), | |
items: [ | |
for (final String team in teams) | |
DropdownMenuItem<String>(value: team, child: Text(team)), | |
DropdownMenuItem<String>( | |
value: "", | |
child: SizedBox( | |
width: 150, | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Icon(Icons.add), | |
SizedBox(width: 10), | |
Text("Add team"), | |
] | |
) | |
) | |
) | |
] | |
), | |
spaced: true, | |
), | |
FormRow( | |
Text("Opponent"), | |
TextField( | |
controller: opponentController, | |
onChanged: (_) => setState(() {}), | |
), | |
spaced: true, | |
), | |
FormRow( | |
Text("Away game"), | |
Checkbox( | |
onChanged: (bool value) => setState(() => away = value), | |
value: away, | |
), | |
), | |
EditableField( | |
label: "Date", | |
value: date == null ? null : "${date.month}-${date.day}-${date.year}", | |
whenNull: Icons.date_range, | |
setNewValue: setDate, | |
), | |
EditableField( | |
label: "Start time", | |
value: start?.format(context), | |
whenNull: Icons.access_time, | |
setNewValue: () async { | |
final TimeOfDay newTime = await showTimePicker( | |
context: context, | |
initialTime: start ?? TimeOfDay.now(), | |
); | |
setState(() => start = newTime); | |
}, | |
), | |
EditableField( | |
label: "End time", | |
value: end?.format(context), | |
whenNull: Icons.access_time, | |
setNewValue: () async { | |
final TimeOfDay newTime = await showTimePicker( | |
context: context, | |
initialTime: end ?? TimeOfDay.now(), | |
); | |
setState(() => end = newTime); | |
}, | |
), | |
SizedBox(height: 30), | |
SportsTile(game), | |
SizedBox(height: 20), | |
Row( | |
mainAxisAlignment: MainAxisAlignment.end, | |
children: [ | |
FlatButton( | |
child: Text("Cancel"), | |
onPressed: () => Navigator.of(context).pop() | |
), | |
RaisedButton( | |
child: Text("Save"), | |
onPressed: ready ? () => Navigator.of(context).pop(game) : null, | |
) | |
] | |
) | |
] | |
) | |
) | |
); | |
} | |
class MainPage extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar(title: Text("Admin page")), | |
body: Center( | |
child: FlatButton.icon( | |
icon: Icon(Icons.add), | |
label: Text("Add a new game"), | |
onPressed: () async => print( | |
(await Navigator.of(context).push<SportsGame>( | |
MaterialPageRoute( | |
builder: (_) => SportGameForm() | |
) | |
))?.description ?? "Cancelled"), | |
) | |
) | |
); | |
} | |
List<String> teams = ["Boys Varsity basketball", "Girls Varsity volleyball", "(other teams)"]; | |
void main() => runApp( | |
MaterialApp( | |
theme: ThemeData( | |
primaryColor: Color(0xff004b8d), | |
accentColor: Color(0xfff9ca15), | |
buttonTheme: ButtonThemeData( | |
colorScheme: ColorScheme.light( | |
primary: Color(0xfff9ca15), | |
secondary: Color(0xfff9ca15), | |
), | |
) | |
), | |
home: MainPage(), | |
) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment