// Local Service + News App (Flutter) - Single-file prototype
// How to run:
// 1) Create a new Flutter project: `flutter create local_service_news`
// 2) Replace lib/main.dart with this file's contents.
// 3) Run: `flutter run` (no external packages required)
//
// Features included in this prototype:
// - Two tabs: Services and News
// - Services: grid of local services with contact and description
// - News: list of news items (title, summary, timestamp)
// - Add news via FloatingActionButton (local in-memory storage)
// - Search news, view details, mark as favorite
// - Sample data seeded
//
// Notes / Next steps (optional):
// - Persist data with Firebase / SQLite / SharedPreferences
// - Add Admin panel (web) to publish news & services
// - Add push notifications (Firebase Cloud Messaging)
// - Add localisation for Bengali/English
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
void main() {
runApp(LocalServiceNewsApp());
}
class LocalServiceNewsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Local Service + News',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State
with SingleTickerProviderStateMixin {
TabController? _tabController;
List services = sampleServices;
List news = List.from(sampleNews);
String newsQuery = '';
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
void _addNews(NewsItem item) {
setState(() {
news.insert(0, item);
});
}
void _toggleFavorite(NewsItem item) {
setState(() {
item.isFavorite = !item.isFavorite;
});
}
List get filteredNews {
if (newsQuery.trim().isEmpty) return news;
final q = newsQuery.toLowerCase();
return news.where((n) => n.title.toLowerCase().contains(q) || n.summary.toLowerCase().contains(q)).toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Local Service + News'),
bottom: TabBar(
controller: _tabController,
tabs: [Tab(text: 'Services'), Tab(text: 'News')],
),
actions: [
IconButton(
icon: Icon(Icons.info_outline),
onPressed: () => _showAbout(context),
),
],
),
body: TabBarView(
controller: _tabController,
children: [
_buildServicesTab(),
_buildNewsTab(),
],
),
floatingActionButton: _tabController?.index == 1
? FloatingActionButton(
onPressed: () => _openAddNewsDialog(context),
child: Icon(Icons.add),
tooltip: 'Add News',
)
: null,
);
}
Widget _buildServicesTab() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.count(
crossAxisCount: 2,
childAspectRatio: 3 / 2,
children: services.map((s) => ServiceCard(service: s)).toList(),
),
);
}
Widget _buildNewsTab() {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search news...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
onChanged: (v) => setState(() => newsQuery = v),
),
),
SizedBox(width: 8),
PopupMenuButton(
onSelected: (v) {
if (v == 'favorites') {
setState(() {
newsQuery = '';
news = news.where((n) => n.isFavorite).toList();
});
} else if (v == 'all') {
setState(() {
news = List.from(sampleNews);
});
}
},
itemBuilder: (context) => [
PopupMenuItem(value: 'favorites', child: Text('Show Favorites')),
PopupMenuItem(value: 'all', child: Text('Reset Sample News')),
],
icon: Icon(Icons.filter_list),
)
],
),
),
Expanded(
child: filteredNews.isEmpty
? Center(child: Text('No news found'))
: ListView.builder(
itemCount: filteredNews.length,
itemBuilder: (context, index) {
final item = filteredNews[index];
return NewsTile(
item: item,
onFavorite: () => _toggleFavorite(item),
onTap: () => _openNewsDetail(context, item),
);
},
),
),
],
);
}
void _openAddNewsDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AddNewsDialog(onAdd: _addNews);
},
);
}
void _openNewsDetail(BuildContext context, NewsItem item) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => NewsDetailPage(item: item)));
}
void _showAbout(BuildContext context) {
showAboutDialog(
context: context,
applicationName: 'Local Service + News',
applicationVersion: 'v0.1 (prototype)',
children: [Text('Prototype app for local services and news.')],
);
}
}
class ServiceCard extends StatelessWidget {
final Service service;
const ServiceCard({required this.service});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: () => _showServiceDetail(context, service),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(child: Icon(service.icon, size: 20)),
SizedBox(width: 8),
Expanded(child: Text(service.name, style: TextStyle(fontWeight: FontWeight.bold))),
],
),
SizedBox(height: 8),
Expanded(child: Text(service.shortDescription, maxLines: 3, overflow: TextOverflow.ellipsis)),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
child: Text('Call'),
onPressed: () => _callNumber(context, service.contact),
)
],
)
],
),
),
),
);
}
void _showServiceDetail(BuildContext context, Service s) {
showModalBottomSheet(
context: context,
builder: (ctx) => Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [CircleAvatar(child: Icon(s.icon)), SizedBox(width: 12), Text(s.name, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))]),
SizedBox(height: 12),
Text(s.description),
SizedBox(height: 12),
Row(children: [Icon(Icons.phone), SizedBox(width: 8), Text(s.contact)]),
SizedBox(height: 12),
Align(alignment: Alignment.centerRight, child: ElevatedButton(onPressed: () => _callNumber(context, s.contact), child: Text('Call')))
],
),
),
);
}
void _callNumber(BuildContext context, String number) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Pretend calling $number')));
// To actually call use `url_launcher` package with tel: scheme
}
}
class NewsTile extends StatelessWidget {
final NewsItem item;
final VoidCallback onFavorite;
final VoidCallback onTap;
const NewsTile({required this.item, required this.onFavorite, required this.onTap});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: ListTile(
onTap: onTap,
leading: item.imageUrl == null
? CircleAvatar(child: Icon(Icons.article))
: CircleAvatar(backgroundImage: NetworkImage(item.imageUrl!)),
title: Text(item.title),
subtitle: Text(item.summary, maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: Icon(item.isFavorite ? Icons.star : Icons.star_border, color: item.isFavorite ? Colors.amber : null),
onPressed: onFavorite,
),
),
);
}
}
class NewsDetailPage extends StatelessWidget {
final NewsItem item;
const NewsDetailPage({required this.item});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(item.title)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.imageUrl != null)
Center(child: Image.network(item.imageUrl!, height: 180, fit: BoxFit.cover)),
SizedBox(height: 12),
Text(item.title, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text(DateFormat.yMMMd().add_jm().format(item.timestamp)),
SizedBox(height: 12),
Expanded(child: SingleChildScrollView(child: Text(item.summary + '\n\n' + (item.content ?? '')))),
],
),
),
);
}
}
class AddNewsDialog extends StatefulWidget {
final Function(NewsItem) onAdd;
AddNewsDialog({required this.onAdd});
@override
_AddNewsDialogState createState() => _AddNewsDialogState();
}
class _AddNewsDialogState extends State {
final _formKey = GlobalKey();
String title = '';
String summary = '';
String content = '';
String imageUrl = '';
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Add News'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Title'),
onSaved: (v) => title = v ?? '',
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null,
),
TextFormField(
decoration: InputDecoration(labelText: 'Summary'),
onSaved: (v) => summary = v ?? '',
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null,
),
TextFormField(
decoration: InputDecoration(labelText: 'Content (optional)'),
onSaved: (v) => content = v ?? '',
maxLines: 3,
),
TextFormField(
decoration: InputDecoration(labelText: 'Image URL (optional)'),
onSaved: (v) => imageUrl = v ?? '',
),
],
),
),
),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: Text('Cancel')),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
final item = NewsItem(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
summary: summary,
content: content.isEmpty ? null : content,
imageUrl: imageUrl.isEmpty ? null : imageUrl,
timestamp: DateTime.now(),
);
widget.onAdd(item);
Navigator.of(context).pop();
}
},
child: Text('Add'),
)
],
);
}
}
// ----------------- Models & Sample Data -----------------
class Service {
final String id;
final String name;
final IconData icon;
final String shortDescription;
final String description;
final String contact;
Service({required this.id, required this.name, required this.icon, required this.shortDescription, required this.description, required this.contact});
}
class NewsItem {
final String id;
final String title;
String summary;
final String? content;
final String? imageUrl;
final DateTime timestamp;
bool isFavorite;
NewsItem({required this.id, required this.title, required this.summary, this.content, this.imageUrl, required this.timestamp, this.isFavorite = false});
}
List sampleServices = [
Service(
id: 's1',
name: 'Hospital',
icon: Icons.local_hospital,
shortDescription: '24/7 emergency hospital near you.',
description: 'City General Hospital\nAddress: 12 Main Road\nOpen: 24 hours\nServices: Emergency, OPD, Pharmacy',
contact: '+8801712345678',
),
Service(
id: 's2',
name: 'Police Station',
icon: Icons.local_police,
shortDescription: 'Local police station for emergencies.',
description: 'Central Thana\nAddress: 5 Police Para\nOpen: 24 hours',
contact: '+8801912345678',
),
Service(
id: 's3',
name: 'Ambulance',
icon: Icons.local_hospital,
shortDescription: 'Ambulance service with quick response.',
description: 'Rapid Ambulance Service\nContact for emergency transport and paramedics.',
contact: '+8801812345678',
),
Service(
id: 's4',
name: 'Electricity Office',
icon: Icons.electrical_services,
shortDescription: 'Pay bills & report outages.',
description: 'Local Electricity Board Office\nServices: Bill payment, outage report',
contact: '+8801612345678',
),
Service(
id: 's5',
name: 'Local Market Prices',
icon: Icons.shopping_cart,
shortDescription: 'Daily market price updates.',
description: 'Fresh updates on vegetable & fish prices in local markets.',
contact: '+8801512345678',
),
Service(
id: 's6',
name: 'Job Board',
icon: Icons.work,
shortDescription: 'Local job listings and part-time work.',
description: 'Post and browse local job openings: shops, tuition, helpers.',
contact: '+8801412345678',
),
];
List sampleNews = [
NewsItem(
id: 'n1',
title: 'Local fair starts today',
summary: 'The annual local fair (Mela) begins at the central ground. Stalls, games and cultural programs tonight.',
content: 'The fair will continue for three days. Visitors are requested to follow local guidelines. Parking is available near the market area.',
imageUrl: null,
timestamp: DateTime.now().subtract(Duration(hours: 3)),
),
NewsItem(
id: 'n2',
title: 'Water supply interruption',
summary: 'Water supply will be interrupted tomorrow from 10:00 to 14:00 due to pipeline maintenance.',
content: 'Residents are advised to store sufficient water for the duration. Repairs expected to complete by afternoon.',
imageUrl: null,
timestamp: DateTime.now().subtract(Duration(days: 1, hours: 2)),
),
NewsItem(
id: 'n3',
title: 'School exam schedule announced',
summary: 'Exams for classes 6-10 will start next Monday. Check the full schedule on the noticeboard.',
content: 'Students should arrive 15 minutes early. Admit cards will be distributed at school office.',
imageUrl: null,
timestamp: DateTime.now().subtract(Duration(days: 2)),
),
];
/*
================== ADMIN PANEL + AUTH (Firebase) - Added Notes & Prototype ==================
Overview:
- This section adds a simple Admin Panel and Authentication flow using Firebase (Firebase Authentication + Firestore).
- Admin can sign in (email/password), add/edit/delete News and Services, and push changes live to the app.
- The mobile app (main.dart) is left as a prototype that uses in-memory data. To enable live data, replace the sample data usage with Firestore queries (instructions below).
Files suggested (project structure):
- lib/main.dart --> (existing) User-facing app
- lib/admin/admin_home.dart --> Admin dashboard (Flutter, works on web & mobile)
- lib/admin/admin_login.dart --> Admin login (Firebase Auth)
- lib/services/firestore_service.dart --> Helper to read/write Firestore
Firebase setup (step-by-step):
1. Go to https://console.firebase.google.com and create a new project (e.g., local-service-news).
2. Add an Android app and (optionally) a Web app. For Android, you'll need the applicationId (package name). For Web, register and copy config.
3. Enable Firestore (Database -> Firestore) and Firebase Authentication (Authentication -> Sign-in method -> Email/Password).
4. In Firestore create two collections: `news` and `services`.
Firestore document shapes (recommended):
- news (collection)
- id (doc id) : {
title: string,
summary: string,
content: string,
imageUrl: string|null,
timestamp: timestamp
}
- services (collection)
- id (doc id) : {
name: string,
shortDescription: string,
description: string,
contact: string,
icon: string (optional key)
}
What I added below:
- A minimal admin_login.dart and admin_home.dart code snippets (Flutter) that use `firebase_core`, `firebase_auth`, and `cloud_firestore` packages.
- A Firestore helper with basic CRUD functions.
Important packages to add in pubspec.yaml:
firebase_core: ^2.0.0
firebase_auth: ^4.0.0
cloud_firestore: ^4.0.0
flutter_web_plugins: # only if building web
Admin login (outline):
- Email & Password fields
- Sign-in button -> FirebaseAuth.instance.signInWithEmailAndPassword
- On success -> Navigator.pushReplacement to AdminHome
Admin Home (outline):
- Two tabs: Manage News, Manage Services
- List items pulled from Firestore (stream)
- FAB to add new item (opens dialog)
- Edit/Delete actions for each item
----------------- Example helper: lib/services/firestore_service.dart -----------------
// NOTE: This is a snippet — copy into a new file and import where needed.
/*
import 'package:cloud_firestore/cloud_firestore.dart';
class FirestoreService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
// News
Stream>> streamNews() {
return _db.collection('news').orderBy('timestamp', descending: true).snapshots().map((snap) => snap.docs.map((d) => {'id': d.id, ...d.data()}).toList());
}
Future addNews(Map data) async {
await _db.collection('news').add({...data, 'timestamp': FieldValue.serverTimestamp()});
}
Future updateNews(String id, Map data) async {
await _db.collection('news').doc(id).update(data);
}
Future deleteNews(String id) async {
await _db.collection('news').doc(id).delete();
}
// Services (similar)
Stream>> streamServices() {
return _db.collection('services').snapshots().map((snap) => snap.docs.map((d) => {'id': d.id, ...d.data()}).toList());
}
Future addService(Map data) async {
await _db.collection('services').add(data);
}
Future updateService(String id, Map data) async {
await _db.collection('services').doc(id).update(data);
}
Future deleteService(String id) async {
await _db.collection('services').doc(id).delete();
}
}
*/
----------------- Example Admin Login (lib/admin/admin_login.dart) -----------------
/*
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AdminLoginPage extends StatefulWidget {
@override
_AdminLoginPageState createState() => _AdminLoginPageState();
}
class _AdminLoginPageState extends State {
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
bool loading = false;
void _login() async {
setState(() => loading = true);
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(email: _emailCtrl.text.trim(), password: _passCtrl.text.trim());
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => AdminHomePage()));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Login failed: \$e')));
} finally {
setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Admin Login')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(children: [
TextField(controller: _emailCtrl, decoration: InputDecoration(labelText: 'Email')),
TextField(controller: _passCtrl, decoration: InputDecoration(labelText: 'Password'), obscureText: true),
SizedBox(height: 12),
ElevatedButton(onPressed: _login, child: loading ? CircularProgressIndicator() : Text('Login'))
]),
),
);
}
}
*/
----------------- Example Admin Home (lib/admin/admin_home.dart) -----------------
/*
import 'package:flutter/material.dart';
import '../services/firestore_service.dart';
class AdminHomePage extends StatelessWidget {
final fs = FirestoreService();
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(title: Text('Admin Panel'), bottom: TabBar(tabs: [Tab(text: 'News'), Tab(text: 'Services')])),
body: TabBarView(children: [
NewsManager(fs: fs),
ServicesManager(fs: fs),
]),
),
);
}
}
// NewsManager & ServicesManager would use the streams from FirestoreService and provide UI to add/edit/delete.
*/
======================================================================================
Next steps I can do for you (choose any):
1) Integrate the mobile app to read/write from Firestore (I'll modify main.dart to use FirestoreService).
2) Create the admin panel full code files (admin_login.dart, admin_home.dart, firestore_service.dart) inside the project and wire routing.
3) Provide step-by-step Firebase console setup and the exact `google-services.json` and `index.html` changes you need to paste (I'll give the template).
4) Or, if you prefer PHP + MySQL admin backend instead of Firebase, I can add that.
I have added the Admin Panel + Auth blueprint and code snippets to this document. Tell me which of the next steps above you want me to perform now—I'll proceed and add those code files directly into the project textdoc.
*/
0 $type={blogger}:
Post a Comment