Skip to content

Commit

Permalink
add achievement submitting
Browse files Browse the repository at this point in the history
Signed-off-by: Max L <maliu2@s.sfusd.edu>
  • Loading branch information
MaxAPCS committed Feb 9, 2024
1 parent e9e89fe commit d15ed4d
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 7 deletions.
31 changes: 31 additions & 0 deletions lib/interfaces/supabase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,35 @@ class SupabaseInterface {
static Future<Map<String, String>> get pitSchema async => await canConnect
? pitscoutStock.fresh(Configuration.instance.season)
: pitscoutStock.get(Configuration.instance.season);

static Set<Achievement>? _achievements;
static Future<Set<Achievement>?> get achievements async =>
(_achievements == null && await canConnect)
? Supabase.instance.client
.from("achievements")
.select("*")
.withConverter((resp) => resp
.map((record) => (
id: record["id"] as int,
name: record["name"] as String,
description: record["description"] as String,
requirements: record["requirements"] as String,
points: record["points"] as int,
season: record["season"] as int?,
event: record["event"] as String?
))
.toSet())
.then((data) => _achievements = data)
: Future.value(_achievements);
static void clearAchievements() => _achievements = null;
}

typedef Achievement = ({
int id,
String name,
String description,
String requirements,
int points,
int? season,
String? event
});
31 changes: 29 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'interfaces/supabase.dart';
import 'pages/achievements.dart';
import 'pages/admin/admin.dart' show adminGoRoute;
import 'pages/configuration.dart';
import 'pages/landing.dart';
Expand Down Expand Up @@ -119,6 +120,7 @@ enum RoutePaths {
landing,
metadata,
configuration,
achievements,
scouting,
matchscout,
pitscout,
Expand Down Expand Up @@ -195,6 +197,15 @@ final GoRouter router = GoRouter(
redirect: (context, state) async => await Configuration.instance.isValid
? null
: state.namedLocation(RoutePaths.configuration.name)),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: 'achievements',
name: RoutePaths.achievements.name,
pageBuilder: (context, state) =>
MaterialPage(child: AchievementsPage(), name: "Achievements"),
redirect: (context, state) async => await Configuration.instance.isValid
? null
: state.namedLocation(RoutePaths.configuration.name)),
])
]),
adminGoRoute,
Expand Down Expand Up @@ -227,7 +238,10 @@ class ScaffoldShell extends StatelessWidget {
: null,
currentAccountPicture: Icon(
UserMetadata.isAuthenticated ? Icons.person : Icons.person_off_outlined,
size: 64),
size: 64,
color: Theme.of(context).brightness == Brightness.dark
? null
: Colors.white),
accountName: Text(UserMetadata.instance.name ?? "User",
style: const TextStyle(fontWeight: FontWeight.w600)),
accountEmail: Text(
Expand Down Expand Up @@ -259,40 +273,53 @@ class ScaffoldShell extends StatelessWidget {
builder: (context, snapshot) => ListTile(
leading: const Icon(Icons.app_registration_outlined),
title: const Text("Metadata"),
dense: MediaQuery.of(context).size.height <= 400,
enabled: snapshot.hasData && snapshot.data!,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.metadata.name))),
ListTile(
leading: const Icon(Icons.settings_rounded),
title: const Text("Configuration"),
dense: MediaQuery.of(context).size.height <= 400,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.configuration.name)),
ListTile(
leading: const Icon(Icons.assignment_rounded),
title: const Text("Match Scouting"),
dense: MediaQuery.of(context).size.height <= 400,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.matchscout.name)),
ListTile(
leading: const Icon(Icons.list_rounded),
title: const Text("Pit Scouting"),
dense: MediaQuery.of(context).size.height <= 400,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.pitscout.name)),
ListTile(
leading: const Icon(Icons.download_for_offline_rounded),
title: const Text("Saved Responses"),
dense: MediaQuery.of(context).size.height <= 400,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.savedresp.name)),
ListTile(
leading: const Icon(Icons.emoji_events_rounded),
title: const Text("Achievements"),
dense: MediaQuery.of(context).size.height <= 400,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.achievements.name)),
ListenableBuilder(
listenable: UserMetadata.instance.cachedPermissions,
builder: (context, _) => UserMetadata.instance.hasAnyAdminPerms
? ListTile(
leading: const Icon(Icons.bar_chart_rounded),
title: const Text("Admin Tools"),
dense: MediaQuery.of(context).size.height <= 400,
onTap: () => GoRouter.of(context)
..pop()
..goNamed(RoutePaths.adminportal.name))
Expand All @@ -301,7 +328,7 @@ class ScaffoldShell extends StatelessWidget {
child: Align(
alignment: Alignment.bottomLeft,
child: SizedBox(
height: 70,
height: 65,
child: DrawerHeader(
margin: EdgeInsets.zero,
child: Text(
Expand Down
178 changes: 178 additions & 0 deletions lib/pages/achievements.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import 'dart:math';

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import '../interfaces/supabase.dart';
import '../pages/configuration.dart';

class AchievementsPage extends StatelessWidget {
final TextEditingController _searchedText = TextEditingController();
final ValueNotifier<Achievement?> _selectedAchievement = ValueNotifier(null);
final TextEditingController _detailsController = TextEditingController();
AchievementsPage({super.key});

@override
Widget build(BuildContext context) => SafeArea(
minimum: const EdgeInsets.only(bottom: 12),
child: Column(children: [
AppBar(title: const Text("Achievements")),
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchedText,
decoration: const InputDecoration(
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search_rounded),
hintText: "Search"))),
FutureBuilder(
future: SupabaseInterface
.achievements, // , Supabase.instance.client.from("achievement_queue").select("*")]
builder: (context, snapshot) => Column(children: [
ListenableBuilder(
listenable: _searchedText,
builder: (context, _) {
List<Achievement> data = snapshot.data == null
? []
: snapshot.data!
.where((achievement) => achievement.name
.toLowerCase()
.contains(_searchedText.text.toLowerCase()))
.toList();
return CarouselSlider(
items: data.map((achievement) {
var autoscrollcontroller = ScrollController();
autoscrollcontroller.addListener(() => autoscrollcontroller.offset >=
autoscrollcontroller.position.maxScrollExtent
? autoscrollcontroller.jumpTo(0)
: autoscrollcontroller.position.pixels == 0
? autoscrollcontroller.animateTo(
autoscrollcontroller.position.maxScrollExtent,
duration: const Duration(seconds: 3),
curve: Curves.linear)
: null);
return Card(
surfaceTintColor: (achievement.season != null &&
achievement.season != Configuration.instance.season) ||
(achievement.event != null &&
achievement.event != Configuration.event)
? Theme.of(context).colorScheme.surfaceVariant
: null // TODO: yellow if awaiting approval, green if approved, red if rejected
,
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(mainAxisSize: MainAxisSize.max, children: [
Flexible(
flex: 5,
fit: FlexFit.tight,
child: Align(
alignment: Alignment.centerLeft,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(achievement.name,
style: Theme.of(context)
.textTheme
.headlineMedium)))),
const Flexible(
flex: 1, fit: FlexFit.tight, child: SizedBox()),
Flexible(
flex: 2,
fit: FlexFit.tight,
child: Align(
alignment: Alignment.centerRight,
child: FittedBox(
child: Text("${achievement.points} pts",
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(color: Colors.grey)))))
]),
const SizedBox(height: 12),
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(
parent: NeverScrollableScrollPhysics()),
controller: autoscrollcontroller,
child: Text(achievement.description,
style:
Theme.of(context).textTheme.bodyLarge)))
])));
}).toList(),
options: CarouselOptions(
viewportFraction: max(300 / MediaQuery.of(context).size.width, 0.6),
enlargeCenterPage: true,
enlargeFactor: 0.2,
height: 200,
enableInfiniteScroll: _searchedText.text.isEmpty,
onPageChanged: (i, _) {
if (_selectedAchievement.value?.id == data[i].id) return;
_detailsController.clear();
_selectedAchievement.value = data[i];
}),
);
}),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListenableBuilder(
listenable: _selectedAchievement,
builder: (context, _) {
if (_selectedAchievement.value == null) {
_selectedAchievement.value = snapshot.data?.first;
if (_selectedAchievement.value == null) return const SizedBox();
}
return Column(children: [
const SizedBox(height: 12),
Text(_selectedAchievement.value!.requirements,
softWrap: true,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Colors.grey))
]);
}))
])),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: TextField(
minLines: null,
maxLines: null,
maxLength: 250,
expands: true,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: "Details (Optional)",
border: OutlineInputBorder()),
controller: _detailsController))),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(children: [
IconButton(onPressed: () {}, icon: const Icon(Icons.attach_file_rounded)),
const Expanded(child: SizedBox()),
FilledButton.icon(
onPressed: () {
if (_selectedAchievement.value == null) return;
Supabase.instance.client.from("achievement_queue").insert({
'achievement': _selectedAchievement.value!.id,
'details': _detailsController.text.isEmpty ? null : _detailsController.text,
'season': Configuration.instance.season,
'event': Configuration.event
}).then((_) {
_detailsController.clear();
// this needs to trigger a rebuild of the achievement list
}).catchError((e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
});
},
icon: const Icon(Icons.send_rounded),
label: const Text("Submit"))
]))
]));
}
25 changes: 21 additions & 4 deletions lib/pages/admin/achievementqueue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class AchievementQueuePage extends StatelessWidget {
String user,
int season,
String event,
String details
String details,
NetworkImage? image
})> _items = UniqueNotifyingList();
AchievementQueuePage({super.key}) {
_fetch();
Expand All @@ -40,7 +41,7 @@ class AchievementQueuePage extends StatelessWidget {
season: record['season'] as int,
event: record['event'] as String,
details: (record["details"] as String).trim(),
// image: NetworkImage(record["image"])
image: record["image"] == null ? null : NetworkImage(record["image"])
))))
.then((items) => _items.setAll(items));

Expand Down Expand Up @@ -86,6 +87,23 @@ class AchievementQueuePage extends StatelessWidget {
key: ObjectKey(e),
title: Text(e.achname),
subtitle: Text("${e.user} @ ${e.season}${e.event}"),
leading: e.image == null
? null
: ClipRRect(
borderRadius: BorderRadius.circular(4),
clipBehavior: Clip.hardEdge,
child: GestureDetector(
child: Image(
image: e.image!,
isAntiAlias: false,
),
onTap: () => showDialog(
context: context,
builder: (context) => Dialog(
child: Image(
image: e.image!,
filterQuality: FilterQuality.medium))),
)),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
childrenPadding:
Expand All @@ -101,7 +119,6 @@ class AchievementQueuePage extends StatelessWidget {
leading: const Icon(Icons.person_search_rounded),
title: const Text("User Description"),
subtitle: Text(e.details, softWrap: true),
// leading: e.image,
dense: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
Expand Down Expand Up @@ -195,7 +212,7 @@ class UniqueNotifyingList<E> extends ChangeNotifier {
return false;
}

operator [](int i) => _internal.elementAt(i);
E operator [](int i) => _internal.elementAt(i);
get length => _internal.length;
get isEmpty => _internal.isEmpty;
}
Loading

0 comments on commit d15ed4d

Please sign in to comment.