By Ethan Kaseff - Visit Cuddlist
Cuddlist is a full stack application utilizing a Python/Flask backend and an Javascript/React/Redux frontend. It is a clone of www.cuddlist.com with added functionality, such as sending and tracking session reqeusts in app.
Cuddlist is a react application organized into modular components for easy control and management.
Redux was used to track state across the global application whenever needed. For example the redux store holds the session user giving the application access to user data, images and session requests.
Cuddlist uses Tailwind, which is a low level CSS Framework, for styling and design. The design is a mix of personal architecture and open source resources graciously available around the web.
Cuddlist uses Flask with Python as the backend server.
The home page serves as the main search page as well. The location dropdown autopopulates from the locations in the database so it always stays up to date. The search button also has error handling in case a user forgets to pick a location first.
The button to send a request ensures that only someone that is logged in as a client can actually send a request. Users that aren't logged in and Cuddlists, can view Cuddlist profiles, but cannot send a reqeust.
function SessionRequestFormModal({id}) {
const [showModal, setShowModal] = useState(false);
const [cuddlist, setCuddlist] = useState(false)
const [buttonColor, setButtonColor] = useState('bg-blue-500 hover:bg-blue-600')
const user = useSelector(state => state.session.user)
useEffect(() => {
if (user) {
if (user.type === 'cuddlists'){
setCuddlist(true)
setButtonColor('bg-gray-300')
}
} else {
setCuddlist(true)
setButtonColor('bg-gray-300')
}
}, [user])
return (
<>
<button className={buttonColor + ` rounded-lg shadow-lg text-white font-bold w-full p-1 text-lg m-1`}
disabled={cuddlist}
type='button'
onClick={() => setShowModal(true)}>
Send Session Request
</button>
{ cuddlist &&
<p className='text-sm text-red-600 font-hairline text-center p-2 m-3'>
Login as a client to send requests
</p>
}
{showModal && (
<Modal onClose={() => setShowModal(false)}>
<SessionRequestForm id={id}/>
</Modal>
)}
</>
);
}
export default SessionRequestFormModal;
The site utilizes modals whenever possible for easy use and navigation. Session request forms were an obvious place to use a modal as it mimiced the third party functionality the other site has, and barely gets the user off of the site before getting placed back to continue navigating.
The profile page is easy to navigate with a helpful tab to distinguish between personal information and session requests. The editing is intuitive with users able to click anywhere on a field (or specifically the small editing button) to trigger the div to switch over to an input. The "Save" Button starts off as gray and disabled and turns blue after some type of change has occured to let the user know they can save now.
profile.editing.mov
// Code below is just showing what one path looks like
const ProfileInformation = () => {
const dispatch = useDispatch();
const user = useSelector(state => state.session.user);
// Boolean values to track what we are editing
const [editFirstName, setEditFirstName] = useState(false);
// Field values to keep in local state
const [firstName, setFirstName] = useState(user.firstName);
const [buttonColor, setButtonColor] = useState('bg-gray-500')
useEffect(() => {
if (firstName !== user.firstName ||
phoneNumber !== user.phoneNumber) {
setButtonColor('bg-blue-700 hover:bg-blue-500')
}
},[firstName, user.firstName])
// Focus on input when it shows up
useEffect(() => {
const input = document.getElementById('input');
if (input) {
input.focus();
}
})
return (
<form onSubmit={handleSubmit}>
<table className='table-fixed'>
<tbody className='divide-y-2'>
<tr>
<td className='w-32'>First Name</td>
<td className=''>
<div>
{!editFirstName && <span onClick={() => setEditFirstName(true)}>{firstName} <i className="fas fa-edit fa-xs"></i></span>}
{editFirstName &&
<input
id='input'
type='text'
name='firstNameUpdate'
onChange={(e) => setFirstName(e.target.value)}
value={firstName}
placeholder='First Name'
onBlur={() => setEditFirstName(false)}
></input>
}
</div>
</td>
</tr>
......
Images are easy to upload and delete with previews being available immediately upon upload and removed immediately upon deletion.
Photo.Functionality.mov
{user.type === 'cuddlists' &&
<div>
<h1 className='text-center text-blue-500 mb-5 font-bold text-xl'>Upload a photo for your page</h1>
<ImageUpload />
<div className='flex flex-wrap'>
{ user.images &&
user.images.map( image => {
return (
<div className='w-1/2 p-6 flex justify-center flex-col relative'>
<img src={image.imageUrl} alt={'photo' + image.id}></img>
<button
onClick={() => dispatch(deleteImage(image.id))}
>
<div className={"absolute right-8 top-8 rounded-full bg-red-500 p-1 text-xs shadow w-6 h-6 "}><i className="fas fa-times text-white"></i></div>
</button>
</div>
)
})
}
</div>
<div>
</div>
</div>
}
Users can easily track session requests in their dashboard. A simple difference like "Client Name" and "Cuddlist Name" help distinguish the page and make it clear which type of user is currently using it.
Light and effective error handling ensures that users don't get caught on a snag. Each error is clear and clearly placed, and only arrives right when there is an issue.
Cuddlist is a company that I have a personal conneciton with so it was great to delve into this project and get my hands dirty on a different side of that business. I enjoyed conceptualizing and building features that I know would be valuable to both the professionals and their clients.
In the future I would like to build the application further to include a messaging platform that would be seemlessly personal and easy for the client, and a robust CRM tool with notes and session reminders for the therapists. Beyond just the tracking of communication, the application could also intake financial data about sessions and budgeting, allowing for the site to serve as a larger one stop shop for individuals working in the field.