diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle index 34e8cce..4e5f251 100644 --- a/frontend/android/app/build.gradle +++ b/frontend/android/app/build.gradle @@ -28,7 +28,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 30 - + minSdkVersion 19 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/frontend/lib/styles/font_styles.dart b/frontend/lib/styles/font_styles.dart index 2d67591..484a060 100644 --- a/frontend/lib/styles/font_styles.dart +++ b/frontend/lib/styles/font_styles.dart @@ -5,21 +5,39 @@ TextStyle mainStyle = const TextStyle( color: Colors.black, fontSize: 14, height: 0.5, - fontWeight: FontWeight.w400 -); + fontWeight: FontWeight.w400); TextStyle linkStyle = const TextStyle( fontFamily: 'SF Pro', color: Color.fromRGBO(29, 56, 80, 1.0), fontSize: 14, height: 0.5, - fontWeight: FontWeight.w400 -); + fontWeight: FontWeight.w400); TextStyle boldStyle = const TextStyle( fontFamily: 'SF Pro', color: Colors.black, fontSize: 14, height: 0.5, - fontWeight: FontWeight.w800 -); \ No newline at end of file + fontWeight: FontWeight.w800); + +TextStyle titleStyle = const TextStyle( + fontFamily: 'SF Pro', + color: Colors.black, + fontSize: 24, + fontWeight: FontWeight.w400); + +TextStyle hintStyle = const TextStyle( + fontFamily: 'SF Pro', + color: Colors.grey, + fontSize: 15, + fontWeight: FontWeight.w400); + +TextStyle largerStyle = const TextStyle( + fontFamily: 'SF Pro', fontSize: 20, fontWeight: FontWeight.w400); + +TextStyle smallHintStyle = const TextStyle( + fontFamily: 'SF Pro', + color: Colors.grey, + fontSize: 10, + fontWeight: FontWeight.w400); diff --git a/frontend/lib/widgets/date_panel.dart b/frontend/lib/widgets/date_panel.dart index e6a9d8e..58dc8af 100644 --- a/frontend/lib/widgets/date_panel.dart +++ b/frontend/lib/widgets/date_panel.dart @@ -3,13 +3,11 @@ import 'package:lp_task_scheduler/widgets/new_task_panel.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; class DatePanel extends StatefulWidget { - Function setViewState = (ViewState state) => {}; - Function setSelectedDate = (DateTime date) => {}; - DatePanel(Function _setViewState, Function _setSelectedDate, Key? key) - : super(key: key) { - setSelectedDate = _setSelectedDate; - setViewState = _setViewState; - } + Function onPressed; + Function onSelectionChanged; + DateTime? initialDate; + DatePanel(this.onPressed, this.onSelectionChanged, this.initialDate, Key? key) + : super(key: key) {} @override State createState() => _DatePanelState(); @@ -17,6 +15,11 @@ class DatePanel extends StatefulWidget { class _DatePanelState extends State { @override + @override + void initState() { + super.initState(); + } + Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -24,8 +27,10 @@ class _DatePanelState extends State { Container( margin: const EdgeInsets.all(30), child: SfDateRangePicker( + initialDisplayDate: widget.initialDate, onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { - widget.setSelectedDate(args.value); + print("hello"); + widget.onSelectionChanged(args.value); }, selectionMode: DateRangePickerSelectionMode.single, initialSelectedDate: DateTime.now(), @@ -54,7 +59,7 @@ class _DatePanelState extends State { color: Colors.black, fontWeight: FontWeight.bold), ), onPressed: () { - widget.setViewState(ViewState.standard); + widget.onPressed(); }, ), )) diff --git a/frontend/lib/widgets/details/detail_description.dart b/frontend/lib/widgets/details/detail_description.dart new file mode 100644 index 0000000..a64c21a --- /dev/null +++ b/frontend/lib/widgets/details/detail_description.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:lp_task_scheduler/styles/font_styles.dart'; + +class DetailDescription extends StatelessWidget { + final String description; + const DetailDescription({Key? key, this.description = ""}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text("Description", style: hintStyle), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(description), + ) + ], + ), + ), + Divider(), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/details/detail_row.dart b/frontend/lib/widgets/details/detail_row.dart new file mode 100644 index 0000000..5901266 --- /dev/null +++ b/frontend/lib/widgets/details/detail_row.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:lp_task_scheduler/styles/font_styles.dart'; + +class DetailRow extends StatelessWidget { + final IconData? icon; + final String label; + final String? data; + final String? points; + final double size; + + const DetailRow( + {this.icon, + required this.label, + this.data, + this.points, + this.size = 30, + Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0), + child: Column( + children: [ + Row( + children: [ + MaterialButton( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(icon, size: size), + ), + onPressed: () {}, + shape: CircleBorder(side: BorderSide(color: Colors.deepOrange)), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(label, style: hintStyle), + Text(data ?? '', style: largerStyle), + ], + ) + ], + ), + Divider(), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/details/finish_button.dart b/frontend/lib/widgets/details/finish_button.dart new file mode 100644 index 0000000..bc3fcb8 --- /dev/null +++ b/frontend/lib/widgets/details/finish_button.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:lp_task_scheduler/styles/font_styles.dart'; + +typedef void OnPressed(); + +class FinishButton extends StatelessWidget { + final OnPressed onPressed; + const FinishButton({Key? key, required this.onPressed}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialButton( + onPressed: onPressed, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + child: FractionallySizedBox( + widthFactor: 1, + child: Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Colors.green, + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Icon(Icons.check, color: Colors.white, size: 20), + )), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text("Mark Finished", style: largerStyle), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/details/task_details.dart b/frontend/lib/widgets/details/task_details.dart new file mode 100644 index 0000000..16290da --- /dev/null +++ b/frontend/lib/widgets/details/task_details.dart @@ -0,0 +1,94 @@ +import 'dart:developer'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:lp_task_scheduler/styles/font_styles.dart'; +import 'package:lp_task_scheduler/widgets/details/detail_description.dart'; +import 'package:lp_task_scheduler/widgets/details/detail_row.dart'; +import 'package:lp_task_scheduler/widgets/details/finish_button.dart'; +import 'package:lp_task_scheduler/widgets/details/task_owner.dart'; +import 'package:lp_task_scheduler/widgets/new_task_panel.dart'; + +import '../assign_dropdown.dart'; +import '../date_panel.dart'; + +class TaskDetails extends StatefulWidget { + final DocumentSnapshot? ds; + TaskDetails({this.ds, Key? key}) : super(key: key); + @override + State createState() => _TaskDetailsState(); +} + +class _TaskDetailsState extends State { + ViewState _viewState = ViewState.standard; + DateFormat formatter = DateFormat('yyyy-MM-dd'); + + void setViewState(ViewState state) { + setState(() { + _viewState = state; + }); + } + + void completeTask() { + widget.ds!.reference.update({"status": "complete"}); + } + + @override + Widget build(BuildContext context) { + if (widget.ds == null || !widget.ds!.exists) { + return Container(); + } + dynamic data = widget.ds!.data(); + print(data.toString()); + DateTime date = DateTime.parse(data["dueDate"].toDate().toString()); + print(data); + return Scaffold( + appBar: AppBar( + title: Text('Task Details'), + ), + body: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), + child: Text(data["title"], style: titleStyle), + ), + Column(children: [ + DetailRow( + icon: Icons.person, + label: "Assigned To", + data: data["assignedTo"] != null + ? data["assignedTo"][0] + : "Unassigned"), + DetailRow( + icon: Icons.calendar_today, + label: "Due:", + data: formatter.format(date)), + DetailRow( + icon: Icons.star_border_rounded, + label: "Points", + data: + data["points"] != null ? data["points"].toString() : "1"), + ]), + DetailDescription( + description: data["description"], + ), + Center(child: FinishButton( + onPressed: () { + completeTask(); + }, + )), + Spacer(), + TaskOwner( + owner: data["owner"] ?? "unknown", + ) + ], + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/details/task_owner.dart b/frontend/lib/widgets/details/task_owner.dart new file mode 100644 index 0000000..e9ecc35 --- /dev/null +++ b/frontend/lib/widgets/details/task_owner.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:lp_task_scheduler/styles/font_styles.dart'; + +class TaskOwner extends StatelessWidget { + final String owner; + final DateTime? date; + const TaskOwner({Key? key, required this.owner, this.date}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + height: 50, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 5, + offset: Offset(0, 3), // changes position of shadow + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + // This is hacky, need help fixing + decoration: BoxDecoration( + color: Colors.blueGrey, + borderRadius: BorderRadius.circular(30), + ), + child: const Padding( + padding: EdgeInsets.only(top: 10), + child: Image( + width: 50, + height: 50, + fit: BoxFit.cover, + image: AssetImage('lib/assets/icons/homelogo.png')), + ), + width: 50, + height: 50, + ), + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 16, 8, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Task created by $owner", style: mainStyle), + Text("1 week ago", style: smallHintStyle), + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/interactive_task.dart b/frontend/lib/widgets/interactive_task.dart index 1b29c64..a079bd5 100644 --- a/frontend/lib/widgets/interactive_task.dart +++ b/frontend/lib/widgets/interactive_task.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:lp_task_scheduler/widgets/details/task_details.dart'; +import 'package:lp_task_scheduler/widgets/touchable_opacity.dart'; import '../src/task.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; @@ -37,15 +39,25 @@ class InteractiveTask extends StatelessWidget { @override Widget build(BuildContext context) { dynamic data = ds!.data(); - return Dismissible( - onDismissed: (direction) => dismissItem(direction), - key: UniqueKey(), - child: Task( - title: data["title"].toString(), - description: data["description"].toString(), - ds: ds), - background: buildSwipeActionLeft(), - secondaryBackground: buildSwipeActionRight(), + return TouchableOpacity( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TaskDetails( + ds: ds, + ))); + }, + child: Dismissible( + onDismissed: (direction) => dismissItem(direction), + key: UniqueKey(), + child: Task( + title: data["title"].toString(), + description: data["description"].toString(), + ds: ds), + background: buildSwipeActionLeft(), + secondaryBackground: buildSwipeActionRight(), + ), ); } diff --git a/frontend/lib/widgets/new_task_panel.dart b/frontend/lib/widgets/new_task_panel.dart index 81a094a..c0f7288 100644 --- a/frontend/lib/widgets/new_task_panel.dart +++ b/frontend/lib/widgets/new_task_panel.dart @@ -70,6 +70,12 @@ class _TaskPanel extends State { }); } + void onDatePressed() { + setState(() { + _showState = ViewState.standard; + }); + } + void setSelectedDate(DateTime date) { setState(() { selectedDate = date; @@ -91,7 +97,7 @@ class _TaskPanel extends State { Widget renderFromViewState() { switch (_showState) { case ViewState.date: - return DatePanel(setViewState, setSelectedDate, null); + return DatePanel(onDatePressed, setSelectedDate, null, null); case ViewState.standard: default: return Column( diff --git a/frontend/lib/widgets/touchable_opacity.dart b/frontend/lib/widgets/touchable_opacity.dart new file mode 100644 index 0000000..dd035e9 --- /dev/null +++ b/frontend/lib/widgets/touchable_opacity.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +typedef void VoidFunction(); + +class TouchableOpacity extends StatefulWidget { + final Widget child; + final VoidFunction onTap; + final Duration duration = const Duration(milliseconds: 50); + final double opacity = 0.5; + + TouchableOpacity({required this.child, required this.onTap}); + + @override + _TouchableOpacityState createState() => _TouchableOpacityState(); +} + +class _TouchableOpacityState extends State { + bool isDown = false; + + @override + void initState() { + super.initState(); + setState(() => isDown = false); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => setState(() => isDown = true), + onTapUp: (_) => setState(() => isDown = false), + onTapCancel: () => setState(() => isDown = false), + onTap: widget.onTap, + child: AnimatedOpacity( + child: widget.child, + duration: widget.duration, + opacity: isDown ? widget.opacity : 1, + ), + ); + } +}