diff --git a/.gitignore b/.gitignore index b8fe4cc..7dc495a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,11 @@ backend/.DS_Store node_modules/ backend/.env - -## Frontend ignored file (Root level) +backend/.idea ## Frontend ignored file (Root level) +frontend/.env # Miscellaneous frontend/*.class diff --git a/backend/app.js b/backend/app.js index e8b54e7..4bb9c28 100644 --- a/backend/app.js +++ b/backend/app.js @@ -10,6 +10,10 @@ import cors from "cors"; import tokenRequired from "./middlewares/tokenRequired.js"; import adminResource from "./resources/adminResource.js"; +import roomListResource from "./resources/rooms/roomListResource.js"; +import roomResource from "./resources/rooms/roomResource.js"; +import lostAndFoundListResource from "./resources/lostAndFound/lostAndFoundListResource.js"; + const PORT = `${process.env.PORT || 3000}`; const app = express(); @@ -26,6 +30,9 @@ app.use("/admin-auth", adminAuthResource); app.use(authResource); app.use("otp", otpResource); app.use("/", testResource); +app.use("/rooms", roomListResource); +app.use("/room", roomResource); +app.use("/lost-and-found", lostAndFoundListResource); app.get("/protected", tokenRequired, (req, res) => { res.json({ message: "Access granted" }); diff --git a/backend/constants/errorMessages.js b/backend/constants/errorMessages.js index 82c0512..ba0efea 100644 --- a/backend/constants/errorMessages.js +++ b/backend/constants/errorMessages.js @@ -2,26 +2,29 @@ export const userAlreadyExists = "User with this email already exists!"; export const userNotFound = "User with this email does not exist!"; export const incorrectPassword = "Incorrect password."; -export const internalServerError = "Internal Server Error. Please try again later."; +export const internalServerError = + "Internal Server Error. Please try again later."; export const userCreated = "User created successfully"; //Database Connection export const databaseConnected = "Database connected successfully"; export const databaseDisconnected = "Database disconnected"; -export const databaseConnectionError = "Error while connecting with the database"; +export const databaseConnectionError = + "Error while connecting with the database"; -// OTP -export const emailIsRequired = 'Email is required'; -export const failedToSendOTPEmail = 'Failed to send OTP to email'; -export const emailAndOTPRequired = 'Email and OTP are required'; -export const noOTPFoundForEmail = 'No OTP found for the email'; -export const incorrectOTP = 'Incorrect OTP'; -export const otpVerfied = 'OTP verified successfully'; -export const otpSent = 'OTP sent successfully'; +// OTP +export const emailIsRequired = "Email is required"; +export const failedToSendOTPEmail = "Failed to send OTP to to email"; +export const emailAndOTPRequired = "Email and OTP are required"; +export const noOTPFoundForEmail = "No OTP found for the email"; +export const incorrectOTP = "Incorrect OTP"; +export const otpVerfied = "OTP verified successfully"; +export const otpSent = "OTP sent successfully"; // Auth Middleware -export const noAuthToken = 'No auth token, access denied'; -export const invalidAuthToken = 'Invalid token'; -export const tokenVerificationFailed = 'Token verification failed, authorization denied.'; -export const invalidUserType = 'Invalid user type'; -export const tokenUpdateError= 'Error updating token'; \ No newline at end of file +export const noAuthToken = "No auth token, access denied"; +export const invalidAuthToken = "Invalid token"; +export const tokenVerificationFailed = + "Token verification failed, authorization denied."; +export const invalidUserType = "Invalid user type"; +export const tokenUpdateError = "Error updating token"; diff --git a/backend/middlewares/multerConfig.js b/backend/middlewares/multerConfig.js new file mode 100644 index 0000000..384f487 --- /dev/null +++ b/backend/middlewares/multerConfig.js @@ -0,0 +1,15 @@ +import multer from "multer"; + +// Define storage configuration +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, "uploads/"); + }, + filename: function (req, file, cb) { + cb(null, file.originalname); + }, +}); + +const uploader = multer({ storage: storage }); + +export default uploader; diff --git a/backend/models/lost_and_found.js b/backend/models/lost_and_found.js index 2ff415d..a191c73 100644 --- a/backend/models/lost_and_found.js +++ b/backend/models/lost_and_found.js @@ -1,30 +1,33 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; const lostAndFoundItemSchema = new mongoose.Schema({ - name: { - type: String, - required: true - }, - lastSeenLocation: { - type: String, - }, - imagePath: { - type: String, - }, - description: { - type: String, - required: true - }, - contactNumber: { - type: String, - required: true - }, - isLost: { - type: Boolean, - required: true - } + name: { + type: String, + required: true, + }, + lastSeenLocation: { + type: String, + }, + imagePath: { + type: String, + nullable: true, + }, + description: { + type: String, + }, + contactNumber: { + type: String, + required: true, + }, + isLost: { + type: Boolean, + required: true, + }, }); -const LostAndFoundItem = mongoose.model('LostAndFoundItem', lostAndFoundItemSchema); +const LostAndFoundItem = mongoose.model( + "LostAndFoundItem", + lostAndFoundItemSchema +); -export default LostAndFoundItem; \ No newline at end of file +export default LostAndFoundItem; diff --git a/backend/models/room.js b/backend/models/room.js index 2af0ea3..a2ca4c1 100644 --- a/backend/models/room.js +++ b/backend/models/room.js @@ -1,26 +1,20 @@ - -const mongoose = require('mongoose'); +import mongoose from "mongoose"; const roomSchema = new mongoose.Schema({ - id: { - type: String, - required: true, - }, - name: { - type: String, - required: true, - }, - vacant: { - type: Boolean, - default: true, - }, - occupantId: { - type: String, - default: null, - }, + name: { + type: String, + required: true, + }, + vacant: { + type: Boolean, + default: true, + }, + occupantId: { + type: String, + default: null, + }, }); -const Room = mongoose.model('Room', roomSchema); - -module.exports = Room; +const Room = mongoose.model("Room", roomSchema); +export default Room; diff --git a/backend/resources/lostAndFound/lostAndFoundListResource.js b/backend/resources/lostAndFound/lostAndFoundListResource.js new file mode 100644 index 0000000..0a682ce --- /dev/null +++ b/backend/resources/lostAndFound/lostAndFoundListResource.js @@ -0,0 +1,74 @@ +import { Router } from "express"; +import LostAndFoundItem from "../../models/lost_and_found.js"; +import fs from "fs/promises"; +import uploader from "../../middlewares/multerConfig.js"; + +const router = Router(); + +// GET method to retrieve all items +router.get("/", async (req, res) => { + try { + // Query the database to retrieve all items + const items = await LostAndFoundItem.find({}); + + // Create an empty array to store items with images + const itemsWithImages = []; + + // Iterate through each item + for (const item of items) { + // Check if imagePath is null + let imagePathBase64 = null; + if (item.imagePath) { + // Read the image file if imagePath is not null + const bufferImage = await fs.readFile(item.imagePath); + imagePathBase64 = bufferImage.toString("base64"); + } + + // Create a new object with the required attributes + const itemWithImage = { + _id: item._id, + name: item.name, + lastSeenLocation: item.lastSeenLocation, + imagePath: imagePathBase64, // Set imagePath to null if null in the database + description: item.description, + contactNumber: item.contactNumber, + isLost: item.isLost, + }; + + // Push the item with image to the array + itemsWithImages.push(itemWithImage); + } + + console.log("Retrieved items:", itemsWithImages.length); + + // Send the response with the items + res.json(itemsWithImages); + } catch (error) { + // Handle errors + console.error("Error:", error); + res.status(500).send("Error retrieving items"); + } +}); + +// POST method +router.post("/", uploader.single("image"), async (req, res) => { + // Access the uploaded file using req.file + const file = req.file; + + // Construct the LostAndFoundItem object with data from the request + const newItem = new LostAndFoundItem({ + name: req.body.name, + lastSeenLocation: req.body.lastSeenLocation, + imagePath: file ? file.path : null, + description: req.body.description, + contactNumber: req.body.contactNumber, + isLost: req.body.isLost, + }); + + // Save the new item to the database + await newItem.save(); + + res.send("Added new item"); +}); + +export default router; diff --git a/backend/resources/rooms/occupantListResource.js b/backend/resources/rooms/occupantListResource.js new file mode 100644 index 0000000..1fb4034 --- /dev/null +++ b/backend/resources/rooms/occupantListResource.js @@ -0,0 +1,27 @@ +import { Router } from "express"; +import Room from "../../models/room.js"; +import Student from "../../models/student.js"; +const router = Router(); + +//GET method +router.get("/", async (req, res) => { + try { + const documentIds = req.body.documentIds; // Assuming the documentIds are sent in the request body + + const occupants = await Promise.all( + documentIds.map(async (documentId) => { + const room = await Room.findOne({ documentId }); + const occupant = await Student.findOne({ _id: room.occupantId }); + return { + occupantName: occupant.name, + roomId: room._id, + }; + }) + ); + + res.json(occupants); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}); diff --git a/backend/resources/rooms/roomListResource.js b/backend/resources/rooms/roomListResource.js new file mode 100644 index 0000000..ceb9cc6 --- /dev/null +++ b/backend/resources/rooms/roomListResource.js @@ -0,0 +1,40 @@ +import { Router } from "express"; +import Room from "../../models/room.js"; +const router = Router(); + +// GET method +router.get("/", async (req, res) => { + // Your code here + const rooms = await Room.find({}); + res.send(rooms); +}); + +// POST method +router.post("/", async (req, res) => { + try { + // Extract data from request body + const { name, vacant, occupantId } = req.body; + + // Create a new room instance + const newRoom = new Room({ + name, + vacant: vacant || true, // Set default value if not provided + occupantId: occupantId || null, // Set default value if not provided + }); + + // Save the new room to the database + await newRoom.save(); + console.log("Room created successfully"); + + // Respond with success message + res + .status(201) + .json({ message: "Room created successfully", room: newRoom }); + } catch (error) { + // Handle errors + console.error("Error creating room:", error); + res.status(500).json({ error: "Internal server error" }); + } +}); + +export default router; diff --git a/backend/resources/rooms/roomResource.js b/backend/resources/rooms/roomResource.js new file mode 100644 index 0000000..f78114f --- /dev/null +++ b/backend/resources/rooms/roomResource.js @@ -0,0 +1,28 @@ +import { Router } from "express"; +import Room from "../../models/room.js"; +const router = Router(); + +// PUT method +router.put("/:id", async (req, res) => { + try { + const { id } = req.params; + const { occupantId } = req.body; + const room = await Room.findById(id); + if (!room) { + return res.status(404).json({ error: "Room not found" }); + } + + room.vacant = false; + room.occupantId = occupantId || room.occupantId; + + await room.save(); + + console.log("Room updated successfully"); + res.json({ message: "Room updated successfully", room }); + } catch (error) { + console.error("Error updating room:", error); + res.status(500).json({ error: "Internal server error" }); + } +}); + +export default router; diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index 443f830..59be995 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -16,20 +16,41 @@ while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" /> - - + + + - - + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/lib/assets/placeholder.png b/frontend/lib/assets/placeholder.png index a242799..42e388f 100644 Binary files a/frontend/lib/assets/placeholder.png and b/frontend/lib/assets/placeholder.png differ diff --git a/frontend/lib/assets/profile_placeholder.png b/frontend/lib/assets/profile_placeholder.png new file mode 100644 index 0000000..a242799 Binary files /dev/null and b/frontend/lib/assets/profile_placeholder.png differ diff --git a/frontend/lib/components/image_tile.dart b/frontend/lib/components/image_tile.dart new file mode 100644 index 0000000..0717239 --- /dev/null +++ b/frontend/lib/components/image_tile.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class ImageTile extends StatelessWidget { + const ImageTile( + {super.key, + this.icon, + required this.onTap, + required this.primaryColor, + required this.secondaryColor, + this.body, + this.contentPadding, + this.image}); + + final Image? image; + final List? body; + final IconData? icon; + final Function onTap; + final EdgeInsets? contentPadding; + final Color primaryColor; + final Color secondaryColor; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + child: Material( + borderRadius: BorderRadius.circular(15), + child: Ink( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(15), color: primaryColor), + child: InkWell( + overlayColor: MaterialStateProperty.all(secondaryColor), + borderRadius: BorderRadius.circular(15), + splashColor: secondaryColor, + onTap: () => onTap(), + child: Center( + child: Container( + padding: contentPadding ?? const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(height: 20), + image != null + ? ClipRRect(borderRadius: BorderRadius.circular(10), child: image) + : Container( + width: double.infinity, + height: 115, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white, + ), + child: const Icon(Icons.image, size: 50, color: Colors.black45), + ), + const SizedBox(height: 10), + ] + + (body ?? []), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/components/menu_tile.dart b/frontend/lib/components/menu_tile.dart index 11828f9..eec90d8 100644 --- a/frontend/lib/components/menu_tile.dart +++ b/frontend/lib/components/menu_tile.dart @@ -2,13 +2,22 @@ import 'package:flutter/material.dart'; class MenuTile extends StatelessWidget { const MenuTile( - {super.key, required this.title, this.icon, required this.onTap, this.primaryColor ,this.secondaryColor }); + {super.key, + required this.title, + this.icon, + required this.onTap, + required this.primaryColor, + required this.secondaryColor, + this.body, + this.contentPadding}); final String title; + final List? body; final IconData? icon; final Function onTap; - final Color? primaryColor; - final Color? secondaryColor; + final EdgeInsets? contentPadding; + final Color primaryColor; + final Color secondaryColor; @override Widget build(BuildContext context) { @@ -17,23 +26,29 @@ class MenuTile extends StatelessWidget { child: Material( borderRadius: BorderRadius.circular(15), child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color:primaryColor ?? Colors.grey[100], - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(15), color: primaryColor), child: InkWell( - overlayColor: MaterialStateProperty.all(secondaryColor ?? Colors.grey[300]), + overlayColor: MaterialStateProperty.all(secondaryColor), borderRadius: BorderRadius.circular(15), - splashColor: secondaryColor ?? Colors.grey[200], + splashColor: secondaryColor, onTap: () => onTap(), child: Center( - child: Text( - title, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.black, - fontSize: 21, - fontFamily: "GoogleSansFlex", + child: Padding( + padding: contentPadding ?? const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.black, + fontSize: 21, + fontFamily: "GoogleSansFlex", + ), + ), + ] + + (body ?? []), ), ), ), diff --git a/frontend/lib/constants/constants.dart b/frontend/lib/constants/constants.dart index ab1ecfa..b18a610 100644 --- a/frontend/lib/constants/constants.dart +++ b/frontend/lib/constants/constants.dart @@ -5,6 +5,8 @@ class AppConstants { static const Color seedColor = Colors.lightBlueAccent; } +enum LoadingState { progress, success, error } + class AuthConstants { static const String facultyAuthLabel = "Faculty"; static const String studentAuthLabel = "Student"; @@ -80,6 +82,11 @@ class StudentRoles { ]; } +class LostAndFoundConstants { + static const String lostState = 'Lost'; + static const String foundState = 'Found'; +} + class MessMenuConstants { static Map>> emptyMenu = { 'Sunday': >{ @@ -137,7 +144,8 @@ class MessMenuConstants { ]; static final List mealTypes = [ - Text('Breakfast', style: TextStyle(color: Colors.teal.shade900, fontSize: 14)), + Text('Breakfast', + style: TextStyle(color: Colors.teal.shade900, fontSize: 14)), Text('Lunch', style: TextStyle(color: Colors.teal.shade900, fontSize: 14)), Text('Snacks', style: TextStyle(color: Colors.teal.shade900, fontSize: 14)), Text('Dinner', style: TextStyle(color: Colors.teal.shade900, fontSize: 14)), @@ -155,6 +163,34 @@ class MessMenuConstants { } class Validators { + static String? descriptionValidator(String? value) { + if (value != null && value.length > 250) { + return "Description cannot exceed 250 characters"; + } + + return null; + } + + static String? contactNumberValidator(String? value) { + if (value == null || value.isEmpty) { + return "Contact number cannot be empty"; + } + + // Check if the contact number contains only digits + if (!RegExp(r'^\+?\d+$').hasMatch(value)) { + return "Invalid contact number format"; + } + + return null; + } + + static String? nonEmptyValidator(String? value) { + if (value == null || value.isEmpty) { + return "Field cannot be empty"; + } + return null; + } + static String? emailValidator(String? value) { if (value == null || value.isEmpty) { return "Email cannot be empty"; diff --git a/frontend/lib/constants/dummy_entries.dart b/frontend/lib/constants/dummy_entries.dart index 32f06d8..220c056 100644 --- a/frontend/lib/constants/dummy_entries.dart +++ b/frontend/lib/constants/dummy_entries.dart @@ -1,6 +1,7 @@ import '../models/achievement.dart'; import '../models/course.dart'; import '../models/faculty.dart'; +import '../models/lost_and_found_item.dart'; import '../models/mess_menu.dart'; import '../models/room.dart'; import '../models/skills.dart'; @@ -433,11 +434,7 @@ class DummyFaculties { id: '2', name: 'Prof. Johnson', email: 'johnson@example.com', - courses: [ - DummyCourses.courses[1], - DummyCourses.courses[6], - DummyCourses.courses[11] - ], + courses: [DummyCourses.courses[1], DummyCourses.courses[6], DummyCourses.courses[11]], cabinNumber: 'C-102', department: 'Mechanical Engineering', ), @@ -445,11 +442,7 @@ class DummyFaculties { id: '3', name: 'Dr. Brown', email: 'brown@example.com', - courses: [ - DummyCourses.courses[2], - DummyCourses.courses[7], - DummyCourses.courses[12] - ], + courses: [DummyCourses.courses[2], DummyCourses.courses[7], DummyCourses.courses[12]], cabinNumber: 'C-103', department: 'Electrical Engineering', ), @@ -457,187 +450,119 @@ class DummyFaculties { id: '4', name: 'Prof. Davis', email: 'davis@example.com', - courses: [ - DummyCourses.courses[3], - DummyCourses.courses[8], - DummyCourses.courses[13] - ], + courses: [DummyCourses.courses[3], DummyCourses.courses[8], DummyCourses.courses[13]], cabinNumber: 'C-104', department: 'Civil Engineering'), Faculty( id: '5', name: 'Dr. Wilson', email: 'wilson@example.com', - courses: [ - DummyCourses.courses[4], - DummyCourses.courses[9], - DummyCourses.courses[14] - ], + courses: [DummyCourses.courses[4], DummyCourses.courses[9], DummyCourses.courses[14]], cabinNumber: 'C-105', department: 'Chemical Engineering'), Faculty( id: '6', name: 'Prof. Miller', email: 'miller@example.com', - courses: [ - DummyCourses.courses[0], - DummyCourses.courses[5], - DummyCourses.courses[10] - ], + courses: [DummyCourses.courses[0], DummyCourses.courses[5], DummyCourses.courses[10]], cabinNumber: 'C-106', department: 'Biotechnology'), Faculty( id: '7', name: 'Dr. Turner', email: 'turner@example.com', - courses: [ - DummyCourses.courses[1], - DummyCourses.courses[6], - DummyCourses.courses[11] - ], + courses: [DummyCourses.courses[1], DummyCourses.courses[6], DummyCourses.courses[11]], cabinNumber: 'C-107', department: 'Aerospace Engineering'), Faculty( id: '8', name: 'Prof. Clark', email: 'clark@example.com', - courses: [ - DummyCourses.courses[2], - DummyCourses.courses[7], - DummyCourses.courses[12] - ], + courses: [DummyCourses.courses[2], DummyCourses.courses[7], DummyCourses.courses[12]], cabinNumber: 'C-108', department: 'Information Technology'), Faculty( id: '9', name: 'Dr. Harris', email: 'harris@example.com', - courses: [ - DummyCourses.courses[3], - DummyCourses.courses[8], - DummyCourses.courses[13] - ], + courses: [DummyCourses.courses[3], DummyCourses.courses[8], DummyCourses.courses[13]], cabinNumber: 'C-109', department: 'Mechatronics'), Faculty( id: '10', name: 'Prof. Turner', email: 'turner@example.com', - courses: [ - DummyCourses.courses[4], - DummyCourses.courses[9], - DummyCourses.courses[14] - ], + courses: [DummyCourses.courses[4], DummyCourses.courses[9], DummyCourses.courses[14]], cabinNumber: 'C-110', department: 'Robotics Engineering'), Faculty( id: '11', name: 'Dr. White', email: 'white@example.com', - courses: [ - DummyCourses.courses[0], - DummyCourses.courses[5], - DummyCourses.courses[10] - ], + courses: [DummyCourses.courses[0], DummyCourses.courses[5], DummyCourses.courses[10]], cabinNumber: 'D-101', department: 'Industrial Engineering'), Faculty( id: '12', name: 'Prof. Allen', email: 'allen@example.com', - courses: [ - DummyCourses.courses[1], - DummyCourses.courses[6], - DummyCourses.courses[11] - ], + courses: [DummyCourses.courses[1], DummyCourses.courses[6], DummyCourses.courses[11]], cabinNumber: 'D-102', department: 'Computer Engineering'), Faculty( id: '13', name: 'Dr. Young', email: 'young@example.com', - courses: [ - DummyCourses.courses[2], - DummyCourses.courses[7], - DummyCourses.courses[12] - ], + courses: [DummyCourses.courses[2], DummyCourses.courses[7], DummyCourses.courses[12]], cabinNumber: 'D-103', department: 'Software Engineering'), Faculty( id: '14', name: 'Prof. Walker', email: 'walker@example.com', - courses: [ - DummyCourses.courses[3], - DummyCourses.courses[8], - DummyCourses.courses[13] - ], + courses: [DummyCourses.courses[3], DummyCourses.courses[8], DummyCourses.courses[13]], cabinNumber: 'D-104', department: 'Environmental Engineering'), Faculty( id: '15', name: 'Dr. Lee', email: 'lee@example.com', - courses: [ - DummyCourses.courses[4], - DummyCourses.courses[9], - DummyCourses.courses[14] - ], + courses: [DummyCourses.courses[4], DummyCourses.courses[9], DummyCourses.courses[14]], cabinNumber: 'D-105', department: 'Petrolesum[ Engineer]ing'), Faculty( id: '16', name: 'Prof. Hall', email: 'hall@example.com', - courses: [ - DummyCourses.courses[0], - DummyCourses.courses[5], - DummyCourses.courses[10] - ], + courses: [DummyCourses.courses[0], DummyCourses.courses[5], DummyCourses.courses[10]], cabinNumber: 'D-106', department: 'Nuclear Engineering'), Faculty( id: '17', name: 'Dr. Miller', email: 'miller@example.com', - courses: [ - DummyCourses.courses[1], - DummyCourses.courses[6], - DummyCourses.courses[11] - ], + courses: [DummyCourses.courses[1], DummyCourses.courses[6], DummyCourses.courses[11]], cabinNumber: 'D-107', department: 'Biomedical Engineering'), Faculty( id: '18', name: 'Prof. Baker', email: 'baker@example.com', - courses: [ - DummyCourses.courses[2], - DummyCourses.courses[7], - DummyCourses.courses[12] - ], + courses: [DummyCourses.courses[2], DummyCourses.courses[7], DummyCourses.courses[12]], cabinNumber: 'D-108', department: 'Chemical Engineering'), Faculty( id: '19', name: 'Dr. Turner', email: 'turner@example.com', - courses: [ - DummyCourses.courses[3], - DummyCourses.courses[8], - DummyCourses.courses[13] - ], + courses: [DummyCourses.courses[3], DummyCourses.courses[8], DummyCourses.courses[13]], cabinNumber: 'D-109', department: 'Electronics Engineering'), Faculty( id: '20', name: 'Prof. Smith', email: 'smith@example.com', - courses: [ - DummyCourses.courses[4], - DummyCourses.courses[9], - DummyCourses.courses[14] - ], + courses: [DummyCourses.courses[4], DummyCourses.courses[9], DummyCourses.courses[14]], cabinNumber: 'D-110', department: 'Computer Science'), ]; @@ -889,6 +814,7 @@ class DummyMenus { } class DummyRooms { + //static List rooms = []; static List rooms = [ Room(id: '1', name: 'Auditorium', vacant: true), Room(id: '2', name: 'Classroom 101', vacant: false, occupantId: 'T001'), @@ -908,15 +834,86 @@ class DummyRooms { Room(id: '16', name: 'Outdoor Sports Arena', vacant: true), Room(id: '17', name: 'Medical Clinic', vacant: false, occupantId: 'S004'), Room(id: '18', name: 'Music Room', vacant: true), - Room( - id: '19', - name: 'Student Council Office', - vacant: false, - occupantId: 'T005'), + Room(id: '19', name: 'Student Council Office', vacant: false, occupantId: 'T005'), Room(id: '20', name: 'Virtual Reality Lab', vacant: true), ]; } +class DummyLostAndFound { + static List lostAndFoundItems = [ + LostAndFoundItem( + name: 'Laptop', + description: 'Black Dell laptop with a sticker on the back', + lastSeenLocation: 'Library', + contactNumber: '+91 1234567890', + isLost: false, + ), + LostAndFoundItem( + name: 'Mobile Phone', + description: 'White iPhone 12 with a black case', + lastSeenLocation: 'Cafeteria', + contactNumber: '+91 9876543210', + isLost: true, + ), + LostAndFoundItem( + name: 'Water Bottle', + description: 'Blue steel water bottle with a dent on the bottom', + lastSeenLocation: 'Gymnasium', + contactNumber: '+91 4567890123', + isLost: false, + ), + LostAndFoundItem( + name: 'Backpack', + description: 'Red and black backpack with a broken zipper', + lastSeenLocation: 'Auditorium', + contactNumber: '+91 7890123456', + isLost: true, + ), + LostAndFoundItem( + name: 'Watch', + description: 'Silver wristwatch with a black leather strap', + lastSeenLocation: 'Classroom 101', + contactNumber: '+91 2345678901', + isLost: false, + ), + LostAndFoundItem( + name: 'Umbrella', + description: 'Green and white striped umbrella with a broken handle', + lastSeenLocation: 'Student Lounge', + contactNumber: '+91 8901234567', + isLost: true, + ), + LostAndFoundItem( + name: 'Sunglasses', + description: 'Black aviator sunglasses with a scratch on the left lens', + lastSeenLocation: 'Cafeteria', + contactNumber: '+91 3456789012', + isLost: false, + ), + LostAndFoundItem( + name: 'Wallet', + description: 'Brown leather wallet with a broken zipper', + lastSeenLocation: 'Library', + contactNumber: '+91 9012345678', + isLost: true, + ), + LostAndFoundItem( + name: 'Headphones', + description: 'Black over-ear headphones with a missing ear cushion', + lastSeenLocation: 'Auditorium', + contactNumber: '+91 6789012345', + isLost: false, + ), + LostAndFoundItem( + name: 'Jacket', + description: 'Blue denim jacket with a tear on the left sleeve', + lastSeenLocation: 'Gymnasium', + contactNumber: '+91 5678901234', + isLost: true, + ), + ]; +} + class DummySkills { static List skills = [ Skill(id: '1', name: 'Programming', level: 5), @@ -1014,15 +1011,13 @@ class DummyAchievements { id: '12', name: 'Training and Development', date: DateTime(2022, 4, 22), - description: - 'Contributed significantly to employee training and development.', + description: 'Contributed significantly to employee training and development.', ), Achievement( id: '13', name: 'Quality Assurance Recognition', date: DateTime(2023, 2, 14), - description: - 'Acknowledged for ensuring high-quality standards in projects.', + description: 'Acknowledged for ensuring high-quality standards in projects.', ), Achievement( id: '14', @@ -1058,16 +1053,14 @@ class DummyAchievements { id: '19', name: 'Health and Wellness Initiative', date: DateTime(2023, 9, 10), - description: - 'Led initiatives to promote health and wellness in the workplace.', + description: 'Led initiatives to promote health and wellness in the workplace.', ), Achievement( id: '20', name: 'Public Speaking Achievement', date: DateTime(2022, 1, 30), - description: - 'Received acclaim for public speaking skills at a conference.', + description: 'Received acclaim for public speaking skills at a conference.', ), ]; - // You can use the dummyEntries list as needed in your application +// You can use the dummyEntries list as needed in your application } diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index e13fae9..77efd40 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:smart_insti_app/constants/constants.dart'; import 'package:smart_insti_app/routes/routes.dart'; -void main() { +Future main() async { + await dotenv.load(fileName: ".env"); runApp(const ProviderScope(child: SmartInstiApp())); } diff --git a/frontend/lib/models/lost_and_found.dart b/frontend/lib/models/lost_and_found_item.dart similarity index 81% rename from frontend/lib/models/lost_and_found.dart rename to frontend/lib/models/lost_and_found_item.dart index 64a01ad..38a9ef1 100644 --- a/frontend/lib/models/lost_and_found.dart +++ b/frontend/lib/models/lost_and_found_item.dart @@ -1,43 +1,44 @@ +// This class is used to create an object of Lost and Found item + class LostAndFoundItem { - String userId; + String? id; String name; String lastSeenLocation; - String imagePath; + String? imagePath; String description; String contactNumber; bool isLost; LostAndFoundItem({ - required this.userId, + this.id, required this.name, required this.lastSeenLocation, - required this.imagePath, + this.imagePath, required this.description, - required this.contactNumber, required this.isLost, + required this.contactNumber, }); factory LostAndFoundItem.fromJson(Map json) { return LostAndFoundItem( - userId: json['user_id'], + id: json['_id'], name: json['name'], lastSeenLocation: json['lastSeenLocation'], imagePath: json['imagePath'], description: json['description'], - contactNumber: json['contactNumber'], isLost: json['isLost'], + contactNumber: json['contactNumber'], ); } Map toJson() { return { - 'user_id': userId, 'name': name, 'lastSeenLocation': lastSeenLocation, 'imagePath': imagePath, 'description': description, - 'contactNumber': contactNumber, 'isLost': isLost, + 'contact': contactNumber, }; } } diff --git a/frontend/lib/models/room.dart b/frontend/lib/models/room.dart index 105ee48..ea40e55 100644 --- a/frontend/lib/models/room.dart +++ b/frontend/lib/models/room.dart @@ -1,14 +1,14 @@ class Room { - Room({this.occupantId, this.id, required this.name, this.vacant}); + Room({this.occupantId, this.id, required this.name, this.vacant = true}); final String? id; final String name; - final bool? vacant; + final bool vacant; final String? occupantId; factory Room.fromJson(Map json) { return Room( - id: json['id'], + id: json['_id'], name: json['name'], vacant: json['vacant'], occupantId: json['occupantId'], @@ -17,7 +17,6 @@ class Room { Map toJson() { return { - 'id': id, 'name': name, 'vacant': vacant, 'occupantId': occupantId, diff --git a/frontend/lib/models/user.dart b/frontend/lib/models/user.dart new file mode 100644 index 0000000..c858ff1 --- /dev/null +++ b/frontend/lib/models/user.dart @@ -0,0 +1,6 @@ +class User { + String email; + String otp; + + User({required this.email, required this.otp}); +} diff --git a/frontend/lib/provider/lost_and_found_provider.dart b/frontend/lib/provider/lost_and_found_provider.dart new file mode 100644 index 0000000..3b3968b --- /dev/null +++ b/frontend/lib/provider/lost_and_found_provider.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_cropper/image_cropper.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:logger/logger.dart'; +import 'package:smart_insti_app/constants/dummy_entries.dart'; +import 'package:smart_insti_app/models/lost_and_found_item.dart'; +import 'package:smart_insti_app/repositories/lost_and_found_repository.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'dart:io'; +import '../constants/constants.dart'; + +final lostAndFoundProvider = + StateNotifierProvider((ref) => LostAndFoundStateNotifier(ref)); + +class LostAndFoundState { + final List lostAndFoundItemList; + final List lostAndFoundImageList; + final TextEditingController itemNameController; + final TextEditingController itemDescriptionController; + final TextEditingController lastSeenLocationController; + final TextEditingController searchLostAndFoundController; + final TextEditingController contactNumberController; + final String listingStatus; + final File? selectedImage; + final LoadingState loadingState; + + LostAndFoundState({ + required this.lostAndFoundItemList, + required this.lostAndFoundImageList, + required this.itemNameController, + required this.itemDescriptionController, + required this.lastSeenLocationController, + required this.searchLostAndFoundController, + required this.contactNumberController, + required this.listingStatus, + this.selectedImage, + required this.loadingState, + }); + + LostAndFoundState copyWith({ + List? lostAndFoundItemList, + List? lostAndFoundImageList, + TextEditingController? itemNameController, + TextEditingController? itemDescriptionController, + TextEditingController? lastSeenLocationController, + TextEditingController? searchLostAndFoundController, + TextEditingController? contactNumberController, + String? listingStatus, + File? selectedImage, + LoadingState? loadingState, + }) { + return LostAndFoundState( + lostAndFoundItemList: lostAndFoundItemList ?? this.lostAndFoundItemList, + lostAndFoundImageList: lostAndFoundImageList ?? this.lostAndFoundImageList, + itemNameController: itemNameController ?? this.itemNameController, + itemDescriptionController: itemDescriptionController ?? this.itemDescriptionController, + lastSeenLocationController: lastSeenLocationController ?? this.lastSeenLocationController, + searchLostAndFoundController: searchLostAndFoundController ?? this.searchLostAndFoundController, + contactNumberController: contactNumberController ?? this.contactNumberController, + listingStatus: listingStatus ?? this.listingStatus, + selectedImage: selectedImage, + loadingState: loadingState ?? this.loadingState, + ); + } +} + +class LostAndFoundStateNotifier extends StateNotifier { + LostAndFoundStateNotifier(Ref ref) + : _api = ref.read(lostAndFoundRepositoryProvider), + super( + LostAndFoundState( + lostAndFoundItemList: DummyLostAndFound.lostAndFoundItems, + lostAndFoundImageList: [], + itemNameController: TextEditingController(), + itemDescriptionController: TextEditingController(), + lastSeenLocationController: TextEditingController(), + searchLostAndFoundController: TextEditingController(), + contactNumberController: TextEditingController(), + listingStatus: LostAndFoundConstants.lostState, + selectedImage: null, + loadingState: LoadingState.progress, + ), + ) { + loadItems(); + } + + final LostAndFoundRepository _api; + + final Logger _logger = Logger(); + + void addItem() { + final LostAndFoundItem item = LostAndFoundItem( + name: state.itemNameController.text, + lastSeenLocation: state.lastSeenLocationController.text, + imagePath: state.selectedImage?.path, + description: state.itemDescriptionController.text, + isLost: state.listingStatus == LostAndFoundConstants.lostState, + contactNumber: state.contactNumberController.text, + ); + state = state.copyWith(loadingState: LoadingState.progress); + _api.addLostAndFoundItem(item); + loadItems(); + } + + launchCaller(String number) async { + final url = "tel:$number"; + if (await canLaunchUrlString(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } + } + + void updateListingStatus(String status) { + state = state.copyWith(listingStatus: status); + } + + void clearControllers() { + state.itemNameController.clear(); + state.itemDescriptionController.clear(); + state.lastSeenLocationController.clear(); + state.contactNumberController.clear(); + state = state.copyWith(selectedImage: null); + } + + Future _cropImage(XFile image) { + return ImageCropper().cropImage( + sourcePath: image.path, + aspectRatio: const CropAspectRatio(ratioX: 4, ratioY: 3), + compressQuality: 100, + compressFormat: ImageCompressFormat.jpg, + uiSettings: [ + AndroidUiSettings( + toolbarTitle: 'Lost and Found', + toolbarColor: Colors.tealAccent, + toolbarWidgetColor: Colors.white, + initAspectRatio: CropAspectRatioPreset.original, + lockAspectRatio: true, + ), + ], + ); + } + + void pickImageFromCamera() async { + final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera); + + if (pickedFile != null) { + CroppedFile? file = await _cropImage(pickedFile); + state = state.copyWith(selectedImage: File(file!.path)); + _logger.d(file.path); + } + } + + void pickImageFromGallery() async { + final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery); + + if (pickedFile != null) { + CroppedFile? file = await _cropImage(pickedFile); + state = state.copyWith(selectedImage: File(file!.path)); + _logger.d(file.path); + } + _logger.d(pickedFile?.path); + } + + void resetImageSelection() { + state = state.copyWith(selectedImage: null); + _logger.d('Image selection reset'); + } + + void loadItems() async { + final items = await _api.lostAndFoundItems(); + state = state.copyWith(lostAndFoundItemList: items, loadingState: LoadingState.success); + } + + Image imageFromBase64String(String base64String) { + return Image.memory(base64Decode(base64String)); + } +} diff --git a/frontend/lib/provider/room_provider.dart b/frontend/lib/provider/room_provider.dart index 322f37a..a189feb 100644 --- a/frontend/lib/provider/room_provider.dart +++ b/frontend/lib/provider/room_provider.dart @@ -8,46 +8,58 @@ import 'package:smart_insti_app/constants/dummy_entries.dart'; import 'package:smart_insti_app/components/menu_tile.dart'; import 'package:smart_insti_app/models/room.dart'; import 'dart:io'; +import '../constants/constants.dart'; +import '../repositories/room_repository.dart'; -final roomProvider = StateNotifierProvider((ref) => RoomProvider()); +final roomProvider = StateNotifierProvider((ref) => RoomProvider(ref)); class RoomState { final List roomList; final List roomTiles; final TextEditingController searchRoomController; final TextEditingController roomNameController; + final LoadingState loadingState; - RoomState({ - required this.roomList, - required this.roomTiles, - required this.searchRoomController, - required this.roomNameController, - }); + RoomState( + {required this.roomList, + required this.roomTiles, + required this.searchRoomController, + required this.roomNameController, + required this.loadingState}); RoomState copyWith({ List? roomList, List? roomTiles, TextEditingController? searchRoomController, TextEditingController? roomNameController, + LoadingState? loadingState, }) { return RoomState( roomList: roomList ?? this.roomList, roomTiles: roomTiles ?? this.roomTiles, searchRoomController: searchRoomController ?? this.searchRoomController, roomNameController: roomNameController ?? this.roomNameController, + loadingState: loadingState ?? this.loadingState, ); } } class RoomProvider extends StateNotifier { - RoomProvider() - : super(RoomState( - roomList: DummyRooms.rooms, - roomTiles: [], - searchRoomController: TextEditingController(), - roomNameController: TextEditingController(), - )); + RoomProvider(Ref ref) + : _api = ref.read(roomRepositoryProvider), + super( + RoomState( + roomList: DummyRooms.rooms, + roomTiles: [], + searchRoomController: TextEditingController(), + roomNameController: TextEditingController(), + loadingState: LoadingState.progress, + ), + ) { + loadRooms(); + } + final RoomRepository _api; final Logger _logger = Logger(); void pickSpreadsheet() async { @@ -70,6 +82,16 @@ class RoomProvider extends StateNotifier { } } + int getVacantCount() { + int vacantCount = 0; + for (Room room in state.roomList) { + if (room.vacant) { + vacantCount++; + } + } + return vacantCount; + } + void addRoom() { final newState = state.copyWith( roomList: [ @@ -82,7 +104,23 @@ class RoomProvider extends StateNotifier { _logger.i("Added room: ${state.roomNameController.text}"); } - void buildRoomTiles(BuildContext context) { + Future loadRooms() async { + final rooms = await _api.getRooms(); + final newState = state.copyWith(roomList: rooms, loadingState: LoadingState.success); + state = newState; + } + + Future reserveRoom(Room room) async { + state = state.copyWith( + loadingState: LoadingState.progress, + ); + + await _api.reserveRoom(room.id!, '12345'); + + await loadRooms(); + } + + void buildRoomTiles(BuildContext context) async { final roomTiles = []; for (Room room in state.roomList) { roomTiles.add( @@ -119,9 +157,9 @@ class RoomProvider extends StateNotifier { ) ], ), - ), - ), ), + ), + ), icon: Icons.add, primaryColor: Colors.grey.shade200, secondaryColor: Colors.grey.shade300, diff --git a/frontend/lib/repositories/lost_and_found_repository.dart b/frontend/lib/repositories/lost_and_found_repository.dart new file mode 100644 index 0000000..ac04aab --- /dev/null +++ b/frontend/lib/repositories/lost_and_found_repository.dart @@ -0,0 +1,56 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; +import 'package:smart_insti_app/constants/dummy_entries.dart'; +import 'package:smart_insti_app/models/lost_and_found_item.dart'; + +final lostAndFoundRepositoryProvider = Provider((_) => LostAndFoundRepository()); + +class LostAndFoundRepository { + final _client = Dio( + BaseOptions( + baseUrl: dotenv.env['BACKEND_DOMAIN']!, + headers: { + "Content-Type": "multipart/form-data", + }, + ), + ); + + Future> lostAndFoundItems() async { + try { + final response = await _client.get('/lost-and-found'); + List items = []; + for (var item in response.data) { + items.add(LostAndFoundItem.fromJson(item)); + } + return items; + } catch (e) { + Logger().e(e); + return DummyLostAndFound.lostAndFoundItems; + } + } + + Future addLostAndFoundItem(LostAndFoundItem item) async { + try { + String? fileName = item.imagePath?.split('/').last; + FormData formData = FormData.fromMap({ + "name": item.name, + "lastSeenLocation": item.lastSeenLocation, + "description": item.description, + "contactNumber": item.contactNumber, + "isLost": item.isLost, + "image": item.imagePath != null + ? await MultipartFile.fromFile( + item.imagePath!, + filename: fileName, + ) + : null, + }); + final response = await _client.post('/lost-and-found', data: formData); + Logger().w(response.data); + } catch (e) { + Logger().e(e); + } + } +} diff --git a/frontend/lib/repositories/room_repository.dart b/frontend/lib/repositories/room_repository.dart new file mode 100644 index 0000000..e14a1a7 --- /dev/null +++ b/frontend/lib/repositories/room_repository.dart @@ -0,0 +1,47 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; +import 'package:smart_insti_app/constants/dummy_entries.dart'; +import '../models/room.dart'; + +final roomRepositoryProvider = Provider((_) => RoomRepository()); + +class RoomRepository { + final _client = Dio( + BaseOptions( + baseUrl: dotenv.env['BACKEND_DOMAIN']!, + ), + ); + + Future> getRooms() async { + try { + final response = await _client.get('/rooms'); + List rooms = []; + for (var room in response.data) { + rooms.add(Room.fromJson(room)); + } + return rooms; + } catch (e) { + return DummyRooms.rooms; + } + } + + Future reserveRoom(String roomId, String occupantId) async { + try { + final response = await _client.put('/room/$roomId', data: {'occupantId': occupantId}); + Logger().i(response.data); + } catch (e) { + Logger().e(e); + } + } + + void addRoom(Room room) async { + try { + final response = await _client.post('/rooms', data: room.toJson()); + Logger().i(response.data); + } catch (e) { + Logger().e(e); + } + } +} diff --git a/frontend/lib/routes/routes.dart b/frontend/lib/routes/routes.dart index 4ba1694..e909319 100644 --- a/frontend/lib/routes/routes.dart +++ b/frontend/lib/routes/routes.dart @@ -7,6 +7,8 @@ import 'package:smart_insti_app/screens/admin/manage_rooms.dart'; import 'package:smart_insti_app/screens/admin/view_students.dart'; import 'package:smart_insti_app/screens/auth/login_general.dart'; import 'package:smart_insti_app/screens/loading_page.dart'; +import 'package:smart_insti_app/screens/home.dart'; +import 'package:smart_insti_app/screens/lost_and_found.dart'; import '../screens/admin/add_faculty.dart'; import '../screens/admin/add_menu.dart'; import '../screens/admin/admin_profile.dart'; @@ -14,8 +16,10 @@ import '../screens/admin/view_courses.dart'; import '../screens/admin/view_faculty.dart'; import '../screens/admin/view_menu.dart'; import '../screens/auth/admin_login.dart'; +import '../screens/room_vacancy.dart'; final GoRouter routes = GoRouter( + initialLocation: '/home', routes: [ GoRoute( path: '/', @@ -23,11 +27,13 @@ final GoRouter routes = GoRouter( ), GoRoute( path: '/login', - pageBuilder: (context, state) => const MaterialPage(child: GeneralLogin()), + pageBuilder: (context, state) => + const MaterialPage(child: GeneralLogin()), routes: [ GoRoute( path: 'admin_login', - pageBuilder: (context, state) => const MaterialPage(child: AdminLogin()), + pageBuilder: (context, state) => + const MaterialPage(child: AdminLogin()), ), ], ), @@ -37,7 +43,8 @@ final GoRouter routes = GoRouter( routes: [ GoRoute( path: 'profile', - pageBuilder: (context, state) => const MaterialPage(child: AdminProfile()), + pageBuilder: (context, state) => + const MaterialPage(child: AdminProfile()), ), GoRoute( path: 'add_students', @@ -77,5 +84,20 @@ final GoRouter routes = GoRouter( ) ], ), + GoRoute( + path: '/home', + pageBuilder: (context, state) => const MaterialPage(child: Home()), + routes: [ + GoRoute( + path: 'classroom_vacancy', + pageBuilder: (context, state) => + const MaterialPage(child: RoomVacancy()), + ), + GoRoute( + path: 'lost_and_found', + pageBuilder: (context, state) => MaterialPage(child: LostAndFound()), + ), + ], + ), ], ); diff --git a/frontend/lib/screens/auth/signin_page.dart b/frontend/lib/screens/auth/signin_page.dart new file mode 100644 index 0000000..e7ed415 --- /dev/null +++ b/frontend/lib/screens/auth/signin_page.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../components/snackbar.dart'; +import '../../services/auth/auth_service.dart'; + +class SignIn extends StatefulWidget { + @override + _SignInState createState() => _SignInState(); +} + +class _SignInState extends State { + bool showOTPFields = false; + final emailRegex = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+'); + final emailController = TextEditingController(); + final otpController1 = TextEditingController(); + final otpController2 = TextEditingController(); + final otpController3 = TextEditingController(); + final otpController4 = TextEditingController(); + + final focusNode1 = FocusNode(); + final focusNode2 = FocusNode(); + final focusNode3 = FocusNode(); + final focusNode4 = FocusNode(); + + final authService = AuthService(); // Create an instance of AuthService + + @override + void initState() { + super.initState(); + + // When the text in the first OTPBox changes, request focus for the second OTPBox + otpController1.addListener(() { + if (otpController1.text.length >= 1) { + FocusScope.of(context).requestFocus(focusNode2); + } + }); + + // Do the same for the other OTPBoxes + otpController2.addListener(() { + if (otpController2.text.length >= 1) { + FocusScope.of(context).requestFocus(focusNode3); + } + }); + + otpController3.addListener(() { + if (otpController3.text.length >= 1) { + FocusScope.of(context).requestFocus(focusNode4); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Login'), + ), + body: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: emailController, + decoration: InputDecoration( + labelText: 'Email', + ), + ), + SizedBox(height: 16.0), + ElevatedButton( + onPressed: () { + // Call sendOtp when the button is clicked + if (emailRegex.hasMatch(emailController.text)) { + // If the email is valid, set the state + setState(() { + showOTPFields = true; + }); + authService.sendOTP( + context: context, + email: emailController.text, + ); + } else { + // If the email is not valid, show a SnackBar with an error message + showSnackBar( + context, + 'Please enter a valid email', + ); + } + }, + child: Text('Send OTP'), + ), + if (showOTPFields) ...[ + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OTPBox(controller: otpController1, focusNode: focusNode1), + OTPBox(controller: otpController2, focusNode: focusNode2), + OTPBox(controller: otpController3, focusNode: focusNode3), + OTPBox(controller: otpController4, focusNode: focusNode4), + ], + ), + ElevatedButton( + onPressed: () async { + final otp = otpController1.text + + otpController2.text + + otpController3.text + + otpController4.text; + print('OTP: $otp'); // Print the OTP + // Call verifyOTP + final isVerified = + await authService.verifyOTP(emailController.text, otp); + + if (isVerified) { + // Navigate to the admin home page + context.go('/admin_home'); + } else { + showSnackBar( + context, + 'Incorrect OTP', + ); + } + }, + child: Text('Verify OTP'), + ), + ], + ], + ), + ), + ); + } +} + +class OTPBox extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + OTPBox({required this.controller, required this.focusNode}); + @override + Widget build(BuildContext context) { + return Container( + width: 50.0, + height: 50.0, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8.0), + ), + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: TextInputType.number, + maxLength: 1, + textAlign: TextAlign.center, + decoration: InputDecoration( + counterText: '', + border: InputBorder.none, + ), + ), + ); + } +} diff --git a/frontend/lib/screens/home.dart b/frontend/lib/screens/home.dart index 2b8b54a..87fb18d 100644 --- a/frontend/lib/screens/home.dart +++ b/frontend/lib/screens/home.dart @@ -1,19 +1,49 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:smart_insti_app/components/menu_tile.dart'; import '../constants/constants.dart'; +import '../provider/room_provider.dart'; -class Home extends StatelessWidget { +class Home extends ConsumerWidget { const Home({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return SafeArea( child: Scaffold( appBar: AppBar( title: const Text(AppConstants.appName), backgroundColor: Colors.lightBlueAccent, ), - body: const Center( - child: Text(AppConstants.appName), + body: GridView.count( + padding: const EdgeInsets.all(10), + crossAxisCount: 2, + children: [ + MenuTile( + title: 'Room\nVacancy', + onTap: () => context.push('/home/classroom_vacancy'), + body: [ + const SizedBox(height: 5), + Consumer( + builder: (_, ref, __) => Text( + '${ref.read(roomProvider.notifier).getVacantCount()} Vacant', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 14), + ), + ), + ], + icon: Icons.class_, + primaryColor: Colors.lightBlueAccent.shade100, + secondaryColor: Colors.lightBlueAccent.shade200, + ), + MenuTile( + title: "Lost\n&\nFound", + onTap: () => context.push('/home/lost_and_found'), + primaryColor: Colors.orangeAccent.shade100, + secondaryColor: Colors.orangeAccent.shade200, + icon: Icons.search), + ], ), ), ); diff --git a/frontend/lib/screens/lost_and_found.dart b/frontend/lib/screens/lost_and_found.dart new file mode 100644 index 0000000..7a085a8 --- /dev/null +++ b/frontend/lib/screens/lost_and_found.dart @@ -0,0 +1,312 @@ +import 'package:animated_toggle_switch/animated_toggle_switch.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:responsive_framework/responsive_framework.dart'; +import 'package:smart_insti_app/components/borderless_button.dart'; +import 'package:smart_insti_app/components/material_textformfield.dart'; +import 'package:smart_insti_app/provider/lost_and_found_provider.dart'; +import '../components/image_tile.dart'; +import '../constants/constants.dart'; + +class LostAndFound extends ConsumerWidget { + LostAndFound({super.key}); + + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ResponsiveScaledBox( + width: 411, + child: Scaffold( + appBar: AppBar( + title: const Text('Lost & Found'), + actions: [ + BorderlessButton( + backgroundColor: Colors.greenAccent.shade100, + splashColor: Colors.green.shade700, + onPressed: () => showDialog( + context: context, + builder: (context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Add Listing', + style: TextStyle(fontSize: 30), + ), + const SizedBox(height: 20), + Consumer( + builder: (_, ref, __) { + if (ref.watch(lostAndFoundProvider).selectedImage == null) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => ref.read(lostAndFoundProvider.notifier).pickImageFromCamera(), + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + child: const Icon(Icons.camera_alt), + ), + const Text("OR"), + ElevatedButton( + onPressed: () => ref.watch(lostAndFoundProvider.notifier).pickImageFromGallery(), + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + child: const Icon(Icons.photo), + ), + ], + ); + } else { + return GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (_) => Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Remove image?"), + const SizedBox(height: 20), + BorderlessButton( + onPressed: () { + ref.read(lostAndFoundProvider.notifier).resetImageSelection(); + Navigator.pop(context); + }, + backgroundColor: Colors.redAccent.shade100.withOpacity(0.5), + splashColor: Colors.red.shade700, + label: const Text('Remove Image'), + ), + ], + ), + ), + ); + }, + child: SizedBox( + width: 200, + height: 150, + child: Image.file(ref.watch(lostAndFoundProvider).selectedImage!), + ), + ); + } + }, + ), + const SizedBox(height: 20), + Form( + key: _formKey, + child: Column( + children: [ + MaterialTextFormField( + hintText: "Item Name", + controller: ref.read(lostAndFoundProvider).itemNameController, + validator: (value) => Validators.nameValidator(value), + ), + const SizedBox(height: 20), + MaterialTextFormField( + hintText: "Item Description", + controller: ref.read(lostAndFoundProvider).itemDescriptionController, + validator: (value) => Validators.descriptionValidator(value), + ), + const SizedBox(height: 20), + MaterialTextFormField( + hintText: "Contact Number", + controller: ref.read(lostAndFoundProvider).contactNumberController, + validator: (value) => Validators.contactNumberValidator(value), + ), + const SizedBox(height: 20), + MaterialTextFormField( + hintText: "Last seen at location", + controller: ref.read(lostAndFoundProvider).lastSeenLocationController, + validator: (value) => Validators.nonEmptyValidator(value), + ), + ], + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerRight, + child: Consumer( + builder: (_, ref, __) { + return AnimatedToggleSwitch.dual( + height: 45, + spacing: 20, + current: ref.watch(lostAndFoundProvider).listingStatus, + first: LostAndFoundConstants.lostState, + second: LostAndFoundConstants.foundState, + textBuilder: (value) => Text(value), + onChanged: (value) => + ref.read(lostAndFoundProvider.notifier).updateListingStatus(value), + styleBuilder: (value) => value == LostAndFoundConstants.lostState + ? ToggleStyle( + indicatorColor: Colors.redAccent, + backgroundColor: Colors.redAccent.shade100, + borderColor: Colors.transparent, + ) + : ToggleStyle( + indicatorColor: Colors.teal, + backgroundColor: Colors.tealAccent.shade100, + borderColor: Colors.transparent, + ), + iconBuilder: (value) => value == LostAndFoundConstants.lostState + ? const Icon( + Icons.search_outlined, + color: Colors.white, + ) + : const Icon( + Icons.check, + color: Colors.white, + ), + ); + }, + ), + ), + const SizedBox(height: 20), + Row( + children: [ + BorderlessButton( + onPressed: () => context.pop(), + backgroundColor: Colors.redAccent.shade100.withOpacity(0.5), + splashColor: Colors.red.shade700, + label: const Text('Cancel'), + ), + const Spacer(), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + ref.read(lostAndFoundProvider.notifier).addItem(); + ref.read(lostAndFoundProvider.notifier).clearControllers(); + context.pop(); + } + }, + child: const Text('Add'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + label: const Text('Add Listing'), + ), + const SizedBox(width: 20), + ], + ), + body: ref.watch(lostAndFoundProvider).loadingState == LoadingState.success + ? (ref.read(lostAndFoundProvider).lostAndFoundItemList.isNotEmpty + ? GridView.count( + crossAxisCount: 2, + childAspectRatio: 0.8, + children: [ + for (var item in ref.watch(lostAndFoundProvider).lostAndFoundItemList) + ImageTile( + image: item.imagePath != null + ? ref.read(lostAndFoundProvider.notifier).imageFromBase64String(item.imagePath!) + : null, + body: [ + Text( + "Item : ${item.name}", + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + ), + ), + Text( + "Last Seen at : ${item.lastSeenLocation}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + ), + ), + Text( + item.isLost ? " Status : Lost" : "Status : Found", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + ), + ), + ], + primaryColor: item.isLost ? Colors.redAccent.shade100 : Colors.tealAccent.shade100, + secondaryColor: item.isLost ? Colors.redAccent.shade200 : Colors.tealAccent.shade200, + onTap: () => showDialog( + context: context, + builder: (_) => Dialog( + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Item name : ${item.name}", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20), + ), + const SizedBox(height: 20), + Text( + "Item description : \n${item.description}", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + Text( + "Last seen at : ${item.lastSeenLocation}", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + item.contactNumber, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(width: 10), + IconButton( + onPressed: () => ref + .read(lostAndFoundProvider.notifier) + .launchCaller(item.contactNumber), + icon: const Icon(Icons.call, color: Colors.green)), + ], + ), + const SizedBox(height: 20), + BorderlessButton( + onPressed: () => context.pop(), + backgroundColor: Colors.redAccent.shade100.withOpacity(0.5), + splashColor: Colors.red.shade700, + label: const Text('Close'), + ), + ], + ), + ), + ), + ), + ), + ], + ) + : const Center( + child: Text("No Listings"), + )) + : const Center(child: CircularProgressIndicator()), + ), + ); + } +} diff --git a/frontend/lib/screens/room_vacancy.dart b/frontend/lib/screens/room_vacancy.dart new file mode 100644 index 0000000..05c4b06 --- /dev/null +++ b/frontend/lib/screens/room_vacancy.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:responsive_framework/responsive_framework.dart'; +import 'package:smart_insti_app/components/borderless_button.dart'; +import 'package:smart_insti_app/components/menu_tile.dart'; +import 'package:smart_insti_app/constants/constants.dart'; +import 'package:smart_insti_app/provider/room_provider.dart'; +import '../models/room.dart'; + +class RoomVacancy extends ConsumerWidget { + const RoomVacancy({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (ref.read(roomProvider).loadingState == LoadingState.progress) ref.read(roomProvider.notifier).loadRooms(); + return ResponsiveScaledBox( + width: 411, + child: Scaffold( + appBar: AppBar( + title: const Text('Room Vacancy'), + ), + body: ref.watch(roomProvider).loadingState == LoadingState.success + ? ref.read(roomProvider).roomList.isNotEmpty + ? GridView.count( + crossAxisCount: 2, + children: [ + for (Room room in ref.read(roomProvider).roomList) + MenuTile( + title: room.name, + onTap: () => showDialog( + context: context, + builder: (_) => Dialog( + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${room.name} : ${room.vacant ? 'Vacant' : 'Occupied'}", + style: const TextStyle(fontSize: 20)), + const SizedBox(height: 20), + room.vacant + ? Row( + children: [ + BorderlessButton( + onPressed: () => context.pop(), + label: const Text('Cancel'), + backgroundColor: Colors.red.shade100, + splashColor: Colors.redAccent, + ), + const Spacer(), + BorderlessButton( + onPressed: () { + ref.read(roomProvider.notifier).reserveRoom(room); + context.pop(); + }, + label: const Text('Reserve'), + backgroundColor: Colors.blue.shade100, + splashColor: Colors.blueAccent, + ), + ], + ) + : Container(), + ], + ), + ), + ), + ), + body: [ + const SizedBox(height: 5), + Text( + room.vacant ? 'Vacant' : 'Occupied', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 10), + room.vacant ? Container() : const Text("by : Aadarsh"), + ], + icon: Icons.class_, + primaryColor: room.vacant ? Colors.greenAccent.shade100 : Colors.redAccent.shade100, + secondaryColor: room.vacant ? Colors.greenAccent.shade200 : Colors.redAccent.shade200, + ), + ], + ) + : const Center(child: Text('No rooms found')) + : const Center(child: CircularProgressIndicator()), + ), + ); + } +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 499dd29..a73f18a 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.1" + animated_toggle_switch: + dependency: "direct main" + description: + name: animated_toggle_switch + sha256: "19afc84373ad0fca147869e1b984afb3778ea8080f8d0af4fd1ff604d41b2635" + url: "https://pub.dev" + source: hosted + version: "0.8.0" args: dependency: transitive description: @@ -137,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + url: "https://pub.dev" + source: hosted + version: "0.3.3+8" crypto: dependency: transitive description: @@ -209,6 +225,38 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" flutter: dependency: "direct main" description: flutter @@ -222,6 +270,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.4.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_keyboard_visibility: dependency: transitive description: @@ -416,6 +472,94 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: f4bad5ed2dfff5a7ce0dfbad545b46a945c702bb6182a921488ef01ba7693111 + url: "https://pub.dev" + source: hosted + version: "5.0.1" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: "865d798b5c9d826f1185b32e5d0018c4183ddb77b7b82a931e1a06aa3b74974e" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e + url: "https://pub.dev" + source: hosted + version: "5.0.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + url: "https://pub.dev" + source: hosted + version: "0.8.9+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + url: "https://pub.dev" + source: hosted + version: "0.8.9+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + url: "https://pub.dev" + source: hosted + version: "2.9.3" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: @@ -496,6 +640,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" package_config: dependency: transitive description: @@ -765,6 +917,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" vector_math: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 786c29d..7592272 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -53,6 +53,11 @@ dependencies: animated_toggle_switch: ^0.8.0 dio: ^5.4.0 animated_text_kit: ^4.2.2 + image_picker: ^1.0.7 + image_cropper: ^5.0.1 + animated_toggle_switch: ^0.8.0 + url_launcher: ^6.2.4 + flutter_dotenv: ^5.1.0 dev_dependencies: flutter_test: @@ -79,6 +84,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: + - .env - lib/assets/ # An image asset can refer to one or more resolution-specific "variants", see