DEV Community

Cover image for Workdrop — The Frontend
Peyton McGinnis
Peyton McGinnis

Posted on • Edited on

Workdrop — The Frontend

This continues my entry for #twiliohackathon!

GitHub Repository (MIT Licensed)
Stackshare


In a previous post, Workdrop — UI Design and Prototyping, I went over my project's design system and some basic UI elements. This post will summarize the actual frontend, built with Nuxt.

Nuxt setup

Usually, I use Gridsome for my Vue SPA's, but it seems like Nuxt is much more oriented towards dynamic applications that are constantly interacting with a backend.

When creating my project, I did not use SSR mode because I wanted to host my site on Netlify. I suppose using SSR would reduce client bundle size, but for now it'll stay an SPA.

nuxt.config.js

Nuxt provides a really nice config file for configuring <head> contents, plugins, middleware, routing, and other build settings.

I inserted some custom <meta> tags for OpenGraph tag support and some other service integration for PWAs.

// nuxt.config.js

export default {
  // ...

  head: {
    title: process.env.npm_package_name || '',
    meta: [
      // ...
      {
        name: 'apple-mobile-web-app-status-bar-style',
        content: 'black-translucent',
      },

      { name: 'twitter:card', content: 'workdrop' },
      { name: 'twitter:url', content: 'https://www.workdrop.app/' },
      { name: 'twitter:title', content: 'workdrop' },
      {
        name: 'twitter:description',
        content: 'An assignment requesting app for teachers and educators.',
      },

      { property: 'og:title', content: 'workdrop' },
      { property: 'og:type', content: 'website' },
      { property: 'og:url', content: 'https://www.workdrop.app/' },
      { property: 'og:image', content: 'https://www.workdrop.app/ogimage.png' },
  },

  // ...
}

Plugins

For error tracking, I'm using Sentry. All you have to do to add Sentry integration to your Nuxt project is install @nuxtjs/sentry and add it to your modules and set your Sentry DSN:

// nuxt.config.js

{
  // ...

  modules: [
    // ...
    '@nuxtjs/sentry'
    // ...
  ],

  sentry: {
    DSN: '[Your DSN]'
  },
  // ...
}

Tailwind and PurgeCSS

When creating a new Nuxt project, you can choose to automatically setup TailwindCSS and PurgeCSS, which go together like bread and butter.

However, global styling rules can be slightly frustrating to configure since PurgeCSS will automatically remove CSS that it doesn't think is being used.

To circumvent this, I added a donotpurge.css (appropriately named) stylesheet that is loaded along with the ignored assets loaded with Tailwind:

/* tailwind.css */

/* purgecss start ignore */
@import 'tailwindcss/base';
@import '~/assets/css/donotpurge.css';
@import 'tailwindcss/components';
/* purgecss end ignore */
@import 'tailwindcss/utilities';

The Design

In my earlier post I discussed the basics of the design system, but did not disclose the full UI. Well, here it is!

Alt Text

Now, onto the actual implementation .

Navigation

For desktop navigation, it's a pretty simple Navbar with a little stylish border animation:

Alt Text

For mobile, I normally like to implement a fullscreen navigation menu to make the links larger and easier to tap. I right-justified the text since most people are right handed to make it easier to reach.

Alt Text

Also, I really considered some of the details in the simplicity of my app, considering the target audience. In this project, I tried to move away from non-labeled buttons for the most part, so rather than using a hamburger icon to open the menu it simply says "MENU", which obviates its function. I actually am considering doing this with all my projects from now on.

Alt Text

The footer is very basic as well:

Alt Text

Landing Page

For the landing page, I am using an illustration from isometric.online as mentioned in my previous post. I customized the colors to fit the design system.

Alt Text

I wanted to get my users up and running ASAP, so the "Request an Assignment" button takes you to the request form without needing to sign in.

About page

I really enjoyed laying out this page's content. Since it doesn't require a lot of interaction, I had a lot more creative freedom.

Alt Text

Request page

This page was very interesting to design and program.

The form is split into four parts, and each part requires one specific piece of information. This way, it's clear each step of the way what is needed and reduces mental overhead.

In the code, it's a bit hacky, but I used a dynamic Vue component. To transition between each part of the form, each form emits a continue or back event. This calls a method that increments a counter and changes the dynamic component to the step of the form that the counter is on.

<component
  :is="currentFormSection"
  @continue="nextStep"
  @back="previousStep"
></component>
const FORM_STEPS = [
  'RequestFormAssignmentName',
  'RequestFormStudents',
  'RequestFormMessage',
  'RequestFormEmail',
  'RequestFormReview',
]

export default {
  // ...
  computed: {
    currentFormSection() {
      return FORM_STEPS[this.currentStep]
    }
  }
  // ...
}

I really want to refactor this to use a state machine library such as XState, but for the time being it works well.

Form errors

Whenever a field is empty of invalid, such as emails, it opens my custom toast notification through a reference.

<toast ref="errorToast" title="Uh oh!" icon="error">
  We couldn't create the assignment. Refresh and try again.
</toast>
this.$refs.errorToast.open()

Form data

Since the form switches between components, it was obvious that Vuex would be needed as a centralized store. The Vuex module is very straightforward:

// store/request.js

export const state = () => ({
  assignmentName: '',
  students: [{ email: '', valid: false }],
  message: '',
  email: '',
})

export const mutations = {
  addStudent(state, email) {
    state.students.push({ email: '', valid: false })
  },
  editStudent(state, { index, newEmail }) {
    state.students[index].email = newEmail
  },
  setStudentValid(state, { index, valid }) {
    state.students[index].valid = valid
  },
  removeStudent(state, { index }) {
    state.students.splice(index, 1)
  },
  setAssignmentName(state, assignmentName) {
    state.assignmentName = assignmentName
  },
  setEmail(state, email) {
    state.email = email
  },
  setMessage(state, message) {
    state.message = message
  },
  clear(state) {
    state.assignmentName = ''
    state.students = [{ email: '', valid: false }]
    state.message = ''
    state.email = ''
  },
}

Email Validation Microinteraction

A few weeks ago, I found a very nice email validation microinteraction from dribbble that had been converted into an actual CSS keyframe transition.

I took the code and converted it into a Vue component, and thought this would be a great opportunity to use it!

Email validation
From Aaron Iker on dribbble

Submit page

The submit page has two possible states: accessing and submitting. The state depends on the provided queries in the URL. Currently, the solution is pretty ugly, but it works.

When accessing submissions, the assigner has the capability to individually download each submission or download them all simultaneously. I plan to integrate zip.js or a similar library to compress the downloads when downloading them all.

When submitting, I used FilePond to easily integrate a nice file uploading component in my page. When a file is submitted, it gets the AwsService from MongoDB Stitch and calls PutObject on the file object.

Alt Text

However (and this had me stuck for a couple days), when using Stitch you have to convert the file to a specific binary type using MongoDB's BSON type by first converting an ArrayBuffer from the file's contents to a UInt8Array:

// pages/submit.vue

const reader = new FileReader()

reader.onload = (e) => {
  const fileData = new Uint8Array(reader.result)
  const fileBson = new BSON.Binary(fileData)

  // upload to S3
  // ...
}

reader.readAsArrayBuffer(file)

The Logic

So now that I've detailed the design, here's a high-level layout of the entire application's flow:

Alt Text

Thank you for reading! The next post will be the official submission. God bless!

Top comments (0)