diff --git a/lib/backend/models/ConnectionRequest.js b/lib/backend/models/ConnectionRequest.js index 623dd39..1908b5c 100644 --- a/lib/backend/models/ConnectionRequest.js +++ b/lib/backend/models/ConnectionRequest.js @@ -30,8 +30,9 @@ const connectionRequestSchema = new mongoose.Schema({ } }); -// Prevent duplicate requests -connectionRequestSchema.index({ from: 1, to: 1 }, { unique: true }); +// Index for efficient querying (removed unique constraint to allow resending after rejection) +connectionRequestSchema.index({ from: 1, to: 1 }); +connectionRequestSchema.index({ from: 1, to: 1, status: 1 }); // Update the updatedAt field before saving connectionRequestSchema.pre('save', function(next) { diff --git a/lib/backend/models/Event.js b/lib/backend/models/Event.js new file mode 100644 index 0000000..50a0705 --- /dev/null +++ b/lib/backend/models/Event.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); + +const eventSchema = new mongoose.Schema( + { + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + date: { type: Date, required: true, index: true }, // store date at 00:00 UTC + title: { type: String, required: true, trim: true }, + }, + { timestamps: true } +); + +// Index for efficient querying by user and date +eventSchema.index({ user: 1, date: 1 }); + +module.exports = mongoose.model('Event', eventSchema); \ No newline at end of file diff --git a/lib/backend/package.json b/lib/backend/package.json index 5690f98..63d6b8f 100644 --- a/lib/backend/package.json +++ b/lib/backend/package.json @@ -1,27 +1,24 @@ { - "name": "barter-system-backend", + "name": "barter-backend", "version": "1.0.0", "description": "Backend for Barter System", "main": "server.js", "scripts": { "start": "node server.js", - "dev": "nodemon server.js" + "dev": "nodemon server.js", + "migrate": "node scripts/dropUniqueIndex.js" }, "dependencies": { - "@google/generative-ai": "^0.24.1", - "bcrypt": "^5.1.1", - "cloudinary": "^2.7.0", - "cors": "^2.8.5", - "dotenv": "^16.3.1", "express": "^4.18.2", - "firebase-admin": "^13.5.0", - "jsonwebtoken": "^9.0.2", - "mongoose": "^7.5.0", - "multer": "^2.0.2", - "socket.io": "^4.8.1", - "zod": "^3.22.4" + "mongoose": "^7.0.0", + "cors": "^2.8.5", + "jsonwebtoken": "^9.0.0", + "bcryptjs": "^2.4.3", + "multer": "^1.4.5-lts.1", + "socket.io": "^4.7.2", + "firebase-admin": "^11.0.0" }, "devDependencies": { - "nodemon": "^3.1.10" + "nodemon": "^2.0.20" } -} +} \ No newline at end of file diff --git a/lib/backend/routes/connectionRoutes.js b/lib/backend/routes/connectionRoutes.js index 917d9ba..71fb9f2 100644 --- a/lib/backend/routes/connectionRoutes.js +++ b/lib/backend/routes/connectionRoutes.js @@ -18,10 +18,11 @@ router.post('/send', verifyToken, async (req, res) => { }); } - // Check if request already exists + // Check if request already exists and is still pending const existingRequest = await ConnectionRequest.findOne({ from: fromUserId, - to: toUserId + to: toUserId, + status: 'pending' }); if (existingRequest) { @@ -31,6 +32,21 @@ router.post('/send', verifyToken, async (req, res) => { }); } + // Check if there's an accepted connection (they're already connected) + const acceptedRequest = await ConnectionRequest.findOne({ + $or: [ + { from: fromUserId, to: toUserId, status: 'accepted' }, + { from: toUserId, to: fromUserId, status: 'accepted' } + ] + }); + + if (acceptedRequest) { + return res.status(400).json({ + success: false, + message: 'You are already connected with this user' + }); + } + // Don't allow self-requests if (fromUserId === toUserId) { return res.status(400).json({ @@ -64,6 +80,15 @@ router.post('/send', verifyToken, async (req, res) => { } catch (error) { console.error('Error sending connection request:', error); + + // Handle duplicate key error specifically + if (error.code === 11000) { + return res.status(400).json({ + success: false, + message: 'Connection request already sent' + }); + } + res.status(500).json({ success: false, message: 'Internal server error' @@ -76,16 +101,42 @@ router.get('/received', verifyToken, async (req, res) => { try { const userId = req.user.userId; - const requests = await ConnectionRequest.find({ + // First, get all pending requests + const allPendingRequests = await ConnectionRequest.find({ to: userId, status: 'pending' }) .populate('from', 'name profileImage logo email') .sort({ createdAt: -1 }); + // Filter out requests from users who are already connected + const validRequests = []; + + for (const request of allPendingRequests) { + const fromUserId = request.from._id; + + // Check if there's an accepted connection between these users + const existingConnection = await ConnectionRequest.findOne({ + $or: [ + { from: userId, to: fromUserId, status: 'accepted' }, + { from: fromUserId, to: userId, status: 'accepted' } + ] + }); + + // Only include the request if users are not already connected + if (!existingConnection) { + validRequests.push(request); + } else { + // If users are already connected, mark this pending request as invalid + // and optionally delete it to clean up the database + console.log(`Removing invalid request from connected user: ${fromUserId}`); + await ConnectionRequest.findByIdAndDelete(request._id); + } + } + res.json({ success: true, - data: requests + data: validRequests }); } catch (error) { @@ -138,6 +189,23 @@ router.post('/accept/:requestId', verifyToken, async (req, res) => { }); } + // Check if users are already connected (additional safety check) + const existingConnection = await ConnectionRequest.findOne({ + $or: [ + { from: request.from, to: request.to, status: 'accepted' }, + { from: request.to, to: request.from, status: 'accepted' } + ] + }); + + if (existingConnection) { + // Delete the invalid pending request + await ConnectionRequest.findByIdAndDelete(requestId); + return res.status(400).json({ + success: false, + message: 'Users are already connected' + }); + } + // Update request status request.status = 'accepted'; await request.save(); @@ -196,6 +264,23 @@ router.post('/reject/:requestId', verifyToken, async (req, res) => { }); } + // Check if users are already connected (additional safety check) + const existingConnection = await ConnectionRequest.findOne({ + $or: [ + { from: request.from, to: request.to, status: 'accepted' }, + { from: request.to, to: request.from, status: 'accepted' } + ] + }); + + if (existingConnection) { + // Delete the invalid pending request + await ConnectionRequest.findByIdAndDelete(requestId); + return res.status(400).json({ + success: false, + message: 'Users are already connected' + }); + } + // Update request status request.status = 'rejected'; await request.save(); diff --git a/lib/backend/routes/eventRoutes.js b/lib/backend/routes/eventRoutes.js new file mode 100644 index 0000000..a54d32a --- /dev/null +++ b/lib/backend/routes/eventRoutes.js @@ -0,0 +1,91 @@ +const express = require('express'); +const router = express.Router(); +const verifyToken = require('../middlewares/verifyToken'); +const Event = require('../models/Event'); + +// Helper to normalize a date string (YYYY-MM-DD) to UTC midnight +function normalizeDate(dateStr) { + // Expecting 'YYYY-MM-DD' from client + const [y, m, d] = dateStr.split('-').map(Number); + return new Date(Date.UTC(y, m - 1, d, 0, 0, 0, 0)); +} + +// Get events for a date +// GET /api/events?date=YYYY-MM-DD +router.get('/', verifyToken, async (req, res) => { + try { + const dateStr = req.query.date; + if (!dateStr) return res.status(400).json({ message: 'date (YYYY-MM-DD) is required' }); + + const date = normalizeDate(dateStr); + const nextDate = new Date(date); + nextDate.setUTCDate(nextDate.getUTCDate() + 1); + + const events = await Event.find({ + user: req.user.userId, + date: { $gte: date, $lt: nextDate }, + }).sort({ createdAt: 1 }); + + res.json(events); + } catch (err) { + console.error('GET /api/events error', err); + res.status(500).json({ message: 'Server error' }); + } +}); + +// Create event +// POST /api/events { date: 'YYYY-MM-DD', title: string } +router.post('/', verifyToken, async (req, res) => { + try { + const { date: dateStr, title } = req.body; + if (!dateStr || !title) return res.status(400).json({ message: 'date and title are required' }); + + const date = normalizeDate(dateStr); + const event = await Event.create({ user: req.user.userId, date, title }); + res.status(201).json(event); + } catch (err) { + console.error('POST /api/events error', err); + res.status(500).json({ message: 'Server error' }); + } +}); + +// Update event (title) +// PATCH /api/events/:id { title } +router.patch('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { title } = req.body; + + if (typeof title !== 'string' || !title.trim()) { + return res.status(400).json({ message: 'title is required' }); + } + + const updated = await Event.findOneAndUpdate( + { _id: id, user: req.user.userId }, + { $set: { title: title.trim() } }, + { new: true } + ); + + if (!updated) return res.status(404).json({ message: 'Event not found' }); + res.json(updated); + } catch (err) { + console.error('PATCH /api/events/:id error', err); + res.status(500).json({ message: 'Server error' }); + } +}); + +// Delete event +// DELETE /api/events/:id +router.delete('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const deleted = await Event.findOneAndDelete({ _id: id, user: req.user.userId }); + if (!deleted) return res.status(404).json({ message: 'Event not found' }); + res.json({ success: true }); + } catch (err) { + console.error('DELETE /api/events/:id error', err); + res.status(500).json({ message: 'Server error' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/lib/backend/routes/skillMatch.js b/lib/backend/routes/skillMatch.js index e40ab72..3e68579 100644 --- a/lib/backend/routes/skillMatch.js +++ b/lib/backend/routes/skillMatch.js @@ -11,12 +11,12 @@ router.get('/match', async (req, res) => { return res.status(400).json({ msg: 'Required and offered skills are needed' }); } - // Find users with complementary skills + // Find users with complementary skills (case-sensitive matching) // User offers what we need AND needs what we offer const users = await User.find({ $and: [ - { skillsOffered: { $regex: required, $options: 'i' } }, - { skillsRequired: { $regex: offered, $options: 'i' } } + { skillsOffered: { $regex: required, $options: '' } }, // Remove 'i' flag for case-sensitive + { skillsRequired: { $regex: offered, $options: '' } } // Remove 'i' flag for case-sensitive ] }).select('_id name profileImage education location profession skillsOffered skillsRequired logo'); diff --git a/lib/backend/scripts/dropUniqueIndex.js b/lib/backend/scripts/dropUniqueIndex.js new file mode 100644 index 0000000..3293bbc --- /dev/null +++ b/lib/backend/scripts/dropUniqueIndex.js @@ -0,0 +1,57 @@ +const mongoose = require('mongoose'); + +// Connect to MongoDB +mongoose.connect('mongodb://localhost:27017/barter', { + useNewUrlParser: true, + useUnifiedTopology: true, +}); + +const db = mongoose.connection; + +db.once('open', async () => { + try { + console.log('Connected to MongoDB'); + + // Drop the unique index on from_1_to_1 + const collection = db.collection('connectionrequests'); + + try { + await collection.dropIndex('from_1_to_1'); + console.log('✅ Successfully dropped unique index: from_1_to_1'); + } catch (error) { + if (error.code === 27) { + console.log('ℹ️ Index from_1_to_1 does not exist (already dropped)'); + } else { + console.error('❌ Error dropping index:', error); + } + } + + // Create new non-unique indexes + try { + await collection.createIndex({ from: 1, to: 1 }); + console.log('✅ Created new non-unique index: from_1_to_1'); + } catch (error) { + console.error('❌ Error creating new index:', error); + } + + try { + await collection.createIndex({ from: 1, to: 1, status: 1 }); + console.log('✅ Created new compound index: from_1_to_1_status_1'); + } catch (error) { + console.error('❌ Error creating compound index:', error); + } + + console.log('🎉 Database migration completed successfully!'); + console.log('Now users can send new connection requests after rejection.'); + + process.exit(0); + } catch (error) { + console.error('❌ Migration failed:', error); + process.exit(1); + } +}); + +db.on('error', (error) => { + console.error('❌ MongoDB connection error:', error); + process.exit(1); +}); diff --git a/lib/backend/server.js b/lib/backend/server.js index d6e9240..0a914bc 100644 --- a/lib/backend/server.js +++ b/lib/backend/server.js @@ -13,6 +13,7 @@ const connectionRoutes = require('./routes/connectionRoutes.js'); // Connection const notificationRoutes = require('./routes/notificationRoutes.js'); // Notification routes const reviewRoutes = require('./routes/reviewRoutes.js'); // Review routes const todoRoutes = require('./routes/todoRoutes.js'); // Todo routes +const eventRoutes = require('./routes/eventRoutes.js'); // Event routes const cors = require('cors'); const Message = require('./models/message.js'); // Import the Message model @@ -93,6 +94,9 @@ app.use('/api/reviews', reviewRoutes); // Register todo routes app.use('/api/todos', todoRoutes); +// Register event routes +app.use('/api/events', eventRoutes); + // Export io for use in other modules module.exports = { io }; diff --git a/lib/chat.dart b/lib/chat.dart index 4436a66..b35d40b 100644 --- a/lib/chat.dart +++ b/lib/chat.dart @@ -73,7 +73,7 @@ class _ChatState extends State { 'senderId': message['from']['_id'], 'senderName': message['from']['name'], 'senderProfile': message['from']['profileImage'], // new line - 'status': 'sent', + 'status': 'sent', }); } }); @@ -390,89 +390,102 @@ class _ChatState extends State { itemBuilder: (context, index) { final msg = _messages[index]; return Align( - alignment: msg['isMe'] ? Alignment.centerRight : Alignment.centerLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Left side (receiver avatar) - if (!msg['isMe']) ...[ - CircleAvatar( - radius: 12, - backgroundColor: Colors.grey.shade300, - backgroundImage: msg['senderProfile'] != null - ? NetworkImage(msg['senderProfile']) - : null, - child: msg['senderProfile'] == null - ? Text( - msg['senderName'][0].toUpperCase(), - style: const TextStyle( - color: Color(0xFF123b53), - fontWeight: FontWeight.bold), - ) - : null, - ), - const SizedBox(width: 6), - ], - - // Message bubble - Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - margin: const EdgeInsets.symmetric(vertical: 4), - decoration: BoxDecoration( - color: msg['isMe'] - ? const Color.fromARGB(255, 58, 137, 164) - : const Color(0xFFB6E1F0), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - msg['text'], - style: TextStyle( - color: msg['isMe'] ? Colors.white : Colors.black87), - ), - const SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - msg['time'], - style: TextStyle( - color: msg['isMe'] ? Colors.white70 : Colors.black54, - fontSize: 10), - ), - if (msg['isMe']) ...[ - const SizedBox(width: 4), - _buildMessageStatusIcon(msg['status'] ?? 'sent'), - ], - ], - ), - ], - ), - ), - - // Right side (sender avatar) - if (msg['isMe']) ...[ - const SizedBox(width: 6), - CircleAvatar( - radius: 12, - backgroundColor: Colors.grey.shade300, - backgroundImage: msg['senderProfile'] != null - ? NetworkImage(msg['senderProfile']) - : null, - child: msg['senderProfile'] == null - ? const Icon(Icons.person, - size: 14, color: Color(0xFF123b53)) - : null, - ), - ], - ], - ), -); - + alignment: msg['isMe'] + ? Alignment.centerRight + : Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Left side (receiver avatar) + if (!msg['isMe']) ...[ + CircleAvatar( + radius: 12, + backgroundColor: Colors.grey.shade300, + backgroundImage: msg['senderProfile'] != + null + ? NetworkImage(msg['senderProfile']) + : null, + child: msg['senderProfile'] == null + ? Text( + msg['senderName'][0] + .toUpperCase(), + style: const TextStyle( + color: Color(0xFF123b53), + fontWeight: FontWeight.bold), + ) + : null, + ), + const SizedBox(width: 6), + ], + + // Message bubble + Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 10), + margin: + const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: msg['isMe'] + ? const Color.fromARGB( + 255, 58, 137, 164) + : const Color(0xFFB6E1F0), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + msg['text'], + style: TextStyle( + color: msg['isMe'] + ? Colors.white + : Colors.black87), + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + msg['time'], + style: TextStyle( + color: msg['isMe'] + ? Colors.white70 + : Colors.black54, + fontSize: 10), + ), + if (msg['isMe']) ...[ + const SizedBox(width: 4), + _buildMessageStatusIcon( + msg['status'] ?? 'sent'), + ], + ], + ), + ], + ), + ), + // Right side (sender avatar) + if (msg['isMe']) ...[ + const SizedBox(width: 6), + CircleAvatar( + radius: 12, + backgroundColor: Colors.grey.shade300, + backgroundImage: msg['senderProfile'] != + null + ? NetworkImage(msg['senderProfile']) + : null, + child: msg['senderProfile'] == null + ? const Icon(Icons.person, + size: 14, + color: Color(0xFF123b53)) + : null, + ), + ], + ], + ), + ); }, ), ), diff --git a/lib/chats.dart b/lib/chats.dart index 9552732..f1dbfa8 100644 --- a/lib/chats.dart +++ b/lib/chats.dart @@ -57,9 +57,10 @@ class _ChatsState extends State with SingleTickerProviderStateMixin { // Get current user ID currentUserId = await ChatService.getCurrentUserId(); - if (chats.isEmpty) { // 👈 Only load once - await _loadChats(); - } + if (chats.isEmpty) { + // 👈 Only load once + await _loadChats(); + } if (currentUserId != null) { await _loadChats(); diff --git a/lib/home.dart b/lib/home.dart index 89abbc3..54dc828 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -54,31 +54,31 @@ class TodoList extends StatelessWidget { todos.isEmpty ? const Center(child: Text("No tasks for today.")) : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: todos.length, - itemBuilder: (context, index) { - final todo = todos[index]; - return ListTile( - leading: Checkbox( - value: todo['done'], - onChanged: (value) => onToggle(index, value), - ), - title: Text( - todo['task'], - style: TextStyle( - decoration: todo['done'] - ? TextDecoration.lineThrough - : TextDecoration.none, - ), - ), - trailing: IconButton( - icon: const Icon(Icons.delete, color: Color(0xFF56195B)), - onPressed: () => onDelete(index), - ), - ); - }, - ), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: todos.length, + itemBuilder: (context, index) { + final todo = todos[index]; + return ListTile( + leading: Checkbox( + value: todo['done'], + onChanged: (value) => onToggle(index, value), + ), + title: Text( + todo['task'], + style: TextStyle( + decoration: todo['done'] + ? TextDecoration.lineThrough + : TextDecoration.none, + ), + ), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Color(0xFF56195B)), + onPressed: () => onDelete(index), + ), + ); + }, + ), ], ); } @@ -100,12 +100,67 @@ class _MyHomePageState extends State { final TextEditingController _todoController = TextEditingController(); final TextEditingController _eventController = TextEditingController(); final Map>> _todos = {}; - final Map> _events = {}; + final Map>> _events = {}; + bool _isLoadingTodos = false; + bool _isLoadingEvents = false; + + @override + void initState() { + super.initState(); + _loadTodosForDate(_focusedDay); + _loadEventsForDate(_focusedDay); + } DateTime _getDateKey(DateTime date) { return DateTime(date.year, date.month, date.day); } + // Load todos from backend for specific date + Future _loadTodosForDate(DateTime date) async { + setState(() { + _isLoadingTodos = true; + }); + + try { + final todos = await TodoService.getTodos(date); + if (todos != null) { + final dateKey = _getDateKey(date); + setState(() { + _todos[dateKey] = todos; + }); + } + } catch (e) { + print('Error loading todos: $e'); + } finally { + setState(() { + _isLoadingTodos = false; + }); + } + } + + // Load events from backend for specific date + Future _loadEventsForDate(DateTime date) async { + setState(() { + _isLoadingEvents = true; + }); + + try { + final events = await EventService.getEvents(date); + if (events != null) { + final dateKey = _getDateKey(date); + setState(() { + _events[dateKey] = events; + }); + } + } catch (e) { + print('Error loading events: $e'); + } finally { + setState(() { + _isLoadingEvents = false; + }); + } + } + void _showAddEventDialog() { _eventController.clear(); showDialog( @@ -122,16 +177,41 @@ class _MyHomePageState extends State { child: const Text("Cancel"), ), ElevatedButton( - onPressed: () { + onPressed: () async { if (_eventController.text.isEmpty) return; + + final title = _eventController.text; final dateKey = _getDateKey(_selectedDay ?? _focusedDay); - setState(() { - _events[dateKey] = [ - ...(_events[dateKey] ?? []), - _eventController.text - ]; - }); Navigator.pop(context); + + try { + final newEvent = await EventService.createEvent(dateKey, title); + if (newEvent != null) { + setState(() { + _events[dateKey] = [...(_events[dateKey] ?? []), newEvent]; + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to add event. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + print('Error adding event: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Network error. Please check your connection.'), + backgroundColor: Colors.red, + ), + ); + } + } }, child: const Text("Add"), ), @@ -270,9 +350,15 @@ class _MyHomePageState extends State { _selectedDay = selectedDay; _focusedDay = focusedDay; }); + // Load todos and events for selected date + _loadTodosForDate(selectedDay); + _loadEventsForDate(selectedDay); }, eventLoader: (day) { - return _events[_getDateKey(day)] ?? []; + final events = _events[_getDateKey(day)] ?? []; + return events + .map((event) => event['title'] ?? '') + .toList(); }, calendarFormat: _calendarFormat, onFormatChanged: (format) { @@ -326,7 +412,7 @@ class _MyHomePageState extends State { final event = entry.value; return ListTile( leading: const Icon(Icons.event), - title: Text(event), + title: Text(event['title'] ?? 'No title'), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -335,7 +421,8 @@ class _MyHomePageState extends State { color: Colors.deepPurple), onPressed: () { TextEditingController editController = - TextEditingController(text: event); + TextEditingController( + text: event['title'] ?? ''); showDialog( context: context, builder: (context) => AlertDialog( @@ -348,15 +435,59 @@ class _MyHomePageState extends State { Navigator.pop(context), child: const Text("Cancel")), TextButton( - onPressed: () { - setState(() { - todaysEvents[index] = - editController.text; - _events[dateKey] = [ - ...todaysEvents - ]; - }); + onPressed: () async { + if (editController.text.isEmpty) + return; + + final eventId = event['_id']; + if (eventId == null) { + Navigator.pop(context); + return; + } + Navigator.pop(context); + + try { + final updatedEvent = + await EventService + .updateEvent(eventId, + editController.text); + if (updatedEvent != null) { + setState(() { + final updatedEvents = [ + ...todaysEvents + ]; + updatedEvents[index] = + updatedEvent; + _events[dateKey] = + updatedEvents; + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Failed to update event. Please try again.'), + backgroundColor: + Colors.red, + ), + ); + } + } + } catch (e) { + print('Error updating event: $e'); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Network error. Please check your connection.'), + backgroundColor: Colors.red, + ), + ); + } + } }, child: const Text("Save"), ), @@ -368,11 +499,49 @@ class _MyHomePageState extends State { IconButton( icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () { - setState(() { - todaysEvents.removeAt(index); - _events[dateKey] = [...todaysEvents]; - }); + onPressed: () async { + final eventId = event['_id']; + if (eventId == null) { + print('Event ID is null, cannot delete'); + return; + } + + try { + final success = + await EventService.deleteEvent(eventId); + if (success) { + setState(() { + final updatedEvents = + List>.from( + todaysEvents); + updatedEvents.removeAt(index); + _events[dateKey] = updatedEvents; + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Failed to delete event. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + print('Error deleting event: $e'); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Network error. Please check your connection.'), + backgroundColor: Colors.red, + ), + ); + } + } }, ), ], @@ -390,34 +559,127 @@ class _MyHomePageState extends State { selectedDate: dateKey, controller: _todoController, todos: todaysTodos, - onAdd: () { + onAdd: () async { if (_todoController.text.isNotEmpty) { - setState(() { - _todos[dateKey] = [ - ...todaysTodos, - {"task": _todoController.text, "done": false} - ]; - _todoController.clear(); - }); + final task = _todoController.text; + _todoController.clear(); + + try { + final newTodo = + await TodoService.createTodo(dateKey, task); + if (newTodo != null) { + setState(() { + _todos[dateKey] = [...todaysTodos, newTodo]; + }); + } else { + // Show error message + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Failed to add todo. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + print('Error adding todo: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Network error. Please check your connection.'), + backgroundColor: Colors.red, + ), + ); + } + } } }, - onToggle: (index, value) { - setState(() { - final updatedTodos = [...todaysTodos]; - updatedTodos[index] = { - ...updatedTodos[index], - 'done': value, - }; - _todos[dateKey] = updatedTodos; - }); + onToggle: (index, value) async { + final todo = todaysTodos[index]; + final todoId = todo['_id']; + + if (todoId == null) { + print('Todo ID is null, cannot update'); + return; + } + + try { + final updatedTodo = + await TodoService.updateTodo(todoId, done: value); + if (updatedTodo != null) { + setState(() { + final updatedTodos = [...todaysTodos]; + updatedTodos[index] = updatedTodo; + _todos[dateKey] = updatedTodos; + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Failed to update todo. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + print('Error updating todo: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Network error. Please check your connection.'), + backgroundColor: Colors.red, + ), + ); + } + } }, - onDelete: (index) { - setState(() { - final updatedTodos = - List>.from(todaysTodos); - updatedTodos.removeAt(index); - _todos[dateKey] = updatedTodos; - }); + onDelete: (index) async { + final todo = todaysTodos[index]; + final todoId = todo['_id']; + + if (todoId == null) { + print('Todo ID is null, cannot delete'); + return; + } + + try { + final success = await TodoService.deleteTodo(todoId); + if (success) { + setState(() { + final updatedTodos = + List>.from(todaysTodos); + updatedTodos.removeAt(index); + _todos[dateKey] = updatedTodos; + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Failed to delete todo. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + print('Error deleting todo: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Network error. Please check your connection.'), + backgroundColor: Colors.red, + ), + ); + } + } }, ), Padding( diff --git a/lib/notification.dart b/lib/notification.dart index 377cf39..3f61c22 100644 --- a/lib/notification.dart +++ b/lib/notification.dart @@ -16,6 +16,8 @@ class _NotificationsState extends State { List> connectionRequests = []; List> allNotifications = []; int unreadCount = 0; + Set _processingRequests = + {}; // Track requests being processed to prevent duplicates @override void initState() { @@ -23,6 +25,47 @@ class _NotificationsState extends State { _loadNotifications(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Refresh notifications when returning to this screen + // This ensures we have the latest pending connection requests + _loadNotifications(); + } + + // Method to refresh notifications (can be called manually) + Future refreshNotifications() async { + await _loadNotifications(); + } + + // Method to clean up processed requests from UI + void _cleanupProcessedRequests() { + setState(() { + // Remove any notifications that are no longer pending + notifications.forEach((section, notificationList) { + notificationList.removeWhere((notification) { + if (notification["type"] == "connection_request") { + final status = notification["status"] ?? "pending"; + return status != "pending"; + } + return false; + }); + }); + + // Remove empty sections + notifications + .removeWhere((section, notificationList) => notificationList.isEmpty); + }); + } + + // Get current user ID for validation + String? _getCurrentUserId() { + // This should be implemented to get the current user's ID + // For now, return null to skip self-request validation + // TODO: Implement proper current user ID retrieval + return null; + } + Future _loadNotifications() async { setState(() { isLoading = true; @@ -34,18 +77,65 @@ class _NotificationsState extends State { final notificationsData = await NotificationService.getNotifications(); if (requests != null) { - connectionRequests = requests; + // Comprehensive filtering to ensure only valid pending requests + connectionRequests = requests.where((request) { + try { + // Basic null check (request can't be null in where clause, but keeping for safety) + + // Check status + final status = request["status"] ?? "pending"; + if (status != "pending") return false; + + // Check required fields + if (request["_id"] == null || request["_id"].toString().isEmpty) + return false; + if (request["from"] == null) return false; + + final from = request["from"]; + if (from is! Map) return false; + if (from["name"] == null || from["name"].toString().isEmpty) + return false; + if (from["_id"] == null || from["_id"].toString().isEmpty) + return false; + + // Basic ID format validation + if (request["_id"].toString().length < 10) return false; + if (from["_id"].toString().length < 10) return false; + + return true; + } catch (e) { + print('Error validating request in filter: $e, request: $request'); + return false; + } + }).toList(); + + print( + 'Loaded ${connectionRequests.length} valid pending connection requests'); + } else { + connectionRequests = []; // Ensure it's not null + print('No connection requests received from backend'); } if (notificationsData != null) { allNotifications = List>.from(notificationsData['data'] ?? []); unreadCount = notificationsData['unreadCount'] ?? 0; + } else { + allNotifications = []; // Ensure it's not null + unreadCount = 0; } _buildNotificationsMap(); + + // Clean up any processed requests from UI + _cleanupProcessedRequests(); } catch (e) { print('Error loading notifications: $e'); + // Set empty arrays on error to prevent null issues + connectionRequests = []; + allNotifications = []; + unreadCount = 0; + _buildNotificationsMap(); } setState(() { @@ -87,43 +177,97 @@ class _NotificationsState extends State { }); } - // Add connection requests + // Add connection requests (only valid pending ones) for (var request in connectionRequests) { - tempNotifications["Today"]!.add({ - "type": "connection_request", - "requestId": request["_id"], - "user": request["from"]["name"], - "userId": request["from"]["_id"], - "profileImage": - request["from"]["profileImage"] ?? request["from"]["logo"], - "userEmail": request["from"]["email"], - "action": "sent you a connection request", - "message": request["message"] ?? "", - "time": _formatTime(request["createdAt"]), - "icon": Icons.person_add, - "accepted": false, - }); + try { + // Comprehensive validation before adding + // (request can't be null in for loop, but keeping for safety) + + // Validate request ID + if (request["_id"] == null || request["_id"].toString().isEmpty) { + print('Skipping request with invalid ID: $request'); + continue; + } + + // Validate from user data + if (request["from"] == null) { + print('Skipping request with null from data: $request'); + continue; + } + + final from = request["from"]; + if (from is! Map) { + print('Skipping request with invalid from data type: $from'); + continue; + } + + // Validate from user required fields + if (from["name"] == null || from["name"].toString().isEmpty) { + print('Skipping request with invalid from name: $from'); + continue; + } + + if (from["_id"] == null || from["_id"].toString().isEmpty) { + print('Skipping request with invalid from ID: $from'); + continue; + } + + // Ensure only pending requests are shown + final status = request["status"] ?? "pending"; + if (status != "pending") { + print( + 'Skipping non-pending request: ${request["_id"]} with status: $status'); + continue; + } + + // Validate request ID format (should be valid ObjectId) + final requestId = request["_id"].toString(); + if (requestId.length < 10) { + // Basic ObjectId validation + print('Skipping request with invalid ID format: $requestId'); + continue; + } + + // Validate from user ID format + final fromUserId = from["_id"].toString(); + if (fromUserId.length < 10) { + // Basic ObjectId validation + print( + 'Skipping request with invalid from user ID format: $fromUserId'); + continue; + } + + // Additional safety check - ensure request is not from self + // This should be handled by backend, but adding extra safety + if (fromUserId == _getCurrentUserId()) { + print('Skipping self-request: $fromUserId'); + continue; + } + + // All validations passed - add to notifications + tempNotifications["Today"]!.add({ + "type": "connection_request", + "requestId": requestId, + "user": from["name"].toString(), + "userId": fromUserId, + "profileImage": from["profileImage"] ?? from["logo"], + "userEmail": from["email"], + "action": "sent you a connection request", + "message": request["message"] ?? "", + "time": _formatTime(request["createdAt"]), + "icon": Icons.person_add, + "accepted": false, + "status": status, + }); + + print('Added valid connection request: ${from["name"]} (${requestId})'); + } catch (e) { + print('Error processing connection request: $e, request: $request'); + continue; // Skip this request and continue with others + } } - // Add some static notifications for demo including sample connection responses - tempNotifications["Today"]!.addAll([ - { - "type": "normal", - "user": "SkillSocket", - "action": "Sample profiles help you explore the interface. Try different skills to find real users!", - "time": "1h ago", - "icon": Icons.info_outline, - "showThumbnail": false - }, - { - "type": "normal", - "user": "System", - "action": "Connection requests to sample profiles are simulated. Use varied skills to discover real matches.", - "time": "3h ago", - "icon": Icons.psychology, - "showThumbnail": false - }, - ]); + // Removed sample profile messages as requested setState(() { notifications = tempNotifications; @@ -201,13 +345,33 @@ class _NotificationsState extends State { return; } + // Prevent multiple requests for the same request + final requestIdStr = requestId.toString(); + if (_processingRequests.contains(requestIdStr)) { + _showOverlayMessage("Request is already being processed"); + return; + } + + // Mark as processing + setState(() { + _processingRequests.add(requestIdStr); + }); + try { final success = await ConnectionService.acceptConnectionRequest(requestId.toString()); if (success) { setState(() { - notifications[section]?[index]["accepted"] = true; + // Remove the notification after successful acceptance + if (notifications[section] != null) { + notifications[section]!.removeAt(index); + if (notifications[section]!.isEmpty) { + notifications.remove(section); + } + } + // Remove from processing set + _processingRequests.remove(requestIdStr); }); _showOverlayMessage("Connection request accepted!"); @@ -236,6 +400,8 @@ class _NotificationsState extends State { notifications.remove(section); } } + // Remove from processing set + _processingRequests.remove(requestIdStr); }); _showOverlayMessage("Request already handled"); } @@ -249,6 +415,8 @@ class _NotificationsState extends State { notifications.remove(section); } } + // Remove from processing set + _processingRequests.remove(requestIdStr); }); _showOverlayMessage("Request already handled"); } @@ -266,6 +434,18 @@ class _NotificationsState extends State { return; } + // Prevent multiple requests for the same request + final requestIdStr = requestId.toString(); + if (_processingRequests.contains(requestIdStr)) { + _showOverlayMessage("Request is already being processed"); + return; + } + + // Mark as processing + setState(() { + _processingRequests.add(requestIdStr); + }); + try { final success = await ConnectionService.rejectConnectionRequest(requestId.toString()); @@ -276,6 +456,8 @@ class _NotificationsState extends State { if (notifications[section]?.isEmpty ?? false) { notifications.remove(section); } + // Remove from processing set + _processingRequests.remove(requestIdStr); }); _showOverlayMessage("Connection request rejected"); @@ -286,6 +468,8 @@ class _NotificationsState extends State { if (notifications[section]?.isEmpty ?? false) { notifications.remove(section); } + // Remove from processing set + _processingRequests.remove(requestIdStr); }); _showOverlayMessage("Request already handled"); } @@ -297,6 +481,8 @@ class _NotificationsState extends State { if (notifications[section]?.isEmpty ?? false) { notifications.remove(section); } + // Remove from processing set + _processingRequests.remove(requestIdStr); }); _showOverlayMessage("Request already handled"); } diff --git a/lib/popup.dart b/lib/popup.dart index 024c1ff..671916c 100644 --- a/lib/popup.dart +++ b/lib/popup.dart @@ -39,6 +39,14 @@ class _ProfileEditScreenState extends State { _loadAlreadySentRequests(); // preload sent requests to prevent duplicates across sessions } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Refresh sent requests when returning to this screen + // This allows sending new requests after rejections + _loadAlreadySentRequests(); + } + // Load current user ID to prevent self-requests and self-profile display Future _loadCurrentUser() async { try { @@ -202,57 +210,162 @@ class _ProfileEditScreenState extends State { // Generate completely random demo profiles - NOT from database // These are synthetic users to show when no real matches exist final demoNames = [ - "Alex Chen", "Maya Patel", "Jordan Lee", "Priya Singh", "Lucas Brown", - "Aria Sharma", "Dev Kumar", "Sofia Rodriguez", "Ryan O'Connor", "Zara Ahmed", - "Kai Nakamura", "Elena Petrov", "Marcus Johnson", "Ava Thompson", "Leo Zhang", - "Ananya Gupta", "Ethan Taylor", "Ravi Mehta", "Isabella Garcia", "Arjun Patel", - "Chloe Wilson", "Rohan Sharma", "Emma Davis", "Vikram Singh", "Olivia Brown", - "Karan Verma", "Sophia Martinez", "Aditya Kumar", "Grace Lee", "Nikhil Jain" + "Alex Chen", + "Maya Patel", + "Jordan Lee", + "Priya Singh", + "Lucas Brown", + "Aria Sharma", + "Dev Kumar", + "Sofia Rodriguez", + "Ryan O'Connor", + "Zara Ahmed", + "Kai Nakamura", + "Elena Petrov", + "Marcus Johnson", + "Ava Thompson", + "Leo Zhang", + "Ananya Gupta", + "Ethan Taylor", + "Ravi Mehta", + "Isabella Garcia", + "Arjun Patel", + "Chloe Wilson", + "Rohan Sharma", + "Emma Davis", + "Vikram Singh", + "Olivia Brown", + "Karan Verma", + "Sophia Martinez", + "Aditya Kumar", + "Grace Lee", + "Nikhil Jain" ]; final demoLocations = [ - "Mumbai, India", "New York, USA", "London, UK", "Toronto, Canada", "Sydney, Australia", - "Berlin, Germany", "Tokyo, Japan", "Paris, France", "Singapore", "Dubai, UAE", - "São Paulo, Brazil", "Stockholm, Sweden", "Amsterdam, Netherlands", "Barcelona, Spain", - "Melbourne, Australia", "Vancouver, Canada", "Seoul, South Korea", "Tel Aviv, Israel", - "Zurich, Switzerland", "Dublin, Ireland", "Vienna, Austria", "Copenhagen, Denmark", - "Helsinki, Finland", "Oslo, Norway", "Prague, Czech Republic", "Warsaw, Poland", - "Budapest, Hungary", "Lisbon, Portugal", "Athens, Greece", "Istanbul, Turkey" + "Mumbai, India", + "New York, USA", + "London, UK", + "Toronto, Canada", + "Sydney, Australia", + "Berlin, Germany", + "Tokyo, Japan", + "Paris, France", + "Singapore", + "Dubai, UAE", + "São Paulo, Brazil", + "Stockholm, Sweden", + "Amsterdam, Netherlands", + "Barcelona, Spain", + "Melbourne, Australia", + "Vancouver, Canada", + "Seoul, South Korea", + "Tel Aviv, Israel", + "Zurich, Switzerland", + "Dublin, Ireland", + "Vienna, Austria", + "Copenhagen, Denmark", + "Helsinki, Finland", + "Oslo, Norway", + "Prague, Czech Republic", + "Warsaw, Poland", + "Budapest, Hungary", + "Lisbon, Portugal", + "Athens, Greece", + "Istanbul, Turkey" ]; final demoEducations = [ - "B.Tech Computer Science", "M.Sc Data Science", "B.E Software Engineering", "MBA Technology", - "B.Sc Information Systems", "M.Tech AI/ML", "B.Des UI/UX", "M.Sc Cybersecurity", - "B.Com + Digital Marketing", "Ph.D Computer Science", "B.Sc Mathematics", "M.A Psychology", - "B.Tech Information Technology", "M.S Computer Science", "B.Sc Software Engineering", - "M.Tech Data Science", "B.Des Product Design", "M.Sc Artificial Intelligence", - "B.E Electronics & Communication", "MBA Business Analytics", "B.Sc Applied Mathematics", - "M.A Digital Media", "B.Tech Mechanical Engineering", "M.Sc Machine Learning", - "B.Sc Statistics", "M.Tech Software Engineering", "B.A Computer Applications", - "M.Sc Information Technology", "B.Tech Civil Engineering", "MBA Marketing" + "B.Tech Computer Science", + "M.Sc Data Science", + "B.E Software Engineering", + "MBA Technology", + "B.Sc Information Systems", + "M.Tech AI/ML", + "B.Des UI/UX", + "M.Sc Cybersecurity", + "B.Com + Digital Marketing", + "Ph.D Computer Science", + "B.Sc Mathematics", + "M.A Psychology", + "B.Tech Information Technology", + "M.S Computer Science", + "B.Sc Software Engineering", + "M.Tech Data Science", + "B.Des Product Design", + "M.Sc Artificial Intelligence", + "B.E Electronics & Communication", + "MBA Business Analytics", + "B.Sc Applied Mathematics", + "M.A Digital Media", + "B.Tech Mechanical Engineering", + "M.Sc Machine Learning", + "B.Sc Statistics", + "M.Tech Software Engineering", + "B.A Computer Applications", + "M.Sc Information Technology", + "B.Tech Civil Engineering", + "MBA Marketing" ]; final demoProfessions = [ - "Software Developer", "Data Analyst", "ML Engineer", "Product Manager", "Full Stack Developer", - "UI/UX Designer", "DevOps Engineer", "Cybersecurity Analyst", "Digital Marketing Specialist", - "Research Scientist", "Technical Writer", "Business Analyst", "Game Developer", "Mobile App Developer", - "Frontend Developer", "Backend Developer", "Data Scientist", "Cloud Architect", "AI Research Engineer", - "Quality Assurance Engineer", "System Administrator", "Database Administrator", "Network Engineer", - "Blockchain Developer", "IoT Developer", "AR/VR Developer", "Site Reliability Engineer", - "Solutions Architect", "Engineering Manager", "Scrum Master", "Product Owner", "Growth Hacker", - "Content Strategist", "SEO Specialist", "Social Media Manager", "Brand Manager" + "Software Developer", + "Data Analyst", + "ML Engineer", + "Product Manager", + "Full Stack Developer", + "UI/UX Designer", + "DevOps Engineer", + "Cybersecurity Analyst", + "Digital Marketing Specialist", + "Research Scientist", + "Technical Writer", + "Business Analyst", + "Game Developer", + "Mobile App Developer", + "Frontend Developer", + "Backend Developer", + "Data Scientist", + "Cloud Architect", + "AI Research Engineer", + "Quality Assurance Engineer", + "System Administrator", + "Database Administrator", + "Network Engineer", + "Blockchain Developer", + "IoT Developer", + "AR/VR Developer", + "Site Reliability Engineer", + "Solutions Architect", + "Engineering Manager", + "Scrum Master", + "Product Owner", + "Growth Hacker", + "Content Strategist", + "SEO Specialist", + "Social Media Manager", + "Brand Manager" ]; final demoComments = [ - "Great collaboration skills and very patient teacher!", "Helped me understand complex concepts with ease.", - "Professional approach and excellent communication.", "Would love to work together on future projects.", - "Knowledgeable and always willing to share expertise.", "Perfect match for skill exchange, highly recommended!", - "Clear explanations and supportive learning environment.", "Inspiring mentor with practical industry experience.", - "Amazing problem-solving skills and creative thinking.", "Very responsive and delivers quality work on time.", - "Excellent technical knowledge and leadership qualities.", "Great at breaking down complex topics into simple steps.", - "Fantastic team player with strong analytical skills.", "Outstanding communication and project management abilities.", - "Innovative approach to challenges and solutions.", "Highly skilled professional with deep industry insights.", - "Exceptional mentor who guides with patience and expertise.", "Reliable collaborator with impressive technical depth." + "Great collaboration skills and very patient teacher!", + "Helped me understand complex concepts with ease.", + "Professional approach and excellent communication.", + "Would love to work together on future projects.", + "Knowledgeable and always willing to share expertise.", + "Perfect match for skill exchange, highly recommended!", + "Clear explanations and supportive learning environment.", + "Inspiring mentor with practical industry experience.", + "Amazing problem-solving skills and creative thinking.", + "Very responsive and delivers quality work on time.", + "Excellent technical knowledge and leadership qualities.", + "Great at breaking down complex topics into simple steps.", + "Fantastic team player with strong analytical skills.", + "Outstanding communication and project management abilities.", + "Innovative approach to challenges and solutions.", + "Highly skilled professional with deep industry insights.", + "Exceptional mentor who guides with patience and expertise.", + "Reliable collaborator with impressive technical depth." ]; List> randomProfiles = []; @@ -266,25 +379,31 @@ class _ProfileEditScreenState extends State { // Generate 8-12 random profiles with complementary skills for more variety final numProfiles = 8 + (DateTime.now().millisecond % 5); // 8-12 profiles - + for (int i = 0; i < numProfiles; i++) { // More randomized selection from larger pools - final nameIndex = (DateTime.now().millisecondsSinceEpoch + i) % demoNames.length; - final locationIndex = (DateTime.now().microsecond + i * 7) % demoLocations.length; - final educationIndex = (DateTime.now().millisecond + i * 13) % demoEducations.length; - final professionIndex = (DateTime.now().microsecond + i * 11) % demoProfessions.length; - + final nameIndex = + (DateTime.now().millisecondsSinceEpoch + i) % demoNames.length; + final locationIndex = + (DateTime.now().microsecond + i * 7) % demoLocations.length; + final educationIndex = + (DateTime.now().millisecond + i * 13) % demoEducations.length; + final professionIndex = + (DateTime.now().microsecond + i * 11) % demoProfessions.length; + randomProfiles.add({ "id": "random_demo_${DateTime.now().millisecondsSinceEpoch}_${i}_${(demoNames[nameIndex].hashCode + i).abs()}", "name": demoNames[nameIndex], "profileImage": null, "skillsOffered": widget.requiredSkill, // Exactly what user needs - "skillsRequired": widget.offeredSkill, // Exactly what user offers + "skillsRequired": widget.offeredSkill, // Exactly what user offers "education": demoEducations[educationIndex], "location": demoLocations[locationIndex], "profession": demoProfessions[professionIndex], - "ratingsValue": 3.5 + (i * 0.25) + (DateTime.now().millisecond % 15) * 0.04, // More varied ratings + "ratingsValue": 3.5 + + (i * 0.25) + + (DateTime.now().millisecond % 15) * 0.04, // More varied ratings "reviews": [ { "reviewer": "Community Member ${(nameIndex + i + 1) % 20 + 1}", @@ -308,7 +427,8 @@ class _ProfileEditScreenState extends State { "title": "Excellent Mentor", "date": "${DateTime.now().subtract(Duration(days: (i + 1) * 3)).day} ${_getMonthName(DateTime.now().month)} 2024", - "comment": demoComments[(professionIndex + i + 5) % demoComments.length] + "comment": + demoComments[(professionIndex + i + 5) % demoComments.length] } ], }); @@ -347,10 +467,11 @@ class _ProfileEditScreenState extends State { try { // This could be expanded to store in SharedPreferences or local database // For now, just log the demo connection request - print('Demo connection request sent to: ${profile["name"]} (${profile["id"]})'); + print( + 'Demo connection request sent to: ${profile["name"]} (${profile["id"]})'); print('Skills offered by demo user: ${profile["skillsOffered"]}'); print('Skills required by demo user: ${profile["skillsRequired"]}'); - + // Future enhancement: Store in local storage for demo notifications // final prefs = await SharedPreferences.getInstance(); // List demoRequests = prefs.getStringList('demo_connection_requests') ?? []; @@ -376,7 +497,11 @@ class _ProfileEditScreenState extends State { final to = (r['to'] is Map) ? (r['to']['_id'] ?? r['to']['id']) : (r['toUserId'] ?? r['to']); - if (to != null) { + final status = r['status'] ?? 'pending'; + + // Only track pending requests (Instagram-like behavior) + // If rejected, allow sending new request + if (to != null && status == 'pending') { _sentRequests.add(to.toString()); } } @@ -402,10 +527,10 @@ class _ProfileEditScreenState extends State { if (_isDemoUser(profile)) { // Simulate successful connection request for demo users _sentRequests.add(toId); - + // Store demo connection request locally (could be used for local notifications) _storeDemoConnectionRequest(profile); - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -438,9 +563,10 @@ class _ProfileEditScreenState extends State { // Already requested in this session ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("You already sent a request to ${profile["name"]}"), + content: Text( + "Connection request already sent to ${profile["name"]}. Please wait for their response."), backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), + duration: const Duration(seconds: 3), ), ); return; @@ -479,9 +605,21 @@ class _ProfileEditScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - "Connection request already sent to ${profile["name"]}"), + "Connection request already sent to ${profile["name"]}. Please wait for their response."), backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), + duration: const Duration(seconds: 3), + ), + ); + } + } else if (errorMessage.toLowerCase().contains('already connected')) { + // Already connected with this user + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text("You are already connected with ${profile["name"]}"), + backgroundColor: Colors.blue, + duration: const Duration(seconds: 3), ), ); } diff --git a/lib/services/chat_service.dart b/lib/services/chat_service.dart index 5cf5488..e548618 100644 --- a/lib/services/chat_service.dart +++ b/lib/services/chat_service.dart @@ -128,7 +128,7 @@ class ChatService { static Future markMessagesAsRead(String partnerId) async { try { final headers = await _getHeaders(); - + final response = await http.post( Uri.parse('$baseUrl/messages/mark-seen/$partnerId'), headers: headers, @@ -176,4 +176,3 @@ class ChatService { } } } - diff --git a/lib/services/connection_service.dart b/lib/services/connection_service.dart index f2e54f7..053fb3d 100644 --- a/lib/services/connection_service.dart +++ b/lib/services/connection_service.dart @@ -23,7 +23,7 @@ class ConnectionService { }) async { try { final headers = await _getHeaders(); - + final response = await http.post( Uri.parse('$baseUrl/send'), headers: headers, @@ -68,14 +68,14 @@ class ConnectionService { static Future>?> getReceivedRequests() async { try { final headers = await _getHeaders(); - + final response = await http.get( Uri.parse('$baseUrl/received'), headers: headers, ); print('Get received requests response: ${response.statusCode}'); - + if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success'] == true) { @@ -93,14 +93,14 @@ class ConnectionService { static Future acceptConnectionRequest(String requestId) async { try { final headers = await _getHeaders(); - + final response = await http.post( Uri.parse('$baseUrl/accept/$requestId'), headers: headers, ); print('Accept request response: ${response.statusCode}'); - + // Try to parse body if present Map? data; try { @@ -112,9 +112,15 @@ class ConnectionService { } // Treat idempotent responses as success (already accepted/handled) - final msg = (data != null ? (data['message']?.toString().toLowerCase() ?? '') : ''); - if (response.statusCode == 400 || response.statusCode == 409 || response.statusCode == 404) { - if (msg.contains('already') || msg.contains('handled') || msg.contains('accepted')) { + final msg = (data != null + ? (data['message']?.toString().toLowerCase() ?? '') + : ''); + if (response.statusCode == 400 || + response.statusCode == 409 || + response.statusCode == 404) { + if (msg.contains('already') || + msg.contains('handled') || + msg.contains('accepted')) { return true; } } @@ -129,14 +135,14 @@ class ConnectionService { static Future rejectConnectionRequest(String requestId) async { try { final headers = await _getHeaders(); - + final response = await http.post( Uri.parse('$baseUrl/reject/$requestId'), headers: headers, ); print('Reject request response: ${response.statusCode}'); - + Map? data; try { data = jsonDecode(response.body); @@ -147,9 +153,15 @@ class ConnectionService { } // Treat idempotent responses as success (already rejected/handled) - final msg = (data != null ? (data['message']?.toString().toLowerCase() ?? '') : ''); - if (response.statusCode == 400 || response.statusCode == 409 || response.statusCode == 404) { - if (msg.contains('already') || msg.contains('handled') || msg.contains('rejected')) { + final msg = (data != null + ? (data['message']?.toString().toLowerCase() ?? '') + : ''); + if (response.statusCode == 400 || + response.statusCode == 409 || + response.statusCode == 404) { + if (msg.contains('already') || + msg.contains('handled') || + msg.contains('rejected')) { return true; } } @@ -164,7 +176,7 @@ class ConnectionService { static Future>?> getSentRequests() async { try { final headers = await _getHeaders(); - + final response = await http.get( Uri.parse('$baseUrl/sent'), headers: headers, diff --git a/lib/services/event_service.dart b/lib/services/event_service.dart new file mode 100644 index 0000000..70523a3 --- /dev/null +++ b/lib/services/event_service.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +class EventService { + static const String baseUrl = 'https://skillsocket-backend.onrender.com/api'; + + // Get auth token from shared preferences + static Future _getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('token'); + } + + // Get headers with authorization + static Future> _getHeaders() async { + final token = await _getToken(); + return { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + // Format date as YYYY-MM-DD + static String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } + + // Get events for a specific date + // GET /api/events?date=YYYY-MM-DD + static Future>?> getEvents(DateTime date) async { + try { + final headers = await _getHeaders(); + final dateStr = _formatDate(date); + + final response = await http.get( + Uri.parse('$baseUrl/events?date=$dateStr'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List data = json.decode(response.body); + return data + .map((item) => { + '_id': item['_id'], + 'title': item['title'], + 'date': item['date'], + }) + .toList(); + } else { + print('Get events error: ${response.statusCode} - ${response.body}'); + return null; + } + } catch (e) { + print('Get events exception: $e'); + return null; + } + } + + // Create a new event + // POST /api/events { date: 'YYYY-MM-DD', title: string } + static Future?> createEvent( + DateTime date, String title) async { + try { + final headers = await _getHeaders(); + final dateStr = _formatDate(date); + + final response = await http.post( + Uri.parse('$baseUrl/events'), + headers: headers, + body: json.encode({'date': dateStr, 'title': title}), + ); + + if (response.statusCode == 201) { + final data = json.decode(response.body); + return { + '_id': data['_id'], + 'title': data['title'], + 'date': data['date'], + }; + } else { + print('Create event error: ${response.statusCode} - ${response.body}'); + return null; + } + } catch (e) { + print('Create event exception: $e'); + return null; + } + } + + // Update an event (edit title) + // PATCH /api/events/:id { title } + static Future?> updateEvent( + String id, String title) async { + try { + final headers = await _getHeaders(); + + final response = await http.patch( + Uri.parse('$baseUrl/events/$id'), + headers: headers, + body: json.encode({'title': title}), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return { + '_id': data['_id'], + 'title': data['title'], + 'date': data['date'], + }; + } else { + print('Update event error: ${response.statusCode} - ${response.body}'); + return null; + } + } catch (e) { + print('Update event exception: $e'); + return null; + } + } + + // Delete an event + // DELETE /api/events/:id + static Future deleteEvent(String id) async { + try { + final headers = await _getHeaders(); + + final response = await http.delete( + Uri.parse('$baseUrl/events/$id'), + headers: headers, + ); + + if (response.statusCode == 200) { + return true; + } else { + print('Delete event error: ${response.statusCode} - ${response.body}'); + return false; + } + } catch (e) { + print('Delete event exception: $e'); + return false; + } + } +} diff --git a/lib/services/firebase_messaging_service.dart b/lib/services/firebase_messaging_service.dart index d3bd6ba..91ba88c 100644 --- a/lib/services/firebase_messaging_service.dart +++ b/lib/services/firebase_messaging_service.dart @@ -22,7 +22,7 @@ class FirebaseMessagingService { if (settings.authorizationStatus == AuthorizationStatus.authorized) { print('User granted permission for notifications'); - + // Get FCM token String? token = await _firebaseMessaging.getToken(); if (token != null) { @@ -74,7 +74,7 @@ class FirebaseMessagingService { // Handle foreground messages (when app is open) static Future _handleForegroundMessage(RemoteMessage message) async { print('Received foreground message: ${message.messageId}'); - + // Show local notification when app is in foreground await _showLocalNotification(message); } @@ -103,7 +103,7 @@ class FirebaseMessagingService { // Navigate based on notification type static void _navigateBasedOnNotification(Map data) { final type = data['type']; - + switch (type) { case 'message': // Navigate to chat screen @@ -112,17 +112,17 @@ class FirebaseMessagingService { // TODO: Add navigation logic print('Navigate to chat: $chatId with $senderId'); break; - + case 'connection_request': // Navigate to notifications screen print('Navigate to notifications screen'); break; - + case 'connection_accepted': // Navigate to connections or chat print('Navigate to connections'); break; - + default: print('Unknown notification type: $type'); } @@ -166,7 +166,7 @@ class FirebaseMessagingService { try { final prefs = await SharedPreferences.getInstance(); final authToken = prefs.getString('token'); - + if (authToken == null) { print('No auth token found, cannot save FCM token'); return; diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 4afe168..a54a1ee 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -23,7 +23,7 @@ class NotificationService { }) async { try { final headers = await _getHeaders(); - + final response = await http.get( Uri.parse('$baseUrl/notifications?page=$page&limit=$limit'), headers: headers, @@ -45,10 +45,11 @@ class NotificationService { } // Mark notifications as read - static Future markNotificationsAsRead(List notificationIds) async { + static Future markNotificationsAsRead( + List notificationIds) async { try { final headers = await _getHeaders(); - + final response = await http.put( Uri.parse('$baseUrl/notifications/read'), headers: headers, @@ -70,7 +71,7 @@ class NotificationService { static Future getUnreadCount() async { try { final headers = await _getHeaders(); - + final response = await http.get( Uri.parse('$baseUrl/notifications/unread-count'), headers: headers, @@ -91,7 +92,7 @@ class NotificationService { static Future sendTestNotification() async { try { final headers = await _getHeaders(); - + final response = await http.post( Uri.parse('$baseUrl/notifications/test'), headers: headers, diff --git a/lib/services/review_service.dart b/lib/services/review_service.dart index b95c5ff..7602eb7 100644 --- a/lib/services/review_service.dart +++ b/lib/services/review_service.dart @@ -5,37 +5,37 @@ import 'package:shared_preferences/shared_preferences.dart'; class ReviewService { // Like a review static Future?> likeReview(String reviewId) async { - try { - final headers = await _getHeaders(); - final response = await http.post( - Uri.parse('$baseUrl/like/$reviewId'), - headers: headers, - ); - if (response.statusCode == 200) { - return jsonDecode(response.body); + try { + final headers = await _getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/like/$reviewId'), + headers: headers, + ); + if (response.statusCode == 200) { + return jsonDecode(response.body); + } + } catch (e) { + print('Exception liking review: $e'); } - } catch (e) { - print('Exception liking review: $e'); + return null; } - return null; -} // Dislike a review static Future?> dislikeReview(String reviewId) async { - try { - final headers = await _getHeaders(); - final response = await http.post( - Uri.parse('$baseUrl/dislike/$reviewId'), - headers: headers, - ); - if (response.statusCode == 200) { - return jsonDecode(response.body); + try { + final headers = await _getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/dislike/$reviewId'), + headers: headers, + ); + if (response.statusCode == 200) { + return jsonDecode(response.body); + } + } catch (e) { + print('Exception disliking review: $e'); } - } catch (e) { - print('Exception disliking review: $e'); + return null; } - return null; -} // Base URL for the review API static const String baseUrl = 'https://skillsocket-backend.onrender.com/api/reviews'; @@ -60,7 +60,7 @@ class ReviewService { }) async { try { final headers = await _getHeaders(); - + final response = await http.post( Uri.parse('$baseUrl/add'), headers: headers, @@ -88,10 +88,7 @@ class ReviewService { } } catch (e) { print('Exception adding review: $e'); - return { - 'success': false, - 'message': 'Network error occurred' - }; + return {'success': false, 'message': 'Network error occurred'}; } } @@ -103,14 +100,14 @@ class ReviewService { }) async { try { final headers = await _getHeaders(); - + final response = await http.get( Uri.parse('$baseUrl/user/$userId?page=$page&limit=$limit'), headers: headers, ); print('Get user reviews response: ${response.statusCode}'); - + if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success'] == true) { @@ -129,21 +126,21 @@ class ReviewService { try { final prefs = await SharedPreferences.getInstance(); final userId = prefs.getString('userId'); - + if (userId == null) { print('No user ID found'); return null; } final headers = await _getHeaders(); - + final response = await http.get( Uri.parse('$baseUrl/by-user/$userId'), headers: headers, ); print('Get my reviews response: ${response.statusCode}'); - + if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success'] == true) { @@ -166,12 +163,12 @@ class ReviewService { }) async { try { final headers = await _getHeaders(); - + final body = {}; if (rating != null) body['rating'] = rating; if (title != null) body['title'] = title; if (skillContext != null) body['skillContext'] = skillContext; - + final response = await http.put( Uri.parse('$baseUrl/update/$reviewId'), headers: headers, @@ -179,7 +176,7 @@ class ReviewService { ); print('Update review response: ${response.statusCode}'); - + if (response.statusCode == 200) { final data = jsonDecode(response.body); return data; @@ -192,10 +189,7 @@ class ReviewService { } } catch (e) { print('Exception updating review: $e'); - return { - 'success': false, - 'message': 'Network error occurred' - }; + return {'success': false, 'message': 'Network error occurred'}; } } @@ -203,14 +197,14 @@ class ReviewService { static Future deleteReview(String reviewId) async { try { final headers = await _getHeaders(); - + final response = await http.delete( Uri.parse('$baseUrl/delete/$reviewId'), headers: headers, ); print('Delete review response: ${response.statusCode}'); - + if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['success'] == true; @@ -231,13 +225,13 @@ class ReviewService { static String getStarDisplay(double rating) { final fullStars = rating.floor(); final hasHalfStar = (rating - fullStars) >= 0.5; - + String stars = '★' * fullStars; if (hasHalfStar) stars += '☆'; - + final emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); stars += '☆' * emptyStars; - + return stars; } } diff --git a/lib/services/todo_service.dart b/lib/services/todo_service.dart index 76f5943..e5ee4f6 100644 --- a/lib/services/todo_service.dart +++ b/lib/services/todo_service.dart @@ -31,7 +31,7 @@ class TodoService { try { final headers = await _getHeaders(); final dateStr = _formatDate(date); - + final response = await http.get( Uri.parse('$baseUrl/todos?date=$dateStr'), headers: headers, @@ -39,12 +39,14 @@ class TodoService { if (response.statusCode == 200) { final List data = json.decode(response.body); - return data.map((item) => { - '_id': item['_id'], - 'task': item['task'], - 'done': item['done'], - 'date': item['date'], - }).toList(); + return data + .map((item) => { + '_id': item['_id'], + 'task': item['task'], + 'done': item['done'], + 'date': item['date'], + }) + .toList(); } else { print('Get todos error: ${response.statusCode} - ${response.body}'); return null; @@ -57,7 +59,8 @@ class TodoService { // Create a new todo // POST /api/todos { date: 'YYYY-MM-DD', task: string } - static Future?> createTodo(DateTime date, String task) async { + static Future?> createTodo( + DateTime date, String task) async { try { final headers = await _getHeaders(); final dateStr = _formatDate(date); @@ -88,7 +91,8 @@ class TodoService { // Update a todo (toggle done or edit task) // PATCH /api/todos/:id { task?, done? } - static Future?> updateTodo(String id, {String? task, bool? done}) async { + static Future?> updateTodo(String id, + {String? task, bool? done}) async { try { final headers = await _getHeaders(); final body = {}; diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index b9545c4..0ae646c 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -11,6 +11,7 @@ class UserService { final prefs = await SharedPreferences.getInstance(); return prefs.getString('userId'); } + // Optionally expose token if needed elsewhere static Future getAuthToken() async { final prefs = await SharedPreferences.getInstance(); @@ -30,7 +31,8 @@ class UserService { static Future?> getUserProfile() async { try { final headers = await _getHeaders(); - final res = await http.get(Uri.parse('$baseUrl/user/profile'), headers: headers); + final res = + await http.get(Uri.parse('$baseUrl/user/profile'), headers: headers); if (res.statusCode == 200) { final data = json.decode(res.body); return data['user']; @@ -45,7 +47,8 @@ class UserService { static Future?> getUserProfileById(String userId) async { try { final headers = await _getHeaders(); - final res = await http.get(Uri.parse('$baseUrl/user/profile/$userId'), headers: headers); + final res = await http.get(Uri.parse('$baseUrl/user/profile/$userId'), + headers: headers); if (res.statusCode == 200) { final data = json.decode(res.body); return data['user']; diff --git a/lib/signup2.dart b/lib/signup2.dart index b7daf84..5094155 100644 --- a/lib/signup2.dart +++ b/lib/signup2.dart @@ -1,4 +1,5 @@ import 'package:barter_system/home.dart'; +import 'package:barter_system/login.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; @@ -47,7 +48,7 @@ class _SignUpPageState extends State { print('🖼️ Starting profile image upload...'); print('🖼️ File path: ${profileImageFile.path}'); print('🖼️ File exists: ${await profileImageFile.exists()}'); - + var request = http.MultipartRequest( 'POST', Uri.parse('https://skillsocket-backend.onrender.com/api/user/upload-logo-public'), @@ -66,7 +67,7 @@ class _SignUpPageState extends State { print('🖼️ File added to request. Sending...'); final streamedResponse = await request.send(); print('🖼️ Response status code: ${streamedResponse.statusCode}'); - + final response = await http.Response.fromStream(streamedResponse); print('🖼️ Response body: ${response.body}'); @@ -105,7 +106,7 @@ class _SignUpPageState extends State { if (widget.userData?['profileImageFile'] != null) { print('📸 Profile image file found, starting upload...'); final profileImageFile = widget.userData!['profileImageFile'] as File; - + // Show uploading message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -115,13 +116,14 @@ class _SignUpPageState extends State { ), ); } - + profileImageUrl = await _uploadProfileImageToCloudinary(profileImageFile); if (profileImageUrl == null) { print('❌ Profile image upload returned null'); setState(() { - _errorMessage = 'Failed to upload profile image. Please check your internet connection and try again.'; + _errorMessage = + 'Failed to upload profile image. Please check your internet connection and try again.'; _isLoading = false; }); return; @@ -220,16 +222,17 @@ class _SignUpPageState extends State { // Show success message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Account created successfully!'), + content: Text( + 'Account created successfully! Please login to continue.'), backgroundColor: Colors.green, ), ); - // Navigate to home page - Navigator.pushReplacement( + // Navigate to login page + Navigator.pushAndRemoveUntil( context, - MaterialPageRoute( - builder: (context) => MyHomePage(title: 'SkillSocket')), + MaterialPageRoute(builder: (context) => LoginScreen()), + (route) => false, ); } } else { @@ -496,4 +499,4 @@ class _SignUpPageState extends State { ), ); } -} \ No newline at end of file +}