From a46be5e65b476169f0c15ad672f0d0a82f3358e1 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 10 Jun 2025 15:12:45 +0000 Subject: [PATCH 1/4] Create TypeScript types for User API cgen-405824ba7f5949edbb61c5a0a727f6f3 --- src/crm/components/CustomersTable.tsx | 346 ++++++++++++++++++++++++ src/crm/components/EditUserModal.tsx | 373 ++++++++++++++++++++++++++ src/crm/hooks/useUsers.ts | 150 +++++++++++ src/crm/pages/Customers.tsx | 137 +++++++++- src/crm/types/user.ts | 91 +++++++ 5 files changed, 1090 insertions(+), 7 deletions(-) create mode 100644 src/crm/components/CustomersTable.tsx create mode 100644 src/crm/components/EditUserModal.tsx create mode 100644 src/crm/hooks/useUsers.ts create mode 100644 src/crm/types/user.ts diff --git a/src/crm/components/CustomersTable.tsx b/src/crm/components/CustomersTable.tsx new file mode 100644 index 0000000..d4bc260 --- /dev/null +++ b/src/crm/components/CustomersTable.tsx @@ -0,0 +1,346 @@ +import * as React from "react"; +import { + Box, + Card, + CardContent, + Typography, + TextField, + InputAdornment, + IconButton, + Avatar, + Chip, + Stack, + Button, + Alert, +} from "@mui/material"; +import { + DataGrid, + GridColDef, + GridPaginationModel, + GridSortModel, + GridActionsCellItem, + GridRowParams, +} from "@mui/x-data-grid"; +import SearchRoundedIcon from "@mui/icons-material/SearchRounded"; +import EditRoundedIcon from "@mui/icons-material/EditRounded"; +import DeleteRoundedIcon from "@mui/icons-material/DeleteRounded"; +import PersonRoundedIcon from "@mui/icons-material/PersonRounded"; +import { User } from "../types/user"; + +interface CustomersTableProps { + users: User[]; + loading: boolean; + error: string | null; + total: number; + page: number; + perPage: number; + onPageChange: (page: number) => void; + onPageSizeChange: (pageSize: number) => void; + onSearch: (query: string) => void; + onSort: (field: string) => void; + onEdit: (user: User) => void; + onDelete: (userId: string) => void; +} + +export default function CustomersTable({ + users, + loading, + error, + total, + page, + perPage, + onPageChange, + onPageSizeChange, + onSearch, + onSort, + onEdit, + onDelete, +}: CustomersTableProps) { + const [searchQuery, setSearchQuery] = React.useState(""); + const [deleteConfirm, setDeleteConfirm] = React.useState(null); + + const handleSearchChange = (event: React.ChangeEvent) => { + const query = event.target.value; + setSearchQuery(query); + + // Debounce search + const timeoutId = setTimeout(() => { + onSearch(query); + }, 500); + + return () => clearTimeout(timeoutId); + }; + + const handlePaginationChange = (model: GridPaginationModel) => { + if (model.page !== page - 1) { + onPageChange(model.page + 1); + } + if (model.pageSize !== perPage) { + onPageSizeChange(model.pageSize); + } + }; + + const handleSortChange = (model: GridSortModel) => { + if (model.length > 0) { + const sort = model[0]; + const sortField = + sort.field === "fullName" + ? "name.first" + : sort.field === "location" + ? "location.city" + : sort.field === "age" + ? "dob.age" + : sort.field; + onSort(sortField); + } + }; + + const handleDeleteClick = (userId: string) => { + if (deleteConfirm === userId) { + onDelete(userId); + setDeleteConfirm(null); + } else { + setDeleteConfirm(userId); + // Auto-cancel after 3 seconds + setTimeout(() => setDeleteConfirm(null), 3000); + } + }; + + const getGenderColor = ( + gender: string, + ): "default" | "primary" | "secondary" => { + switch (gender.toLowerCase()) { + case "male": + return "primary"; + case "female": + return "secondary"; + default: + return "default"; + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + }; + + const columns: GridColDef[] = [ + { + field: "avatar", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => ( + + + + ), + }, + { + field: "fullName", + headerName: "Name", + flex: 1, + minWidth: 150, + valueGetter: (value, row) => `${row.name.first} ${row.name.last}`, + renderCell: (params) => ( + + + {params.row.name.title} {params.row.name.first}{" "} + {params.row.name.last} + + + @{params.row.login.username} + + + ), + }, + { + field: "email", + headerName: "Email", + flex: 1, + minWidth: 200, + renderCell: (params) => ( + {params.value} + ), + }, + { + field: "location", + headerName: "Location", + flex: 1, + minWidth: 150, + valueGetter: (value, row) => + `${row.location.city}, ${row.location.country}`, + renderCell: (params) => ( + + {params.row.location.city} + + {params.row.location.country} + + + ), + }, + { + field: "phone", + headerName: "Phone", + width: 130, + renderCell: (params) => ( + + {params.value} + + ), + }, + { + field: "gender", + headerName: "Gender", + width: 100, + renderCell: (params) => ( + + ), + }, + { + field: "age", + headerName: "Age", + width: 80, + valueGetter: (value, row) => row.dob.age, + renderCell: (params) => ( + {params.value} + ), + }, + { + field: "registered", + headerName: "Registered", + width: 120, + valueGetter: (value, row) => row.registered.date, + renderCell: (params) => ( + + {formatDate(params.value)} + + ), + }, + { + field: "actions", + type: "actions", + headerName: "Actions", + width: 100, + getActions: (params: GridRowParams) => [ + } + label="Edit" + onClick={() => onEdit(params.row)} + color="primary" + />, + } + label={ + deleteConfirm === params.row.login.uuid + ? "Confirm Delete" + : "Delete" + } + onClick={() => handleDeleteClick(params.row.login.uuid)} + color={deleteConfirm === params.row.login.uuid ? "error" : "default"} + sx={{ + color: + deleteConfirm === params.row.login.uuid + ? "error.main" + : "text.secondary", + }} + />, + ], + }, + ]; + + return ( + + + + + Customers ({total.toLocaleString()}) + + + + + ), + }} + /> + + + {error && ( + + {error} + + )} + + + + row.login.uuid} + disableRowSelectionOnClick + density="comfortable" + sx={{ + border: "none", + "& .MuiDataGrid-cell": { + borderBottom: "1px solid", + borderBottomColor: "divider", + }, + "& .MuiDataGrid-columnHeaders": { + borderBottom: "2px solid", + borderBottomColor: "divider", + backgroundColor: "background.paper", + }, + "& .MuiDataGrid-footerContainer": { + borderTop: "2px solid", + borderTopColor: "divider", + }, + }} + /> + + + ); +} diff --git a/src/crm/components/EditUserModal.tsx b/src/crm/components/EditUserModal.tsx new file mode 100644 index 0000000..756692c --- /dev/null +++ b/src/crm/components/EditUserModal.tsx @@ -0,0 +1,373 @@ +import * as React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Grid, + Box, + Alert, + Avatar, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, +} from "@mui/material"; +import { User, UserUpdateRequest } from "../types/user"; + +interface EditUserModalProps { + open: boolean; + onClose: () => void; + user: User | null; + onSave: (userId: string, updates: UserUpdateRequest) => Promise; + loading: boolean; +} + +export default function EditUserModal({ + open, + onClose, + user, + onSave, + loading, +}: EditUserModalProps) { + const [formData, setFormData] = React.useState({}); + const [error, setError] = React.useState(null); + const [success, setSuccess] = React.useState(false); + + // Reset form when user changes or modal opens + React.useEffect(() => { + if (user && open) { + setFormData({ + email: user.email, + name: { + title: user.name.title, + first: user.name.first, + last: user.name.last, + }, + gender: user.gender, + location: { + city: user.location.city, + state: user.location.state, + country: user.location.country, + postcode: user.location.postcode, + street: { + number: user.location.street.number, + name: user.location.street.name, + }, + }, + phone: user.phone, + cell: user.cell, + }); + setError(null); + setSuccess(false); + } + }, [user, open]); + + const handleInputChange = (field: string, value: any) => { + setFormData((prev) => { + const keys = field.split("."); + const updated = { ...prev }; + + if (keys.length === 1) { + updated[keys[0] as keyof UserUpdateRequest] = value; + } else if (keys.length === 2) { + const [parent, child] = keys; + updated[parent as keyof UserUpdateRequest] = { + ...(updated[parent as keyof UserUpdateRequest] as any), + [child]: value, + }; + } else if (keys.length === 3) { + const [parent, middle, child] = keys; + updated[parent as keyof UserUpdateRequest] = { + ...(updated[parent as keyof UserUpdateRequest] as any), + [middle]: { + ...((updated[parent as keyof UserUpdateRequest] as any)?.[middle] || + {}), + [child]: value, + }, + }; + } + + return updated; + }); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!user) return; + + setError(null); + setSuccess(false); + + try { + const success = await onSave(user.login.uuid, formData); + if (success) { + setSuccess(true); + setTimeout(() => { + onClose(); + }, 1000); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to update user"); + } + }; + + const handleClose = () => { + setError(null); + setSuccess(false); + onClose(); + }; + + if (!user) return null; + + const fullName = `${user.name.first} ${user.name.last}`; + + return ( + + + + + + Edit Customer + + {fullName} • {user.email} + + + + + +
+ + {error && ( + + {error} + + )} + + {success && ( + + Customer updated successfully! + + )} + + + {/* Personal Information */} + + + Personal Information + + + + + + Title + + + + + + + handleInputChange("name.first", e.target.value) + } + required + /> + + + + handleInputChange("name.last", e.target.value)} + required + /> + + + + handleInputChange("email", e.target.value)} + required + /> + + + + + Gender + + + + + {/* Contact Information */} + + + Contact Information + + + + + handleInputChange("phone", e.target.value)} + /> + + + + handleInputChange("cell", e.target.value)} + /> + + + {/* Address Information */} + + + Address Information + + + + + + handleInputChange( + "location.street.number", + parseInt(e.target.value) || 0, + ) + } + /> + + + + + handleInputChange("location.street.name", e.target.value) + } + /> + + + + + handleInputChange("location.city", e.target.value) + } + /> + + + + + handleInputChange("location.state", e.target.value) + } + /> + + + + + handleInputChange("location.country", e.target.value) + } + /> + + + + + handleInputChange("location.postcode", e.target.value) + } + /> + + + + + + + + +
+
+ ); +} diff --git a/src/crm/hooks/useUsers.ts b/src/crm/hooks/useUsers.ts new file mode 100644 index 0000000..0c9881a --- /dev/null +++ b/src/crm/hooks/useUsers.ts @@ -0,0 +1,150 @@ +import { useState, useEffect, useCallback } from "react"; +import { + User, + UsersApiResponse, + UserUpdateRequest, + ApiResponse, +} from "../types/user"; + +const API_BASE_URL = "https://user-api.builder-io.workers.dev/api"; + +export const useUsers = () => { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(10); + + const fetchUsers = useCallback( + async ( + searchQuery?: string, + sortBy?: string, + currentPage?: number, + currentPerPage?: number, + ) => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams({ + page: (currentPage || page).toString(), + perPage: (currentPerPage || perPage).toString(), + }); + + if (searchQuery) { + params.append("search", searchQuery); + } + + if (sortBy) { + params.append("sortBy", sortBy); + } + + const response = await fetch(`${API_BASE_URL}/users?${params}`); + + if (!response.ok) { + throw new Error(`Failed to fetch users: ${response.statusText}`); + } + + const data: UsersApiResponse = await response.json(); + setUsers(data.data); + setTotal(data.total); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch users"); + setUsers([]); + } finally { + setLoading(false); + } + }, + [page, perPage], + ); + + const updateUser = useCallback( + async (userId: string, updates: UserUpdateRequest): Promise => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/users/${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updates), + }); + + if (!response.ok) { + throw new Error(`Failed to update user: ${response.statusText}`); + } + + const result: ApiResponse = await response.json(); + + if (result.success) { + // Refresh the users list after successful update + await fetchUsers(); + return true; + } else { + throw new Error(result.message || "Failed to update user"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to update user"); + return false; + } finally { + setLoading(false); + } + }, + [fetchUsers], + ); + + const deleteUser = useCallback( + async (userId: string): Promise => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/users/${userId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error(`Failed to delete user: ${response.statusText}`); + } + + const result: ApiResponse = await response.json(); + + if (result.success) { + // Refresh the users list after successful deletion + await fetchUsers(); + return true; + } else { + throw new Error(result.message || "Failed to delete user"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to delete user"); + return false; + } finally { + setLoading(false); + } + }, + [fetchUsers], + ); + + // Load initial data + useEffect(() => { + fetchUsers(); + }, []); + + return { + users, + loading, + error, + total, + page, + perPage, + setPage, + setPerPage, + fetchUsers, + updateUser, + deleteUser, + }; +}; diff --git a/src/crm/pages/Customers.tsx b/src/crm/pages/Customers.tsx index bd63a59..410a9dd 100644 --- a/src/crm/pages/Customers.tsx +++ b/src/crm/pages/Customers.tsx @@ -1,17 +1,140 @@ import * as React from "react"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import Alert from "@mui/material/Alert"; +import { useUsers } from "../hooks/useUsers"; +import { User } from "../types/user"; +import CustomersTable from "../components/CustomersTable"; +import EditUserModal from "../components/EditUserModal"; export default function Customers() { + const { + users, + loading, + error, + total, + page, + perPage, + setPage, + setPerPage, + fetchUsers, + updateUser, + deleteUser, + } = useUsers(); + + const [editingUser, setEditingUser] = React.useState(null); + const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); + const [actionError, setActionError] = React.useState(null); + const [successMessage, setSuccessMessage] = React.useState( + null, + ); + + const handleSearch = (query: string) => { + setPage(1); // Reset to first page when searching + fetchUsers(query); + }; + + const handleSort = (field: string) => { + fetchUsers(undefined, field, page, perPage); + }; + + const handlePageChange = (newPage: number) => { + setPage(newPage); + fetchUsers(undefined, undefined, newPage, perPage); + }; + + const handlePageSizeChange = (newPageSize: number) => { + setPerPage(newPageSize); + setPage(1); // Reset to first page when changing page size + fetchUsers(undefined, undefined, 1, newPageSize); + }; + + const handleEdit = (user: User) => { + setEditingUser(user); + setIsEditModalOpen(true); + setActionError(null); + setSuccessMessage(null); + }; + + const handleSaveUser = async (userId: string, updates: any) => { + setActionError(null); + const success = await updateUser(userId, updates); + if (success) { + setSuccessMessage("Customer updated successfully!"); + setTimeout(() => setSuccessMessage(null), 3000); + return true; + } + return false; + }; + + const handleDelete = async (userId: string) => { + setActionError(null); + const success = await deleteUser(userId); + if (success) { + setSuccessMessage("Customer deleted successfully!"); + setTimeout(() => setSuccessMessage(null), 3000); + } else { + setActionError("Failed to delete customer"); + setTimeout(() => setActionError(null), 3000); + } + }; + + const handleCloseModal = () => { + setIsEditModalOpen(false); + setEditingUser(null); + }; + return ( - - Customers Page - - - This is the customers management page where you can view and manage your - customer data. - + + + + Customer Management + + + Manage your customer database, search for specific customers, and + update their information. + + + + {actionError && ( + setActionError(null)}> + {actionError} + + )} + + {successMessage && ( + setSuccessMessage(null)}> + {successMessage} + + )} + + + + + + + ); } diff --git a/src/crm/types/user.ts b/src/crm/types/user.ts new file mode 100644 index 0000000..8370400 --- /dev/null +++ b/src/crm/types/user.ts @@ -0,0 +1,91 @@ +export interface UserLogin { + uuid: string; + username: string; + password: string; +} + +export interface UserName { + title: string; + first: string; + last: string; +} + +export interface UserStreet { + number: number; + name: string; +} + +export interface UserCoordinates { + latitude: number; + longitude: number; +} + +export interface UserTimezone { + offset: string; + description: string; +} + +export interface UserLocation { + street: UserStreet; + city: string; + state: string; + country: string; + postcode: string; + coordinates: UserCoordinates; + timezone: UserTimezone; +} + +export interface UserDob { + date: string; + age: number; +} + +export interface UserRegistered { + date: string; + age: number; +} + +export interface UserPicture { + large: string; + medium: string; + thumbnail: string; +} + +export interface User { + login: UserLogin; + name: UserName; + gender: string; + location: UserLocation; + email: string; + dob: UserDob; + registered: UserRegistered; + phone: string; + cell: string; + picture: UserPicture; + nat: string; +} + +export interface UsersApiResponse { + page: number; + perPage: number; + total: number; + span: string; + effectivePage: number; + data: User[]; +} + +export interface UserUpdateRequest { + email?: string; + login?: Partial; + name?: Partial; + gender?: string; + location?: Partial; + phone?: string; + cell?: string; +} + +export interface ApiResponse { + success: boolean; + message: string; + uuid?: string; +} From eca93efe3aebfd635645706eec1ef59bb8daf235 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 10 Jun 2025 15:16:06 +0000 Subject: [PATCH 2/4] Change DataGrid density to standard for better spacing cgen-5bf32647c84840269f0bac07bbec80f6 --- src/crm/components/CustomersTable.tsx | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/crm/components/CustomersTable.tsx b/src/crm/components/CustomersTable.tsx index d4bc260..46195ce 100644 --- a/src/crm/components/CustomersTable.tsx +++ b/src/crm/components/CustomersTable.tsx @@ -150,15 +150,9 @@ export default function CustomersTable({ minWidth: 150, valueGetter: (value, row) => `${row.name.first} ${row.name.last}`, renderCell: (params) => ( - - - {params.row.name.title} {params.row.name.first}{" "} - {params.row.name.last} - - - @{params.row.login.username} - - + + {params.row.name.title} {params.row.name.first} {params.row.name.last} + ), }, { @@ -178,12 +172,9 @@ export default function CustomersTable({ valueGetter: (value, row) => `${row.location.city}, ${row.location.country}`, renderCell: (params) => ( - - {params.row.location.city} - - {params.row.location.country} - - + + {params.row.location.city}, {params.row.location.country} + ), }, { @@ -322,7 +313,7 @@ export default function CustomersTable({ onSortModelChange={handleSortChange} getRowId={(row) => row.login.uuid} disableRowSelectionOnClick - density="comfortable" + density="standard" sx={{ border: "none", "& .MuiDataGrid-cell": { From 803b995e68b7e67de86f3ac5bd1c59f6e2bc2f76 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 10 Jun 2025 15:16:14 +0000 Subject: [PATCH 3/4] Change DataGrid density to comfortable for optimal spacing cgen-df60e9e6a44b4ae28333d8e42b3eab22 --- src/crm/components/CustomersTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crm/components/CustomersTable.tsx b/src/crm/components/CustomersTable.tsx index 46195ce..3741087 100644 --- a/src/crm/components/CustomersTable.tsx +++ b/src/crm/components/CustomersTable.tsx @@ -313,7 +313,7 @@ export default function CustomersTable({ onSortModelChange={handleSortChange} getRowId={(row) => row.login.uuid} disableRowSelectionOnClick - density="standard" + density="comfortable" sx={{ border: "none", "& .MuiDataGrid-cell": { From 67551b47ed7b7548ccc873a34e58c262cdcdb47f Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 10 Jun 2025 15:21:14 +0000 Subject: [PATCH 4/4] Create UserEditForm component cgen-8e92b7bfae7b4a83bea6e84aba63606f --- src/crm/components/EditUserModal.tsx | 214 +------------------------- src/crm/components/UserEditForm.tsx | 221 +++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 206 deletions(-) create mode 100644 src/crm/components/UserEditForm.tsx diff --git a/src/crm/components/EditUserModal.tsx b/src/crm/components/EditUserModal.tsx index 756692c..c316710 100644 --- a/src/crm/components/EditUserModal.tsx +++ b/src/crm/components/EditUserModal.tsx @@ -5,18 +5,12 @@ import { DialogContent, DialogActions, Button, - TextField, - Grid, Box, - Alert, Avatar, Typography, - FormControl, - InputLabel, - Select, - MenuItem, } from "@mui/material"; import { User, UserUpdateRequest } from "../types/user"; +import UserEditForm from "./UserEditForm"; interface EditUserModalProps { open: boolean; @@ -153,205 +147,13 @@ export default function EditUserModal({
- {error && ( - - {error} - - )} - - {success && ( - - Customer updated successfully! - - )} - - - {/* Personal Information */} - - - Personal Information - - - - - - Title - - - - - - - handleInputChange("name.first", e.target.value) - } - required - /> - - - - handleInputChange("name.last", e.target.value)} - required - /> - - - - handleInputChange("email", e.target.value)} - required - /> - - - - - Gender - - - - - {/* Contact Information */} - - - Contact Information - - - - - handleInputChange("phone", e.target.value)} - /> - - - - handleInputChange("cell", e.target.value)} - /> - - - {/* Address Information */} - - - Address Information - - - - - - handleInputChange( - "location.street.number", - parseInt(e.target.value) || 0, - ) - } - /> - - - - - handleInputChange("location.street.name", e.target.value) - } - /> - - - - - handleInputChange("location.city", e.target.value) - } - /> - - - - - handleInputChange("location.state", e.target.value) - } - /> - - - - - handleInputChange("location.country", e.target.value) - } - /> - - - - - handleInputChange("location.postcode", e.target.value) - } - /> - - + diff --git a/src/crm/components/UserEditForm.tsx b/src/crm/components/UserEditForm.tsx new file mode 100644 index 0000000..1259343 --- /dev/null +++ b/src/crm/components/UserEditForm.tsx @@ -0,0 +1,221 @@ +import * as React from "react"; +import { + TextField, + Grid, + Box, + Alert, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, +} from "@mui/material"; +import { User, UserUpdateRequest } from "../types/user"; + +interface UserEditFormProps { + user: User; + formData: UserUpdateRequest; + onInputChange: (field: string, value: any) => void; + error: string | null; + success: boolean; +} + +export default function UserEditForm({ + user, + formData, + onInputChange, + error, + success, +}: UserEditFormProps) { + return ( + <> + {error && ( + + {error} + + )} + + {success && ( + + Customer updated successfully! + + )} + + + {/* Personal Information */} + + + Personal Information + + + + + + Title + + + + + + onInputChange("name.first", e.target.value)} + required + /> + + + + onInputChange("name.last", e.target.value)} + required + /> + + + + onInputChange("email", e.target.value)} + required + /> + + + + + Gender + + + + + {/* Contact Information */} + + + Contact Information + + + + + onInputChange("phone", e.target.value)} + /> + + + + onInputChange("cell", e.target.value)} + /> + + + {/* Address Information */} + + + Address Information + + + + + + onInputChange( + "location.street.number", + parseInt(e.target.value) || 0, + ) + } + /> + + + + + onInputChange("location.street.name", e.target.value) + } + /> + + + + onInputChange("location.city", e.target.value)} + /> + + + + onInputChange("location.state", e.target.value)} + /> + + + + onInputChange("location.country", e.target.value)} + /> + + + + onInputChange("location.postcode", e.target.value)} + /> + + + + ); +}