Focal is a workflow and client management tool for wedding photographers. It aims to help photographers consolidate clients information (personal info, photography packages, shoot events, and private notes) and tie it to a set of workflow tasks.
Future features would include integrating third-party services like Google Gmail, Google Calendar or Stripe to reduce context switching for the users. For example, the app would be able to send emails, update their calendars, or send invoices directly from the workflow tasks.
The project is built in React and works with a Elixir/Phoenix API. Other dependencies include react-testing-library
, formik
, date-fns
, axios
, sass
, react-router
, moment
, jest
.
Note that the project is hosted on Heroku free-plan which has slow spin-up times -- Please give it a few seconds to load.
- Google Authentication
- Create & Update Client Information
- Create & Update Client Packages
- Create & Update Client Events
- Auto Generated Workflows Tied to Client Packages with Todo-style Task Completion.
- All Clients View with a List of the Current Task for All Clients
- Creating New Client
- Updating Client Information
- Creating Client Packages
- Updating Client Packages to Create Workflows and Create Events
- Updating Client Packages to Remove Events and Workflows
- Toggling Task Completion in Client Workflows
git clone https://github.com/Focal-App/focal.git
cd focal
npm install
npm start // in one terminal
npm test // in a second terminal
git push heroku master --force
git push origin/<branch name>:master --force
The DataAdapter class acts as a boundary between the data received from the API and the data shape the rest of the frontend codebase relies on. By wrapping incoming API data with our DataAdapters, any changes to the API data shape can be handled in one place as opposed to updating the changes throughout the codebase.
It can set formatted default values for null or un-renderable API values that the frontend can render with. For example:
// Null values are turned to dashes to signify no content
null → "-"
// ISOStrings are converted to formatted dates
"2019-04-17T07:00:00.000Z" → "April 17, 2019"
// Penny integers are converted to price strings
1000 → "10.00"
These formatted values allow the frontend codebase to immediately render values without checking the validity of the value (no more event_name ? event_name : "-"
validity checks in react components).
For example, dates need to be a specific date format to render correctly in the input date elements.
For example, DataAdapter.toEventModel()
will generate a default event data object. This is then used in a create event form to make sure the form data shape is what the API expects.
This codebase utilities 3 levels of tests, with a focus on fewer unit tests and more behavioral tests.
Unit tests are generally used for simple utility functions.
Integration tests touches several react components, but no mock API calls.
Behavioral Tests can mount the entire application and mock API calls. It is used to test behavioral flows that touches numerous pages and components.
Example Behavioral Test:
const user_uuid = "1234";
const client_uuid = "1111";
const authUser = { uuid: user_uuid, avatar: "avatar-image-link" };
it(`renders list of clients if there is client data after successful call,
clicking view will take user to client detail page`, async () => {
// setup component and mock api calls
const clientsList = MockApiData.successData([
MockApiData.partialClientData({
client_first_name: "Sammy",
partner_first_name: "David",
upcoming_shoot_date: "2020-07-17T14:00:00Z",
uuid: client_uuid
}),
MockApiData.partialClientData({
uuid: "0000",
client_first_name: "Natasha",
partner_first_name: "Zihao",
package_name: "Wedding Classic",
upcoming_shoot_date: "2020-09-17T14:00:00Z",
current_stage: MockApiData.taskData({
category: "Proposal & Retainer",
step: "Confirm Proposal & Retainer"
})
})
])
const client = MockApiData.successData(
MockApiData.allClientData({ uuid: client_uuid })
)
const apiHandler = new MockAPIHandler({
[Endpoints.getClients(user_uuid)]: [clientsList],
[Endpoints.getClient(client_uuid)]: [client]
});
let component;
// mounts the entire application as we are testing the flow between 2 pages
await act(async () => {
component = render(
<MemoryRouter initialEntries={["/clients"]} initialIndex={0}>
<App apiHandler={apiHandler} authUser={authUser}/>
</MemoryRouter>
)
})
const { findByText, getByText, getAllByText } = component;
// asserts expected text to be found on All Clients Page
await waitForElement(() =>
findByText(/Sammy & David/i)
)
getByText(/Wedding Premier/i)
getByText(/July 17, 2020/i)
getByText(/New Client Inquiry/i)
getByText(/Request More Information/i)
getByText(/Natasha & Zihao/i)
getByText(/September 17, 2020/i)
getByText(/Confirm Proposal & Retainer/i)
// Clicking the View button to navigate to a client detail page
await act(async () => {
fireEvent.click(getAllByText("View")[0]);
})
// asserts expected text to be found on the Client Detail Page
await waitForElement(() =>
findByText(/sammy lee/i)
)
getByText(/client information/i)
getAllByText(/package/i)
getAllByText(/new client inquiry/i)
})