Add bus detail functionality and update organization selection UI

This commit is contained in:
ImBenji
2026-03-29 19:12:49 +01:00
parent 55cd970173
commit 3dfea45afb
10 changed files with 425 additions and 97 deletions
+9 -1
View File
@@ -1,5 +1,6 @@
import "dart:convert";
import "package:bus_running_record/constants.dart";
import "package:bus_running_record/provider/collaboration_state.dart";
import "package:bus_running_record/provider/supabase_state.dart";
import "package:provider/provider.dart";
@@ -136,7 +137,7 @@ Future<void> showJoinOrganizationDialog(BuildContext context) async {
constraints: const BoxConstraints(maxWidth: 420),
child: TextField(
autofocus: true,
placeholder: const Text("https://.../#/invite/<token>"),
placeholder: Text("https://$kAppHostname/#/invite/<token>"),
onChanged: (value) {
inviteInput = value;
},
@@ -152,6 +153,13 @@ Future<void> showJoinOrganizationDialog(BuildContext context) async {
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text("Cancel"),
),
Button.text(
onPressed: () {
Navigator.of(dialogContext).pop();
showCreateOrganizationDialog(context);
},
child: const Text("Create one instead"),
),
Button.primary(
onPressed: () => Navigator.of(dialogContext).pop(inviteInput),
child: const Text("Join"),
+124 -73
View File
@@ -48,7 +48,7 @@ class HomeLeftSidebar extends StatelessWidget {
aspectRatio: 1,
child: IconButton.outline(
onPressed: () {
unawaited(showCreateOrganizationDialog(context));
unawaited(showJoinOrganizationDialog(context));
},
shape: ButtonShape.circle,
size: ButtonSize.normal,
@@ -86,62 +86,79 @@ class HomeLeftSidebar extends StatelessWidget {
Expanded(
child: Container(
color: Theme.of(context).colorScheme.background,
child: IntrinsicWidth(
child: Column(
child: IntrinsicWidth(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gap(topPadding),
Padding(
padding: const EdgeInsets.all(8),
child: SizedBox(
width: double.infinity,
child: Button.ghost(
alignment: Alignment.center,
leading: const Icon(Icons.add),
child: const Text("Create Organization"),
onPressed: () {
unawaited(showCreateOrganizationDialog(context));
},
),
// Organisation Name
Container(
padding: const EdgeInsets.all(8.0),
height: 170,
alignment: Alignment.bottomLeft,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 8),
child: Transform.translate(
offset: const Offset(0, 2),
child: Text(
collab.organizations.where((o) => o.id == collab.selectedOrganizationId).map((o) => o.name).firstOrNull ?? "Select a server",
style: const TextStyle(height: 1),
).extraBold.large,
),
),
Builder(
builder: (btnContext) => IconButton.ghost(
icon: Icon(LucideIcons.ellipsisVertical),
onPressed: () {
final orgId = collab.selectedOrganizationId;
if (orgId == null) return;
final org = collab.organizations.where((o) => o.id == orgId).firstOrNull;
if (org == null) return;
showDropdown<void>(
context: btnContext,
anchorAlignment: Alignment.bottomRight,
alignment: Alignment.topLeft,
builder: (menuContext) {
return DropdownMenu(
children: [
MenuLabel(child: Text(org.name).small.semiBold),
const MenuDivider(),
MenuButton(
leading: const Icon(LucideIcons.plus).iconSmall,
onPressed: (_) {
unawaited(showCreateChannelDialog(context, organizationId: org.id));
},
child: const Text("Add Channel"),
),
const MenuDivider(),
MenuButton(
leading: const Icon(LucideIcons.settings2).iconSmall,
onPressed: (_) {
context.go("/org/${org.id}/settings");
},
child: const Text("Settings"),
),
],
);
},
);
},
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: SizedBox(
width: double.infinity,
child: Button.ghost(
alignment: Alignment.center,
leading: const Icon(LucideIcons.userRoundPlus),
child: const Text("Join Organization"),
onPressed: () {
unawaited(showJoinOrganizationDialog(context));
},
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: SizedBox(
width: double.infinity,
child: Button.secondary(
alignment: Alignment.center,
leading: const Icon(LucideIcons.bug),
child: const Text("Auth Debug"),
onPressed: () {
unawaited(runAuthDebug(context));
},
),
),
),
const Divider(),
Divider(),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
@@ -150,14 +167,34 @@ class HomeLeftSidebar extends StatelessWidget {
right: 16,
bottom: 8,
),
child: _OrganizationList(
child: _SelectedOrgChannelList(
organizations: organizations,
selectedOrganizationId: collab.selectedOrganizationId,
isLoading: collab.isLoadingOrganizations,
errorMessage: collab.errorMessage,
),
),
),
const Divider(),
if (false)
Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: SizedBox(
width: double.infinity,
child: Button.secondary(
alignment: Alignment.center,
leading: const Icon(LucideIcons.bug),
child: const Text("Auth Debug"),
onPressed: () {
unawaited(runAuthDebug(context));
},
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Button.ghost(
@@ -165,7 +202,7 @@ class HomeLeftSidebar extends StatelessWidget {
children: [
Avatar(initials: initials),
const Gap(8),
Expanded(child: Basic(title: Text(displayName))),
Basic(title: Text(displayName)),
const Icon(LucideIcons.logOut).iconSmall,
],
),
@@ -175,8 +212,7 @@ class HomeLeftSidebar extends StatelessWidget {
),
),
],
),
),
)),
),
),
],
@@ -184,14 +220,16 @@ class HomeLeftSidebar extends StatelessWidget {
}
}
class _OrganizationList extends StatelessWidget {
const _OrganizationList({
class _SelectedOrgChannelList extends StatelessWidget {
const _SelectedOrgChannelList({
required this.organizations,
required this.selectedOrganizationId,
required this.isLoading,
required this.errorMessage,
});
final List<OrganizationSummary> organizations;
final String? selectedOrganizationId;
final bool isLoading;
final String? errorMessage;
@@ -207,26 +245,26 @@ class _OrganizationList extends StatelessWidget {
);
}
if (organizations.isEmpty) {
if (selectedOrganizationId == null || organizations.isEmpty) {
return Center(
child: Text("No organizations yet. Create one to get started.").small,
child: Text("Select a server to view channels.").small.muted,
);
}
final collab = context.watch<CollaborationProvider>();
return ListView.separated(
itemBuilder: (context, index) {
final org = organizations[index];
return _OrganizationGroup(
organization: org,
channels: collab.channelsForOrganization(org.id),
selectedOrganizationId: collab.selectedOrganizationId,
selectedChannelId: collab.selectedChannelId,
);
},
separatorBuilder: (context, index) => const Gap(4),
itemCount: organizations.length,
final matchingOrgs = organizations.where((o) => o.id == selectedOrganizationId);
if (matchingOrgs.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
final org = matchingOrgs.first;
return _OrganizationGroup(
organization: org,
channels: collab.channelsForOrganization(org.id),
selectedOrganizationId: collab.selectedOrganizationId,
selectedChannelId: collab.selectedChannelId,
);
}
}
@@ -329,6 +367,19 @@ class _ServerRail extends StatelessWidget {
items: [
MenuLabel(child: Text(organization.name)),
const MenuDivider(),
MenuButton(
leading: const Icon(LucideIcons.plus).iconSmall,
onPressed: (_) {
unawaited(
showCreateChannelDialog(
context,
organizationId: organization.id,
),
);
},
child: const Text("Add Channel"),
),
const MenuDivider(),
MenuButton(
leading: const Icon(LucideIcons.settings2).iconSmall,
onPressed: (_) {