Initial commit

This commit is contained in:
St. Nebula
2026-04-23 23:58:59 -05:00
commit 47b9e3c159
257 changed files with 18913 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:onsolgo/core/constants.dart';
/// Bottom sheet: list comments under [parent] (`parent.collection('comments')`).
class CommentsSheet extends StatefulWidget {
final DocumentReference parent;
final String title;
const CommentsSheet({
super.key,
required this.parent,
this.title = 'Comments',
});
static Future<void> show(BuildContext context, {required DocumentReference parent, String title = 'Comments'}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.grey[900],
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => CommentsSheet(parent: parent, title: title),
);
}
@override
State<CommentsSheet> createState() => _CommentsSheetState();
}
class _CommentsSheetState extends State<CommentsSheet> {
final _textC = TextEditingController();
bool _sending = false;
@override
void dispose() {
_textC.dispose();
super.dispose();
}
Future<void> _send() async {
final uid = FirebaseAuth.instance.currentUser?.uid;
if (uid == null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Sign in to comment')));
return;
}
final text = _textC.text.trim();
if (text.isEmpty) return;
setState(() => _sending = true);
try {
final uSnap = await FirebaseFirestore.instance.collection('users').doc(uid).get();
final name = (uSnap.data()?['username'] as String?)?.trim() ?? 'Citizen';
await widget.parent.collection('comments').add({
'uid': uid,
'authorName': name,
'text': text,
'timestamp': FieldValue.serverTimestamp(),
});
_textC.clear();
if (mounted) FocusScope.of(context).unfocus();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not post: $e')));
}
} finally {
if (mounted) setState(() => _sending = false);
}
}
@override
Widget build(BuildContext context) {
final uid = FirebaseAuth.instance.currentUser?.uid ?? '';
final padBottom = MediaQuery.of(context).viewInsets.bottom;
return Padding(
padding: EdgeInsets.only(bottom: padBottom),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.55,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 8),
child: Row(
children: [
Text(widget.title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
const Spacer(),
IconButton(icon: const Icon(Icons.close, color: Colors.white54), onPressed: () => Navigator.pop(context)),
],
),
),
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: widget.parent.collection('comments').orderBy('timestamp', descending: false).snapshots(),
builder: (context, snap) {
if (snap.hasError) {
return Center(child: Text('Error: ${snap.error}', style: const TextStyle(color: Colors.redAccent)));
}
if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: kOnsolGold));
final docs = snap.data!.docs;
if (docs.isEmpty) {
return Center(child: Text('No comments yet.', style: TextStyle(color: Colors.grey[500])));
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: docs.length,
itemBuilder: (context, i) {
final c = docs[i].data() as Map<String, dynamic>;
final author = c['authorName'] ?? 'Citizen';
final text = c['text'] ?? '';
final own = c['uid'] == uid;
return Card(
color: Colors.black54,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
dense: true,
title: Text(author, style: const TextStyle(color: kOnsolGold, fontSize: 12, fontWeight: FontWeight.bold)),
subtitle: Text(text, style: const TextStyle(color: Colors.white70, fontSize: 14)),
trailing: own
? IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20),
onPressed: () async {
try {
await docs[i].reference.delete();
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e')));
}
}
},
)
: null,
),
);
},
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textC,
style: const TextStyle(color: Colors.white),
minLines: 1,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add a comment…',
hintStyle: TextStyle(color: Colors.grey[600]),
filled: true,
fillColor: Colors.black38,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
onSubmitted: (_) => _send(),
),
),
const SizedBox(width: 8),
_sending
? const Padding(padding: EdgeInsets.all(12), child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)))
: IconButton.filled(
style: IconButton.styleFrom(backgroundColor: kOnsolGold, foregroundColor: Colors.black),
onPressed: _send,
icon: const Icon(Icons.send),
),
],
),
),
],
),
),
);
}
}