Add Dialog with Pageview to select new house, and then drop a pin where the house is, then zoom map to that area.

This commit is contained in:
2025-04-20 20:36:13 -07:00
parent 9170fffdf8
commit dd9d1a67ab
19 changed files with 1581 additions and 6 deletions

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:wheres_my_sign/common/constants.dart';
class CustomDialogBox extends StatefulWidget {
final String title, descriptions, text;
final Image? img;
const CustomDialogBox({
super.key,
required this.title,
required this.descriptions,
required this.text,
this.img,
});
@override
CustomDialogBoxState createState() => CustomDialogBoxState();
}
class CustomDialogBoxState extends State<CustomDialogBox> {
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Constants.padding),
),
elevation: 0,
backgroundColor: Colors.transparent,
child: contentBox(context),
);
}
contentBox(context) {
return Stack(
children: <Widget>[
Container(
padding: EdgeInsets.only(
left: Constants.padding,
top: Constants.avatarRadius + Constants.padding,
right: Constants.padding,
bottom: Constants.padding,
),
margin: EdgeInsets.only(top: Constants.avatarRadius),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: Colors.white,
borderRadius: BorderRadius.circular(Constants.padding),
boxShadow: [
BoxShadow(
color: Colors.black,
offset: Offset(0, 10),
blurRadius: 10,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
widget.title,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
),
SizedBox(height: 15),
Text(
widget.descriptions,
style: TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
SizedBox(height: 22),
Align(
alignment: Alignment.bottomRight,
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(widget.text, style: TextStyle(fontSize: 18)),
),
),
],
),
),
Positioned(
left: Constants.padding,
right: Constants.padding,
child: CircleAvatar(
backgroundColor: Colors.transparent,
radius: Constants.avatarRadius,
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(Constants.avatarRadius),
),
child: Image.asset(
"assets/icons/house.png",
width: Constants.avatarRadius * 2,
height: Constants.avatarRadius * 2,
fit: BoxFit.cover,
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,336 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart' as gmaps;
import 'package:flutter_google_places_sdk/flutter_google_places_sdk.dart'
as places;
import 'package:wheres_my_sign/common/constants.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'dart:async';
import 'package:wheres_my_sign/models/property.dart';
class CustomDialogBox extends StatefulWidget {
final String title, descriptions, text;
final Image? img;
const CustomDialogBox({
super.key,
required this.title,
required this.descriptions,
required this.text,
this.img,
});
@override
CustomDialogBoxState createState() => CustomDialogBoxState();
}
class CustomDialogBoxState extends State<CustomDialogBox> {
final PageController _pageController = PageController();
int _currentPage = 0;
final _addressController = TextEditingController();
final _signsController = TextEditingController();
String? _selectedRadius;
final _addressFocusNode = FocusNode();
Timer? _debouncer;
List<places.AutocompletePrediction> _predictions = [];
final places.FlutterGooglePlacesSdk _places = places.FlutterGooglePlacesSdk(
'AIzaSyBLSUk32a5qGm3M_n9Yii66I7wi0rmA8oM',
);
late Property property; // Declare property to store final details
@override
void dispose() {
_addressController.dispose();
_signsController.dispose();
_debouncer?.cancel();
super.dispose();
}
void _onAddressChanged(String value) {
if (_debouncer?.isActive ?? false) _debouncer!.cancel();
_debouncer = Timer(const Duration(milliseconds: 300), () async {
if (value.isNotEmpty) {
final result = await _places.findAutocompletePredictions(value);
setState(() => _predictions = result.predictions);
} else {
setState(() => _predictions = []);
}
});
}
void _selectPrediction(places.AutocompletePrediction prediction) async {
// Fetch place details
final placeDetails = await _places.fetchPlace(
prediction.placeId,
fields: [
places.PlaceField.Location,
places.PlaceField.AddressComponents,
places.PlaceField.Name,
],
);
final place = placeDetails.place;
// Null checks for place and latLng
if (place != null && place.latLng != null) {
final latLng =
place
.latLng!; // Use the null check operator, since we've already verified it's not null
setState(() {
_addressController.text = prediction.fullText;
_predictions = [];
property = Property(
latitude: latLng.lat,
longitude: latLng.lng,
address:
place.name ?? 'Unknown', // Default to 'Unknown' if name is null
signLocations: [],
);
});
}
FocusScope.of(context).unfocus(); // Close keyboard
}
void _nextPage() {
if (_currentPage < 3) {
setState(() => _currentPage++);
_pageController.animateToPage(
_currentPage,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _previousPage() {
if (_currentPage > 0) {
setState(() => _currentPage--);
_pageController.animateToPage(
_currentPage,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
Widget _buildNavigationButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (_currentPage > 0)
TextButton(onPressed: _previousPage, child: const Text("Previous")),
const Spacer(),
if (_currentPage < 3)
TextButton(onPressed: _nextPage, child: const Text("Next"))
else
TextButton(
onPressed: () {
Navigator.of(context).pop(property);
},
child: const Text("Show"),
),
],
);
}
Widget _buildPageContent() {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.25,
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
// Page 1
Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Let's show a property. Over the next few pages, we'll ask for the address, how many signs you want to place, and the radius you'd like to reach. The final page will confirm your input and show the best locations for your open house signs. Ready? Let's Begin!",
textAlign: TextAlign.justify,
),
const SizedBox(height: 20),
const Spacer(),
_buildNavigationButtons(),
],
),
// Page 2 - Address with Autocomplete
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _addressController,
focusNode: _addressFocusNode,
onChanged: _onAddressChanged,
decoration: const InputDecoration(
labelText: 'Property Address',
),
),
if (_predictions.isNotEmpty)
Container(
height: 150,
child: ListView.builder(
itemCount: _predictions.length,
itemBuilder: (context, index) {
final p = _predictions[index];
return ListTile(
title: Text(p.fullText),
onTap: () => _selectPrediction(p),
);
},
),
),
const SizedBox(height: 20),
Spacer(),
_buildNavigationButtons(),
],
),
// Page 3
Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _signsController,
decoration: const InputDecoration(labelText: 'Number of Signs'),
keyboardType: TextInputType.number,
),
const SizedBox(height: 10),
DropdownButtonFormField<String>(
value: _selectedRadius,
decoration: const InputDecoration(labelText: 'Search Radius'),
items:
['1 mile', '2 miles', '5 miles']
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) => setState(() => _selectedRadius = val),
),
const SizedBox(height: 20),
const Spacer(),
_buildNavigationButtons(),
],
),
// Page 4 - Summary
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Summary",
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text("Address: ${_addressController.text}"),
Text("Signs: ${_signsController.text}"),
Text("Radius: ${_selectedRadius ?? 'Not selected'}"),
const SizedBox(height: 20),
const Spacer(),
_buildNavigationButtons(),
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Constants.padding),
),
elevation: 0,
backgroundColor: Colors.transparent,
child: contentBox(context),
);
}
Widget contentBox(BuildContext context) {
return Stack(
children: <Widget>[
Container(
padding: EdgeInsets.only(
left: Constants.padding,
top: Constants.avatarRadius + Constants.padding,
right: Constants.padding,
bottom: Constants.padding,
),
margin: EdgeInsets.only(top: Constants.avatarRadius),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: Colors.white,
borderRadius: BorderRadius.circular(Constants.padding),
boxShadow: const [
BoxShadow(
color: Colors.black,
offset: Offset(0, 10),
blurRadius: 10,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
widget.title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 15),
Text(
widget.descriptions,
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
SmoothPageIndicator(
controller: _pageController,
count: 4,
effect: WormEffect(
dotHeight: 10,
dotWidth: 10,
spacing: 8,
activeDotColor: Theme.of(context).primaryColor,
dotColor: Colors.grey.shade300,
),
),
const SizedBox(height: 15),
_buildPageContent(),
],
),
),
Positioned(
left: Constants.padding,
right: Constants.padding,
child: CircleAvatar(
backgroundColor: Colors.transparent,
radius: Constants.avatarRadius,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(Constants.avatarRadius),
),
child:
widget.img ??
Image.asset(
"assets/icons/house.png",
width: Constants.avatarRadius * 2,
height: Constants.avatarRadius * 2,
fit: BoxFit.cover,
),
),
),
),
],
);
}
}