Skip to content

Commit

Permalink
Add driver's license to user pages
Browse files Browse the repository at this point in the history
  • Loading branch information
aelassas committed Dec 3, 2024
1 parent 8bfc806 commit 547b17c
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 43 deletions.
32 changes: 29 additions & 3 deletions api/__tests__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ describe('POST /api/create-user', () => {
if (!await helper.exists(tempAvatar)) {
await fs.copyFile(AVATAR1_PATH, tempAvatar)
}
const tempLicense = path.join(env.CDN_TEMP_LICENSES, LICENSE1)
if (!await helper.exists(tempLicense)) {
await fs.copyFile(LICENSE1_PATH, tempLicense)
}

const contractFileName = `${nanoid()}.pdf`
const contractFile = path.join(env.CDN_TEMP_CONTRACTS, contractFileName)
Expand All @@ -197,6 +201,7 @@ describe('POST /api/create-user', () => {
location: 'location',
bio: 'bio',
avatar: AVATAR1,
license: LICENSE1,
contracts,
}
let res = await request(app)
Expand Down Expand Up @@ -291,10 +296,12 @@ describe('POST /api/create-user', () => {
expect(userToken?.token.length).toBeGreaterThan(0)
await userToken?.deleteOne()

// test success (without avatar)
// test success (without avatar and license)
let email = testHelper.GetRandomEmail()
payload.email = email
payload.type = bookcarsTypes.UserType.User
payload.avatar = undefined
payload.license = undefined
res = await request(app)
.post('/api/create-user')
.set(env.X_ACCESS_TOKEN, token)
Expand All @@ -308,9 +315,28 @@ describe('POST /api/create-user', () => {
expect(userToken?.token.length).toBeGreaterThan(0)
await userToken?.deleteOne()

// test success (avatar not found)
// test success (avatar and license not found)
email = testHelper.GetRandomEmail()
payload.email = email
payload.avatar = `${nanoid()}.jpg`
payload.license = `${nanoid()}.pdf`
res = await request(app)
.post('/api/create-user')
.set(env.X_ACCESS_TOKEN, token)
.send(payload)
expect(res.statusCode).toBe(200)
user = await User.findOne({ email })
expect(user).not.toBeNull()
await user?.deleteOne()
userToken = await Token.findOne({ user: user?._id })
expect(userToken).not.toBeNull()
expect(userToken?.token.length).toBeGreaterThan(0)
await userToken?.deleteOne()

// test failure (user already exists)
payload.email = USER2_EMAIL
payload.avatar = 'unknown.jpg'
// payload.avatar = `${nanoid()}.jpg`
// payload.license = `${nanoid()}.pdf`
res = await request(app)
.post('/api/create-user')
.set(env.X_ACCESS_TOKEN, token)
Expand Down
13 changes: 13 additions & 0 deletions api/src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ export const create = async (req: Request, res: Response) => {
}
}

// license
if (body.license && user.type === bookcarsTypes.UserType.User) {
const license = path.join(env.CDN_TEMP_LICENSES, body.license)
if (await helper.exists(license)) {
const filename = `${user._id}${path.extname(body.license)}`
const newPath = path.join(env.CDN_LICENSES, filename)

await fs.rename(license, newPath)
user.license = filename
await user.save()
}
}

if (body.password) {
return res.sendStatus(200)
}
Expand Down
6 changes: 4 additions & 2 deletions backend/src/assets/css/create-user.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ div.create-user {
color: #121212;
}

div.create-user .driver-license-field {
margin-top: 15px;
}

/* Device width is less than or equal to 960px */

@media only screen and (width <=960px) {
.user-form {
width: 360px;
height: 950px;
padding: 30px;
}
}
Expand All @@ -32,7 +35,6 @@ div.create-user {
@media only screen and (width >=960px) {
.user-form {
width: 550px;
height: 840px;
padding: 30px;
}
}
22 changes: 22 additions & 0 deletions backend/src/assets/css/driver-license.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
div.driver-license {
display: flex;
flex-direction: row;
align-items: center;
}

div.driver-license .filename {
width: 100%;
margin-right: 5px;
color: #666;
}

div.driver-license div.actions {
display: flex;
flex-direction: row;
}

@media only screen and (width <=960px) {
div.driver-license .filename {
width: 210px;
}
}
4 changes: 4 additions & 0 deletions backend/src/assets/css/update-user.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ div.update-user .resend-activation-link {
margin: 20px 0 10px;
}

div.update-user .driver-license-field {
margin-top: 15px;
}

/* Device width is less than or equal to 960px */

@media only screen and (width <=960px) {
Expand Down
18 changes: 18 additions & 0 deletions backend/src/common/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,21 @@ export const carOptionAvailable = (car: bookcarsTypes.Car | undefined, option: s
* @returns {boolean}
*/
export const isValidURL = (url: string) => validator.isURL(url, { protocols: ['http', 'https'] })

/**
* Download URI.
*
* @param {string} uri
* @param {string} [name='']
*/
export const downloadURI = (uri: string, name: string = '') => {
const link = document.createElement('a')
// If you don't know the name or want to use
// the webserver default set name = ''
link.setAttribute('download', name)
link.setAttribute('target', '_blank')
link.href = uri
document.body.appendChild(link)
link.click()
link.remove()
}
153 changes: 153 additions & 0 deletions backend/src/components/DriverLicense.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React, { useState } from 'react'
import { IconButton, Input, OutlinedInput } from '@mui/material'
import { Upload as UploadIcon, Delete as DeleteIcon, Visibility as ViewIcon } from '@mui/icons-material'
import * as bookcarsTypes from ':bookcars-types'
import * as bookcarsHelper from ':bookcars-helper'
import { strings as commonStrings } from '@/lang/common'
import * as UserService from '@/services/UserService'
import * as helper from '@/common/helper'
import env from '@/config/env.config'

import '@/assets/css/driver-license.css'

interface DriverLicenseProps {
user?: bookcarsTypes.User
variant?: 'standard' | 'outlined'
className?: string
onUpload?: (filename: string) => void
onDelete?: () => void
}

const DriverLicense = ({
user,
variant = 'standard',
className,
onUpload,
onDelete,
}: DriverLicenseProps) => {
const [license, setLicense] = useState(user?.license || null)

const handleClick = async () => {
const upload = document.getElementById('upload-license') as HTMLInputElement
upload.value = ''
setTimeout(() => {
upload.click()
}, 0)
}

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) {
helper.error()
return
}

const reader = new FileReader()
const file = e.target.files[0]

reader.onloadend = async () => {
try {
let filename: string | null = null
if (user) {
// upload new file
const res = await UserService.updateLicense(user._id!, file)
if (res.status === 200) {
filename = res.data
} else {
helper.error()
}
} else {
// Remove previous temp file
if (license) {
await UserService.deleteTempLicense(license)
}
// upload new file
filename = await UserService.createLicense(file)
}

if (filename) {
if (onUpload) {
onUpload(filename)
}
}

setLicense(filename)
} catch (err) {
helper.error(err)
}
}

reader.readAsDataURL(file)
}

return (
<div className={`driver-license ${className || ''}`}>
{variant === 'standard' ? (
<Input
value={license || commonStrings.UPLOAD_DRIVER_LICENSE}
readOnly
onClick={handleClick}
className="filename"
/>
) : (
<OutlinedInput
value={license || commonStrings.UPLOAD_DRIVER_LICENSE}
readOnly
onClick={handleClick}
className="filename"
/>
)}
<div className="actions">
<IconButton
size="small"
onClick={handleClick}
>
<UploadIcon className="icon" />
</IconButton>

{license && (
<>
<IconButton
size="small"
onClick={() => {
const url = `${bookcarsHelper.trimEnd(user ? env.CDN_LICENSES : env.CDN_TEMP_LICENSES, '/')}/${license}`
helper.downloadURI(url)
}}
>
<ViewIcon className="icon" />
</IconButton>
<IconButton
size="small"
onClick={async () => {
try {
let status = 0
if (user) {
status = await UserService.deleteLicense(user._id!)
} else {
status = await UserService.deleteTempLicense(license!)
}

if (status === 200) {
setLicense(null)

if (onDelete) {
onDelete()
}
} else {
helper.error()
}
} catch (err) {
helper.error(err)
}
}}
>
<DeleteIcon className="icon" />
</IconButton>
</>
)}
</div>
<input id="upload-license" type="file" hidden onChange={handleChange} />
</div>
)
}

export default DriverLicense
4 changes: 4 additions & 0 deletions backend/src/lang/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const strings = new LocalizedStrings({
LICENSE_REQUIRED: 'Permis de Conduire Requis',
LICENSE: 'Permis de Conduire',
MIN_RENTAL_DAYS: 'Jours Minimum de location',
DRIVER_LICENSE: 'Permis de conduire',
UPLOAD_DRIVER_LICENSE: 'Charger le permis de conduire...',
},
en: {
GENERIC_ERROR: 'An unhandled error occurred.',
Expand Down Expand Up @@ -168,6 +170,8 @@ const strings = new LocalizedStrings({
LICENSE_REQUIRED: "Driver's License Required",
LICENSE: "Driver's License",
MIN_RENTAL_DAYS: 'Minimum Rental Days',
DRIVER_LICENSE: "Driver's License",
UPLOAD_DRIVER_LICENSE: "Upload driver's license...",
},
})

Expand Down
Loading

0 comments on commit 547b17c

Please sign in to comment.