Skip to content

Commit 1945175

Browse files
committed
PC-112: Fix bug where the logo doesn't adjust correctly
1 parent edd3cbd commit 1945175

14 files changed

Lines changed: 164 additions & 99 deletions

File tree

app/assets/stylesheets/application.bootstrap.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ $utilities: map-merge(
2525
@import 'application/login_page';
2626
@import 'application/customer_form.scss';
2727
@import 'application/input.scss';
28+
@import 'application/header';

app/assets/stylesheets/application/customer_form.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
.image-placeholder {
77
max-width: 136px;
8-
min-height: 117px;
8+
max-height: 117px;
99

1010
label:hover {
1111
cursor: pointer;
File renamed without changes.

app/assets/stylesheets/application/layout.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
@import 'layout/header';
2-
31
.layout {
42
background-image: url('./assets/bg-calculator.png');
53
background-position: left calc(#{min(50vh, 504px)});

app/controllers/api/v1/customers_controller.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ def index
77
end
88

99
def upsert
10-
customer = Customer.where('LOWER(company_name) = ?',
11-
customer_params[:company_name].downcase).first_or_initialize
10+
company_name = customer_params[:company_name].downcase
11+
customer = Customer.where('LOWER(company_name) = ?', company_name).first_or_initialize
1212
customer.assign_attributes(customer_params)
13+
1314
if customer.save
1415
render json: CustomerSerializer.new(customer).serializable_hash, status: :ok
1516
else
16-
render json: { errors: customer.errors.full_messages }, status: :unprocessable_entity
17+
render json: ErrorSerializer.new(customer.errors).serializable_hash, status: :unprocessable_entity
1718
end
1819
end
1920

2021
private
2122

2223
def customer_params
23-
params.expect(customer: [:company_name, :first_name, :last_name, :email, :position, :address,
24+
params.expect(customer: [:company_name, :first_name, :last_name,
25+
:email, :position, :address,
2426
:notes, :logo])
2527
end
2628
end

app/javascript/components/services/fetchService.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ENDPOINTS } from '../shared'
22
import { get, del, post, put } from './api/httpRequests'
3+
import { extractNames } from '../utils'
34

45
export const fetchQuotes = {
56
create: (data) => post(ENDPOINTS.QUOTES, data),
@@ -13,4 +14,19 @@ export const fetchAuthentication = {
1314
export const fetchCustomers = {
1415
index: () => get(ENDPOINTS.CUSTOMERS),
1516
upsert: (data) => post(ENDPOINTS.CUSTOMERS_UPSERT, data),
17+
upsertUseFormData: (customer) => {
18+
const formData = new FormData()
19+
const { first_name, last_name } = extractNames(customer.full_name)
20+
21+
if (customer.logo_file) formData.append('customer[logo]', customer.logo_file)
22+
formData.append('customer[company_name]', customer.company_name)
23+
formData.append('customer[first_name]', first_name)
24+
formData.append('customer[last_name]', last_name)
25+
formData.append('customer[position]', customer.position)
26+
formData.append('customer[email]', customer.email)
27+
formData.append('customer[address]', customer.address)
28+
formData.append('customer[notes]', customer.notes)
29+
30+
return post(ENDPOINTS.CUSTOMERS_UPSERT, formData)
31+
},
1632
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from './fetchService'
2+
export * from './api/httpRequests'
3+
export * from './api/axiosInstance'

app/javascript/components/shared/CustomerForm.jsx

Lines changed: 98 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import React, { useEffect, useState } from 'react'
22
import { Row, Col, Form, Button } from 'react-bootstrap'
33
import { PcDropdownSelect, PcCompanyLogoUploader, PcInput } from '../ui'
4-
import { ROUTES, STEPS } from './constants'
4+
import { EMPTY_ENTITIES, INPUT_VALIDATORS, ROUTES, STEPS } from './constants'
55
import { fetchCustomers, fetchQuotes } from '../services'
66
import { useAppHooks } from '../hooks'
77
import { extractNames } from '../utils'
88

99
export const CustomerForm = () => {
10-
const defaultCustomer = {
11-
company_name: '',
12-
full_name: '',
13-
email: '',
14-
position: '',
15-
address: '',
16-
notes: '',
17-
logo_file: null,
18-
logo_url: null,
19-
}
20-
2110
const [customers, setCustomers] = useState([])
22-
const [customer, setCustomer] = useState(defaultCustomer)
23-
11+
const [customer, setCustomer] = useState(EMPTY_ENTITIES.customer)
12+
const [isNextDisabled, setIsNextDisabled] = useState(true)
2413
const [errors, setErrors] = useState({})
2514

2615
const { navigate } = useAppHooks()
@@ -36,34 +25,36 @@ export const CustomerForm = () => {
3625
label: customer.attributes.company_name,
3726
}))
3827

39-
const validateForm = () => {
40-
const newErrors = {}
41-
42-
if (!customer.company_name.trim()) {
43-
newErrors.company_name = 'Company name is required'
44-
}
45-
46-
if (!!customer.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customer.email)) {
47-
newErrors.email = 'Invalid email format'
48-
}
49-
50-
setErrors(newErrors)
51-
52-
return Object.keys(newErrors).length === 0
53-
}
28+
const selectedCompany =
29+
customers.find((c) => c.attributes.company_name.toLowerCase() === customer.company_name.toLowerCase())?.id ||
30+
customer.company_name
5431

5532
const handleCompanyChange = (e) => {
56-
const value = e.target.value
33+
const { value } = e.target
5734
const selectedCustomer = customers.find((customer) => customer.id === value)
5835

5936
if (selectedCustomer) {
6037
setCustomer(selectedCustomer.attributes)
6138
} else {
6239
setCustomer({
63-
...defaultCustomer,
40+
...EMPTY_ENTITIES.customer,
6441
company_name: value,
6542
})
6643
}
44+
45+
setErrors((prev) => ({ ...prev, company_name: '' }))
46+
}
47+
48+
const handleCompanyInputChange = (e) => {
49+
const { value } = e.target
50+
51+
if (value) {
52+
setIsNextDisabled(false)
53+
setErrors((prev) => ({ ...prev, company_name: '' }))
54+
} else {
55+
setIsNextDisabled(true)
56+
setErrors((prev) => ({ ...prev, company_name: 'Company name is required' }))
57+
}
6758
}
6859

6960
const handleInputChange = (e) => {
@@ -76,73 +67,92 @@ export const CustomerForm = () => {
7667
setErrors((prev) => ({ ...prev, [id]: '' }))
7768
}
7869

79-
const handleChangeFullName = (e) => {
70+
const handleEmailChange = (e) => {
8071
const { value } = e.target
81-
setCustomer({
82-
...customer,
83-
...extractNames(value),
84-
full_name: value,
85-
})
72+
73+
if (value && !INPUT_VALIDATORS.email.test(value)) {
74+
setIsNextDisabled(true)
75+
setErrors((prev) => ({ ...prev, email: 'Invalid email format' }))
76+
} else {
77+
setIsNextDisabled(false)
78+
setErrors((prev) => ({ ...prev, email: '' }))
79+
}
80+
81+
setCustomer((prev) => ({ ...prev, email: value }))
8682
}
8783

88-
const handleChangeLogo = (e) => {
89-
const file = e.target.files[0]
84+
const handleFullNameChange = (e) => {
85+
const { value } = e.target
9086

9187
setCustomer((prev) => ({
9288
...prev,
93-
logo_file: file || null,
94-
logo_url: file ? URL.createObjectURL(file) : null,
89+
...extractNames(value),
90+
full_name: value,
9591
}))
96-
97-
setErrors((prev) => ({ ...prev, logo: '' }))
9892
}
9993

100-
const handleNext = (e) => {
101-
e.preventDefault()
94+
const handleLogoChange = (e) => {
95+
const file = e.target.files[0]
10296

103-
if (!validateForm()) {
97+
if (!file) {
98+
setErrors((prev) => ({ ...prev, logo: '' }))
10499
return
105100
}
106101

107-
const formData = new FormData()
108-
const { first_name, last_name } = extractNames(customer.full_name)
109-
110-
if (customer.logo_file) formData.append('customer[logo]', customer.logo_file)
111-
formData.append('customer[company_name]', customer.company_name)
112-
formData.append('customer[first_name]', first_name)
113-
formData.append('customer[last_name]', last_name)
114-
formData.append('customer[position]', customer.position)
115-
formData.append('customer[email]', customer.email)
116-
formData.append('customer[address]', customer.address)
117-
formData.append('customer[notes]', customer.notes)
118-
119-
fetchCustomers.upsert(formData)
120-
.then(async (response) => {
121-
const { data: customerData } = response
122-
123-
if (!customers.some((c) => c.id === customerData.id)) {
124-
setCustomers((prev) => [...prev, customerData])
125-
}
126-
127-
const { data: quoteData } = await fetchQuotes.create({
128-
quote: {
129-
customer_id: customerData.id,
130-
total_price: 0,
131-
step: STEPS.ITEM_PRICING,
132-
},
133-
})
134-
135-
navigate(`${ROUTES.ITEM_PRICING}?quote_id=${quoteData.id}`)
136-
}).catch((error) => {
137-
setErrors(prev => ({ ...prev, logo: error.response.data.errors }))
138-
})
102+
const logoErrors = []
103+
104+
if (file.size > INPUT_VALIDATORS.maxSizeFile) {
105+
logoErrors.push('Logo must be less than 2MB')
106+
}
107+
108+
if (!INPUT_VALIDATORS.fileType.includes(file.type)) {
109+
logoErrors.push('Logo must be a JPEG or PNG file')
110+
}
111+
112+
if (logoErrors.length > 0) {
113+
setIsNextDisabled(true)
114+
setErrors((prev) => ({ ...prev, logo: logoErrors.join('\n') }))
115+
} else {
116+
setIsNextDisabled(false)
117+
setCustomer((prev) => ({
118+
...prev,
119+
logo_file: file,
120+
logo_url: URL.createObjectURL(file),
121+
}))
122+
123+
setErrors((prev) => ({ ...prev, logo: '' }))
124+
}
139125
}
140126

141-
if (!customers) return null
127+
const handleNext = async (e) => {
128+
e.preventDefault()
142129

143-
const selectedCompany =
144-
customers.find((c) => c.attributes.company_name.toLowerCase() === customer.company_name.toLowerCase())?.id ||
145-
customer.company_name
130+
setIsNextDisabled(true) // disable next button while form is being submitted
131+
132+
try {
133+
const { data: customerData } = await fetchCustomers.upsertUseFormData(customer)
134+
135+
if (!customers.some((c) => c.id === customerData.id)) {
136+
setCustomers((prev) => [...prev, customerData])
137+
}
138+
139+
const { data: quoteData } = await fetchQuotes.create({
140+
quote: {
141+
customer_id: customerData.id,
142+
total_price: 0,
143+
step: STEPS.ITEM_PRICING,
144+
},
145+
})
146+
147+
navigate(`${ROUTES.ITEM_PRICING}?quote_id=${quoteData.id}`)
148+
} catch (error) {
149+
const logoErrors = error?.response?.data?.errors || { errors: [] }
150+
151+
setErrors(prev => ({ ...prev, ...logoErrors }))
152+
} finally {
153+
setIsNextDisabled(false) // enable next button
154+
}
155+
}
146156

147157
return (
148158
<Form onSubmit={handleNext} className={'d-flex flex-column w-100 align-items-center'}>
@@ -152,7 +162,8 @@ export const CustomerForm = () => {
152162
<Col className={'image-placeholder'}>
153163
<PcCompanyLogoUploader
154164
id="company_logo"
155-
onChange={handleChangeLogo}
165+
onChange={handleLogoChange}
166+
accept={INPUT_VALIDATORS.fileType.join(',')}
156167
logo={customer.logo_url}
157168
error={errors.logo} />
158169
</Col>
@@ -172,7 +183,7 @@ export const CustomerForm = () => {
172183
value={selectedCompany}
173184
error={errors.company_name}
174185
onChange={handleCompanyChange}
175-
onInputChange={handleInputChange}
186+
onInputChange={handleCompanyInputChange}
176187
hasIcon={true}
177188
/>
178189
</Col>
@@ -186,7 +197,7 @@ export const CustomerForm = () => {
186197
label="Client"
187198
height="42px"
188199
value={customer.full_name}
189-
onChange={handleChangeFullName}
200+
onChange={handleFullNameChange}
190201
/>
191202
</Col>
192203
<Col className="title-input">
@@ -215,7 +226,7 @@ export const CustomerForm = () => {
215226
height="42px"
216227
value={customer.email}
217228
error={errors.email}
218-
onChange={handleInputChange}
229+
onChange={handleEmailChange}
219230
/>
220231
</Col>
221232
<Col>
@@ -244,7 +255,7 @@ export const CustomerForm = () => {
244255
</Col>
245256
</Row>
246257
</div>
247-
<Button type={'submit'} className="pc-btn-next" disabled={!customer.company_name}>
258+
<Button type={'submit'} className="pc-btn-next" disabled={isNextDisabled}>
248259
Next
249260
</Button>
250261
</Form>

app/javascript/components/shared/constants.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,24 @@ export const IMAGE_ASSETS = {
4343
},
4444
// future asset files can be added here
4545
}
46+
47+
export const EMPTY_ENTITIES = {
48+
customer: {
49+
company_name: '',
50+
full_name: '',
51+
email: '',
52+
position: '',
53+
address: '',
54+
notes: '',
55+
logo_file: null,
56+
logo_url: null,
57+
}
58+
// future empty entities can be added here
59+
}
60+
61+
export const INPUT_VALIDATORS = {
62+
email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
63+
maxSizeFile: 2 * 1024 * 1024,
64+
fileType: ['image/jpeg', 'image/png'],
65+
// future validation messages can be added here
66+
}

app/javascript/components/ui/PcCompanyLogoUploader.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const PcCompanyLogoUploader = ({ id, logo, error, ...props }) => {
66
const logoDisplay = logo ?
77
<img src={logo}
88
alt="Company Logo"
9-
className={'img-fluid'} />
9+
className={'object-fit-contain w-100 h-100'} />
1010
: <PcIcon name="placeholder" alt="Placeholder Logo" />
1111

1212
return <Form.Group className="d-flex flex-column w-100 h-100">
@@ -19,7 +19,6 @@ export const PcCompanyLogoUploader = ({ id, logo, error, ...props }) => {
1919
id={id}
2020
className={'d-none'}
2121
type={'file'}
22-
accept={'image/jpeg,image/png'}
2322
{...props}
2423
/>
2524
{error && <div className="text-danger fs-12">{error}</div>}

0 commit comments

Comments
 (0)