Skip to content

Instantly share code, notes, and snippets.

@Levi-Lesches
Created February 21, 2020 18:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Levi-Lesches/55b6fc50b4592365e9929a1a01115d71 to your computer and use it in GitHub Desktop.
Save Levi-Lesches/55b6fc50b4592365e9929a1a01115d71 to your computer and use it in GitHub Desktop.
Ram Life admin sports builder
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