Plugs & Mugs
A one-spot look up for working outside the home office
currently on beta release. wait times, noise levels, descriptions and photos have not yet been added, but the rest of the data is accurate
As remote work becomes the norm, workers seek workspaces beyond their home offices. To simplify the overwhelming task of sifting through suitable locations, Plugs and Mugs presents a curated collection of 100+ work-friendly spaces across New York City, each tested out in person. Each location has bathrooms, wifi, and outlets - essential to any 3+ hour work session.
Branding
Design process
Plugs and Mugs was a close collaborative effort with my friend and coffee blogger who originally suggested the project to be a blog. He had a name for it - plugs and mugs, which has a nice catchiness and lilt. For this process, I analyzed the blog articles and content of the author's writing. It was personal, friendly, and fun, all while being informative and succinct. With complete creative freedom on my side, I wanted to push this project in three regards: usability, art, and branding.
Illustration and color choice
I wanted illustration to be a core part of the design, since it can add so much interest and vibrancy to a static website. I took inspiration from traditional art,game design, illustrators like Pascal Campion, and casual story-focused video games like Florence and Assemble with Care - Art through which I experienced a sense of cozyness.
As I started sketching in Photoshop, a shy girl appeared in the image, who happens to be a coffee connoisseur. She is always accompanied by a mysterious Shiba Inu and a blue cat with strawberry-whiskers.
I had the idea that every location will have an illustration that uniquely describe the presented space. With time, users would not have to look at the description, but imagine intuitively via the vocabulary of the image, whether it is a cafe that suits them.
Using the initial sketches, I set an illustration guide: line drawing, flat coloring, and a limited palette. To capture the freshness and exuberance of Spring, I carefully selected 13 themed colors. To capture genuineness and simplicity, I restricted myself to the classic cartoonist Photoshop brush and 4 brush sizes.
After surveying common cafe properties to illustrate, from location type, table size, to wifi strength, I created a total of 45 layers that can be remixed into a representation of a unique cafe.
To maintain a visual hierarchy, I combined a bright and playful pink and white for the UI to keep the UI layer separate from the map layer.
Typography
I chose the humanist sans serif Gitan Latin and the old style, readable Edita for the blog content, inspired by the humanist and humorous style of magazines like the New Yorker.
Logo
The logo is p&m, with the p (plug) having two plug pins to the left and the m (mug) with a little handle on the right. For a flourish, I borrowed the elegant ampersand from Quiverleaf CF, another humanist sans serif font.
Web Design
The website has three main layers. The map layer, the page layer, and the search tools and navigation layer. The map layer is low contrast, configured using mapTiler, and echoes the low saturation illustration colors. The second layer is medium contrast and optimized for readability by leaving plenty of whitespace. The search tools and navigation layer is high contrast but has less whitespace. Each layer has unique transitions and shadows to further convey depth and hierarchy.
UI components and Icons
Various UI components can be found around the application, such as buttons, tags, inputs, and multiselects. For icons, the lighthearted and stylized visual language of Icon Park Outline makes for a good pairing.
Development
For development, I chose Nuxt 3 for its delightful developer experience paired with server-side rendering (SSR) superpowers while still providing a single-page application (SPA) user experience. For the backend, I used Node.js and ExpressJS, and for data storage I use Amazon S3 and MongoDB. For deployment, I used Cyclic.sh for the backend and Netlify for the frontend.
Admin functionality - Adding a location using search or map pin
Admins can add locations to the map in two ways: using the search function or by placing a pin on the map. The code below demonstrates the second method. To convert a location description into coordinates, I used a service called Nominatim for search functionality. For extracting an address from the pin's coordinates on the map, I relied on OpenCage Data, which provides more accurate results. This feature makes it easy for users to add new locations by either searching for a specific address or interactively selecting a spot on the map.
// Place marker on map functionality
// Create a draggable marker
const marker = new Marker({
draggable: true,
});
// Initialize marker coordinates with default values
const markerCoordinates = ref([-73.99333517453792, 40.73245042300121]);
// Store coordinate details for display in the UI
const coordinateDetails = ref(null);
// Initialize the marker on the map
function initMarker() {
// Get the current center coordinates of the map
const { lng, lat } = mapStore.MAP.getCenter();
// Add the marker to the map
marker.setLngLat([lng, lat]).addTo(mapStore.MAP);
// Update the local marker coordinates
markerCoordinates.value = [lng, lat];
// Update markerCoordinates on drag end
marker.on("dragend", onDragEnd);
}
// Use forward geocoder to get the calculated address
watch(markerCoordinates.value, () => {
// Fetch address from OpenCage Data
fetch(
`https://api.opencagedata.com/geocode/v1/json?q=${markerCoordinates.value[1]}+${markerCoordinates.value[0]}&key=5e9a72c0e8f9413e928547cc630b1651`,
)
.then((res) => res.json())
.then((res) => {
coordinateDetails.value = res.results[0].formatted;
});
});
// Handle marker drag end event
function onDragEnd() {
const lngLat = marker.getLngLat();
markerCoordinates.value = [lngLat.lng, lngLat.lat];
}
Managing data and images
Location data is stored as JSON in MongoDB, and the associated images are stored in an AWS S3 Bucket. When adding or editing a location, I create a descriptive image by combining different layers using jimp. I then compress the image using imagemin before uploading it to the S3 Bucket. Once the image is uploaded successfully, I save its URL in the location JSON object and store it in MongoDB.
// composite.js
import jimp from "jimp";
import imagemin from "imagemin";
import imageminPngquant from "imagemin-pngquant";
import fs from "fs";
import path from "path";
// Function to create composite image
export async function createCompositeImage(properties) {
return new Promise((resolve, reject) => {
try {
const layers = ["layers/white bg.png"];
// Push layers according to properties, ordered by background to foreground
if (properties.size === "studio") {
layers.push("layers/bg small.png");
}
// ... Rest of the layers here
const jimps = [];
// Read all layers into jimps array
for (let i = 0; i < layers.length; i++) {
jimps.push(jimp.read(layers[i]));
}
Promise.all(jimps)
.then(function (data) {
return Promise.all(jimps);
})
.then(function (data) {
const numToComposite = layers.length;
// Subtract first image; let i = 1;
for (let i = 1; i < numToComposite; i++) {
data[0].composite(data[i], 0, 0);
}
const tempPath = path.join(
"/",
"tmp",
"temp-images",
`${properties.id}.png`,
);
const targetFolder = path.join("/", "tmp", "composite-images");
// Write the composite image to temporary path
data[0].write(tempPath, async function () {
console.log(`${properties.id} written to /tmp/temp-images/`);
// Compress the image and move it to target folder
const files = await imagemin([tempPath], {
destination: targetFolder,
plugins: [
imageminPngquant({
quality: [0.6, 0.8],
}),
],
});
console.log(`${properties.id} written to images`);
if (files) {
resolve(true);
fs.rmSync(tempPath, { force: true });
} else {
reject(false);
}
});
});
} catch (e) {
reject(e);
}
});
}
Summary
At the moment, the app is still in the testing phase and the data is being populated. The next steps for this app would include gathering user feedback through mechanisms like a basic upvote/downvote system and suggestion forms. The project could evolve into a SaaS platform, a social platform for coworkers, a closed community forum, or a featured cafe platform. Regardless of its direction, one thing will be certain: the integrity and quality of the location choices must serve the needs of its users.