Compare commits
2 Commits
405bbc8d4c
...
78b1b1f078
Author | SHA1 | Date | |
---|---|---|---|
|
78b1b1f078 | ||
|
0e4c5d96cc |
@ -1,6 +1,7 @@
|
|||||||
using API.Models;
|
using API.Models;
|
||||||
using API.Persistence.Repositories;
|
using API.Persistence.Repositories;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace API.Application.Users.Commands
|
namespace API.Application.Users.Commands
|
||||||
{
|
{
|
||||||
@ -13,25 +14,30 @@ namespace API.Application.Users.Commands
|
|||||||
_repository = repository;
|
_repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Handle(UserDTO userDTO)
|
public async Task<IActionResult> Handle(UpdateUserDTO UpdateUserDTO)
|
||||||
{
|
{
|
||||||
List<User> existingUsers = await _repository.QueryAllUsersAsync();
|
List<User> existingUsers = await _repository.QueryAllUsersAsync();
|
||||||
User currentUser = await _repository.QueryUserByIdAsync(userDTO.Id);
|
User currentUser = await _repository.QueryUserByIdAsync(UpdateUserDTO.Id);
|
||||||
|
|
||||||
foreach (User existingUser in existingUsers)
|
foreach (User existingUser in existingUsers)
|
||||||
{
|
{
|
||||||
if (existingUser.Username == userDTO.Username && existingUser.Username != currentUser.Username)
|
if (existingUser.Username == UpdateUserDTO.Username && existingUser.Username != currentUser.Username)
|
||||||
{
|
{
|
||||||
return new ConflictObjectResult(new { message = "Username is already in use." });
|
return new ConflictObjectResult(new { message = "Username is already in use." });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.Email == userDTO.Email && existingUser.Email != currentUser.Email)
|
if (existingUser.Email == UpdateUserDTO.Email && existingUser.Email != currentUser.Email)
|
||||||
{
|
{
|
||||||
return new ConflictObjectResult(new { message = "Email is already in use." });
|
return new ConflictObjectResult(new { message = "Email is already in use." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentUser.Username = userDTO.Username;
|
|
||||||
currentUser.Email = userDTO.Email;
|
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(UpdateUserDTO.Password);
|
||||||
|
|
||||||
|
|
||||||
|
currentUser.Username = UpdateUserDTO.Username;
|
||||||
|
currentUser.Email = UpdateUserDTO.Email;
|
||||||
|
currentUser.HashedPassword = hashedPassword;
|
||||||
|
|
||||||
bool success = await _repository.UpdateUserAsync(currentUser);
|
bool success = await _repository.UpdateUserAsync(currentUser);
|
||||||
if (success)
|
if (success)
|
||||||
@ -40,5 +46,19 @@ namespace API.Application.Users.Commands
|
|||||||
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private bool IsPasswordSecure(string password)
|
||||||
|
{
|
||||||
|
var hasUpperCase = new Regex(@"[A-Z]+");
|
||||||
|
var hasLowerCase = new Regex(@"[a-z]+");
|
||||||
|
var hasDigits = new Regex(@"[0-9]+");
|
||||||
|
var hasSpecialChar = new Regex(@"[\W_]+");
|
||||||
|
var hasMinimum8Chars = new Regex(@".{8,}");
|
||||||
|
|
||||||
|
return hasUpperCase.IsMatch(password)
|
||||||
|
&& hasLowerCase.IsMatch(password)
|
||||||
|
&& hasDigits.IsMatch(password)
|
||||||
|
&& hasSpecialChar.IsMatch(password)
|
||||||
|
&& hasMinimum8Chars.IsMatch(password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
using API.Models;
|
|
||||||
using API.Persistence.Repositories;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace API.Application.Users.Commands
|
|
||||||
{
|
|
||||||
public class UpdateUserPassword
|
|
||||||
{
|
|
||||||
private readonly IUserRepository _repository;
|
|
||||||
|
|
||||||
public UpdateUserPassword(IUserRepository repository)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> Handle(ChangePasswordDTO changePasswordDTO)
|
|
||||||
{
|
|
||||||
if (!IsPasswordSecure(changePasswordDTO.NewPassword))
|
|
||||||
{
|
|
||||||
return new ConflictObjectResult(new { message = "New Password is not secure." });
|
|
||||||
}
|
|
||||||
|
|
||||||
User currentUser = await _repository.QueryUserByIdAsync(changePasswordDTO.Id);
|
|
||||||
if (currentUser == null || !BCrypt.Net.BCrypt.Verify(changePasswordDTO.OldPassword, currentUser.HashedPassword))
|
|
||||||
{
|
|
||||||
return new UnauthorizedObjectResult(new { message = "Old Password is incorrect" });
|
|
||||||
}
|
|
||||||
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(changePasswordDTO.NewPassword);
|
|
||||||
|
|
||||||
currentUser.HashedPassword = hashedPassword;
|
|
||||||
|
|
||||||
bool success = await _repository.UpdateUserPasswordAsync(currentUser);
|
|
||||||
if (success)
|
|
||||||
return new OkResult();
|
|
||||||
else
|
|
||||||
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPasswordSecure(string password)
|
|
||||||
{
|
|
||||||
var hasUpperCase = new Regex(@"[A-Z]+");
|
|
||||||
var hasLowerCase = new Regex(@"[a-z]+");
|
|
||||||
var hasDigits = new Regex(@"[0-9]+");
|
|
||||||
var hasSpecialChar = new Regex(@"[\W_]+");
|
|
||||||
var hasMinimum8Chars = new Regex(@".{8,}");
|
|
||||||
|
|
||||||
return hasUpperCase.IsMatch(password)
|
|
||||||
&& hasLowerCase.IsMatch(password)
|
|
||||||
&& hasDigits.IsMatch(password)
|
|
||||||
&& hasSpecialChar.IsMatch(password)
|
|
||||||
&& hasMinimum8Chars.IsMatch(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,7 +20,6 @@ namespace API.Controllers
|
|||||||
private readonly QueryUserById _queryUserById;
|
private readonly QueryUserById _queryUserById;
|
||||||
private readonly CreateUser _createUser;
|
private readonly CreateUser _createUser;
|
||||||
private readonly UpdateUser _updateUser;
|
private readonly UpdateUser _updateUser;
|
||||||
private readonly UpdateUserPassword _updateUserPassword;
|
|
||||||
private readonly DeleteUser _deleteUser;
|
private readonly DeleteUser _deleteUser;
|
||||||
private readonly LoginUser _loginUser;
|
private readonly LoginUser _loginUser;
|
||||||
|
|
||||||
@ -29,7 +28,6 @@ namespace API.Controllers
|
|||||||
QueryUserById queryUserById,
|
QueryUserById queryUserById,
|
||||||
CreateUser createUser,
|
CreateUser createUser,
|
||||||
UpdateUser updateUser,
|
UpdateUser updateUser,
|
||||||
UpdateUserPassword updateUserPassword,
|
|
||||||
DeleteUser deleteUser,
|
DeleteUser deleteUser,
|
||||||
LoginUser loginUser)
|
LoginUser loginUser)
|
||||||
{
|
{
|
||||||
@ -37,7 +35,6 @@ namespace API.Controllers
|
|||||||
_queryUserById = queryUserById;
|
_queryUserById = queryUserById;
|
||||||
_createUser = createUser;
|
_createUser = createUser;
|
||||||
_updateUser = updateUser;
|
_updateUser = updateUser;
|
||||||
_updateUserPassword = updateUserPassword;
|
|
||||||
_deleteUser = deleteUser;
|
_deleteUser = deleteUser;
|
||||||
_loginUser = loginUser;
|
_loginUser = loginUser;
|
||||||
}
|
}
|
||||||
@ -65,16 +62,9 @@ namespace API.Controllers
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<IActionResult> PutUser(UserDTO userDTO)
|
public async Task<IActionResult> PutUser(UpdateUserDTO UpdateUserDTO)
|
||||||
{
|
{
|
||||||
return await _updateUser.Handle(userDTO);
|
return await _updateUser.Handle(UpdateUserDTO);
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpPut("password")]
|
|
||||||
public async Task<IActionResult> PutUserPassword(ChangePasswordDTO changePasswordDTO)
|
|
||||||
{
|
|
||||||
return await _updateUserPassword.Handle(changePasswordDTO);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
@ -6,7 +6,6 @@ public class User : BaseModel
|
|||||||
{
|
{
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
public string? Password { get; set; }
|
|
||||||
public string HashedPassword { get; set; }
|
public string HashedPassword { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,10 +29,11 @@ public class SignUpDTO
|
|||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChangePasswordDTO
|
public class UpdateUserDTO
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string OldPassword { get; set; }
|
public string Email { get; set; }
|
||||||
public string NewPassword { get; set; }
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,5 @@ namespace API.Persistence.Repositories
|
|||||||
Task<User> QueryUserByIdAsync(string id);
|
Task<User> QueryUserByIdAsync(string id);
|
||||||
Task<User> QueryUserByEmailAsync(string email);
|
Task<User> QueryUserByEmailAsync(string email);
|
||||||
Task<bool> UpdateUserAsync(User user);
|
Task<bool> UpdateUserAsync(User user);
|
||||||
Task<bool> UpdateUserPasswordAsync(User user);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -57,21 +57,6 @@ namespace API.Persistence.Repositories
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateUserPasswordAsync(User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_context.Entry(user).State = EntityState.Modified;
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteUserAsync(string id)
|
public async Task<bool> DeleteUserAsync(string id)
|
||||||
{
|
{
|
||||||
var user = await _context.Users.FindAsync(id);
|
var user = await _context.Users.FindAsync(id);
|
||||||
|
@ -36,7 +36,6 @@ namespace API
|
|||||||
builder.Services.AddScoped<QueryUserById>();
|
builder.Services.AddScoped<QueryUserById>();
|
||||||
builder.Services.AddScoped<CreateUser>();
|
builder.Services.AddScoped<CreateUser>();
|
||||||
builder.Services.AddScoped<UpdateUser>();
|
builder.Services.AddScoped<UpdateUser>();
|
||||||
builder.Services.AddScoped<UpdateUserPassword>();
|
|
||||||
builder.Services.AddScoped<DeleteUser>();
|
builder.Services.AddScoped<DeleteUser>();
|
||||||
builder.Services.AddScoped<LoginUser>();
|
builder.Services.AddScoped<LoginUser>();
|
||||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mobile/models.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
@ -67,7 +68,9 @@ Future<bool> isLoggedIn(BuildContext context) async {
|
|||||||
|
|
||||||
final token = prefs.getString('token');
|
final token = prefs.getString('token');
|
||||||
if (token == null){
|
if (token == null){
|
||||||
|
prefs.remove('id');
|
||||||
loggedIn = false;
|
loggedIn = false;
|
||||||
|
user = User as User?;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,10 @@ class _SideMenuState extends State<SideMenu> {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
prefs.remove('token');
|
prefs.remove('token');
|
||||||
|
prefs.remove('id');
|
||||||
setState(() {
|
setState(() {
|
||||||
loggedIn = false;
|
loggedIn = false;
|
||||||
|
user = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
187
Mobile/lib/editprofile.dart
Normal file
187
Mobile/lib/editprofile.dart
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'dart:developer' as useMAN;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mobile/models.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'api.dart' as api;
|
||||||
|
import 'base/variables.dart';
|
||||||
|
|
||||||
|
class EditProfilePage extends StatefulWidget {
|
||||||
|
final User? userData;
|
||||||
|
|
||||||
|
const EditProfilePage({super.key, required this.userData});
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditProfilePage> createState() => _ProfilePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfilePageState extends State<EditProfilePage> {
|
||||||
|
TextEditingController usernameInput = TextEditingController();
|
||||||
|
TextEditingController emailInput = TextEditingController();
|
||||||
|
TextEditingController passwordInput = TextEditingController();
|
||||||
|
TextEditingController confirmPasswordInput = TextEditingController();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Initialize the controllers with existing data
|
||||||
|
usernameInput.text = widget.userData!.username;
|
||||||
|
emailInput.text = widget.userData!.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Dispose of the controllers when the widget is disposed
|
||||||
|
usernameInput.dispose();
|
||||||
|
emailInput.dispose();
|
||||||
|
passwordInput.dispose();
|
||||||
|
confirmPasswordInput.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveProfile() async {
|
||||||
|
if (passwordInput.text != confirmPasswordInput.text) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Passwords do not match')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String? id = prefs.getString('id');
|
||||||
|
|
||||||
|
final response = await api
|
||||||
|
.request(context, api.ApiService.auth, 'PUT', '/api/users', {
|
||||||
|
'id' : id,
|
||||||
|
'username': usernameInput.text,
|
||||||
|
'email': emailInput.text,
|
||||||
|
'password': passwordInput.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
useMAN.log('data');
|
||||||
|
|
||||||
|
|
||||||
|
if (response!.isEmpty) {
|
||||||
|
prefs.remove('token');
|
||||||
|
loggedIn = true;
|
||||||
|
user = User(
|
||||||
|
id!,
|
||||||
|
emailInput.text,
|
||||||
|
usernameInput.text,
|
||||||
|
DateTime.now(),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(); // Close the dialog
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Something went wrong! Please contact an admin.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteProfile(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Confirm Deletion'),
|
||||||
|
content: Text('Are you sure you want to delete your profile?'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: Text('Cancel'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(); // Close the dialog
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Text('Delete'),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
onPressed: () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String? id = prefs.getString('id');
|
||||||
|
|
||||||
|
final response = await api
|
||||||
|
.request(context, api.ApiService.auth, 'DELETE', '/api/users/$id', null);
|
||||||
|
|
||||||
|
if (response!.isEmpty) {
|
||||||
|
prefs.remove('token');
|
||||||
|
prefs.remove('id');
|
||||||
|
setState(() {
|
||||||
|
loggedIn = false;
|
||||||
|
user = null;
|
||||||
|
});
|
||||||
|
Navigator.of(context).pop(); // Close the dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.pushReplacementNamed(context, '/register');
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Something went wrong! Please contact an admin.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Edit Profile'),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context); // Navigates back when the back button is pressed
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: usernameInput,
|
||||||
|
decoration: InputDecoration(labelText: 'Name'),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: emailInput,
|
||||||
|
decoration: InputDecoration(labelText: 'Email'),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: passwordInput,
|
||||||
|
decoration: InputDecoration(labelText: 'New password'),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: confirmPasswordInput,
|
||||||
|
decoration: InputDecoration(labelText: 'Repeat new password'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _saveProfile, // Save and pop
|
||||||
|
child: Text('Save'),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => _deleteProfile(context),
|
||||||
|
child: Text('Delete'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red, // Red text
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:mobile/models.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'base/sidemenu.dart';
|
import 'base/sidemenu.dart';
|
||||||
import 'api.dart' as api;
|
import 'api.dart' as api;
|
||||||
|
import 'editprofile.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
const ProfilePage({super.key});
|
const ProfilePage({super.key});
|
||||||
@ -105,7 +106,16 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Add your edit action here
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.9, // 90% height
|
||||||
|
child: EditProfilePage(userData: user),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Edit'),
|
child: const Text('Edit'),
|
||||||
),
|
),
|
||||||
|
@ -42,7 +42,7 @@ Widget build(BuildContext context) {
|
|||||||
selectedIndex: 3,
|
selectedIndex: 3,
|
||||||
body: Scaffold(
|
body: Scaffold(
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Center( // Added SingleChildScrollView here
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(minWidth: 100, maxWidth: 400),
|
constraints: const BoxConstraints(minWidth: 100, maxWidth: 400),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
Loading…
Reference in New Issue
Block a user