itsmesonia / workdeck

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

๐Ÿ•ถ๏ธ workdeck

Welcome to Deck! A full-stack productivity app developed during our first Multiverse Bootcamp project. Deck allows the user to select a user profile and input their username before creating a project workspace. The user can also create multiple project workspaces and create different tasks unique to each project. The user can then manage the status of each task and edit them as they see fit.

๐ŸŽ’ Getting Started

Clone or download the repo

  • npm init
  • node server (to run the server)

๐ŸŽฏ Brief

Initialise a relational database for Projects, Tasks for each project, and assigned Users for each tasks. Decide on 2 critical user journeys and encode these two journeys. A project board must have at least 3 areas - todo, in progress, done

Add CRUD features for all three objects:

  • It must be possible to view all the tasks on a project board.
  • It must be possible to create a task on a project board.
  • It must be possible to delete a task on a project board.
  • It must be possible to edit a task on a project board.

A task must have textual description. A task must start in the 'todo' state. It must be possible to move tasks from one area to another. e.g. from 'todo' to 'in progress'. A task must be either unassigned or assigned to a single user. A user must have a name and an avatar (so we can recognise them). Tasks that are assigned to a user need to display the user's avatar on the task. It must be possible to create a new project board. It must be possible to view all the project boards. The application must have client & server-side validation in place.

Write test scripts in either Jest of Cypress to test out features (the application must have evidence of automated unit testing).

๐Ÿš€ Timeframe - 1 week

๐Ÿ”ฅ Team size - 3

๐Ÿ•น Main Technologies Used

Frontend & UI:

  • HTML5
  • CSS3 (Flexbox)
  • JavaScript (ES6)
  • Google fonts
  • Figma

Backend

  • Handlebars
  • Node.js
  • Express
  • SQLite3
  • Sequelize

Version Control

  • Git
  • GitHub

Testing Suite

  • Cypress

โœ”๏ธ Approach Taken

๐Ÿ’ญ Project Plan

  • Ideas and discussions, research, team brief, mockups
  • Set up (libraries, backend and frontend basic scafolding)
  • Backend functionality and database set up, testing endpoints and HTTP requests
  • Frontend UI design, research, test CRUD
  • Adding new features to both ends
  • Sanitize user inputs
  • Styling and Troubleshooting

๐Ÿ”ฎ Wireframing- Mockups Architecture

Our app comprises of 4 main pages: User, Project, Project Dashboard, and Tasks for single projects listing all tasks pertaining to the specific project.

We constructed a very basic mockup on figma to illustrate the user journey. The user must first select an avatar and confirm user name, before being able to view the next page to confirm the Project they will be able to create. Once a new project is created, they can view the project dashboard listing all created projects. The user is able to view tasks for each project upon clicking the link to view tasks and manage their statuses accordingly.

Landing Page

New User Page

New Projects Page

Project Dashboard Page

Project Tasks Page

Edit Form

๐Ÿ’Ž Sequelize - db.js, model.js, server.js

The objective was to implement a data model with Sequelize.

We first created db.js that sets up a connection to the database and imports sequelize types while establishing a filepath for the database.

const {Sequelize, DataTypes, Model} = require('sequelize');
const path = require('path');
const sequelize = new Sequelize('database', 'username', 'password', {
    dialect: 'sqlite',
    storage: path.join(__dirname, 'database.sqlite') //best practise for file paths
    
});

 module.exports={sequelize, DataTypes, Model};

๐Ÿ—บ๏ธ Defining the model in model.js

We quickly learnt models are fundamental in Sequelize and initialised 3 to represent our tables in our database. It's represented by classes that extends Model. This format only requires us to extend from the Sequelize class Model as a method of inheritance. The addition of an init method defines the table columns and their types, while the options setting sets the timestamps: false to avoid a created_at column appearing.

Below is an exerpt of the Project model:

class Project extends Model {

}

Project.init({
    name: DataTypes.STRING,
    summary: DataTypes.STRING,
}, options);

๐Ÿงฉ Sequelize relationships in model.js

To connect tables in sequelize our model definitions needed to specify their relationships in order for Sequelize to generate new foreign key columns. The below lists the relational defintions between our models:

Project.hasMany(Task)
User.hasMany(Task)
Task.belongsTo(User)
Task.belongsTo(Project)

๐Ÿ—๏ธ Handlebars File Structure

The views folder contains Handlebars templates which get rendered into layouts.

views
โ”œโ”€โ”€ projects.handlebars
โ””โ”€โ”€ allProjects.handlebars
โ””โ”€โ”€ createProject.handlebars
โ””โ”€โ”€ createTask.handlebars
โ””โ”€โ”€ createUser.handlebars
โ””โ”€โ”€ layouts
    โ””โ”€โ”€ main.handlebars

๐Ÿšง Passing the data to the views template - Handlebars

We used a templating framework called Handlebars to dynamically iterate, inject and render relevant information from our sequelize database by parsing them through handlebar's placeholder syntax {{}}.

Our first step was to prepare the data for the views template and the second step was to pass it to the views template. The example below uses sequelize to fetch all the projects and passes them to the createFirstProject.handlebars layout template. An if check is used to check if any projects already exist in the database -if true, the user will be directed to the allProjects page, else the user is sent to createProject layouts template. The second argument to response.render is the projects data returned from the sequelize query.

app.get('/', async (req, res) => {
    const projects = await Project.findAll()
   if(projects.length > 0){
    res.render('allProjects', {projects})
   } else {
    res.render('createFirstProject')
   }
})

app.get('/all-projects', async (req, res) => {
    const projects = await Project.findAll()
    if(projects.length > 0){
    res.render('allProjects', {projects})
    } else {
    res.render('createFirstProject')
    }
})

To repeat a block of code for every item in our projects array from our seqelize query, we used Handlebars's built in template helper {{#each}}. An exerpt of this is below:

  <div class="row">
    {{#each projects}}
    <a href="/projects/{{this.id}}">
    <div class="col-sm-3">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title">{{this.name}}</h5>
          <p class="card-text">{{this.summary}}</p>
          <a href="/projects/{{this.id}}" class="btn btn-primary">Open Project</a>
        </div>
      </div>
    </div>
    </a>
    {{/each}}

๐Ÿ›ค๏ธ Route parameters

As we wanted to incorporate different pages to the user journey, we needed to create dynamic links leading to different viewable pages.

In order to click on an individual project card in the project dashboard and then view that project's task page, we needed to create a dynamic link by wrapping our project card in an anchor tag. We then made the href point to a particular address on our server:

 <div class="row">
    {{#each projects}}
    <a href="/projects/{{this.id}}">
    ....
    </a>
    {{/each}}

๐Ÿ“ฒ New Route

To deal with these requests we needed to add new routes on our server. The last part of our route for this particular request, is going to be different depending on which project the user clicks on. We needed a route that also took in a route parameter:

app.get('/projects/:projectid', async (req, res) => {
    const projectID =  req.params.projectid
    const project = await Project.findByPk(projectID)
    const tasks = await project.getTasks({
        include: [User]
    })

๐Ÿ” Forms and Submitting User Inputs - CRUD operations

We created forms for our CRUD operations. The input type of 'submit' creates a button which posts the form data to the specified URL in the form's action attribute.

  <form class="row g-3" action="/create-project" method="POST">
    <div class="mb-3">
      <label for="projectname" class="form-label">Project Title</label>
      <input type="text" class="form-control" id="projectname" placeholder="Project Title" name="name">
    </div>
    <div class="mb-3">
      <label for="projectsummary" class="form-label">Description</label>
      <textarea type="text" class="form-control" id="projectsummary" rows="3" name="summary"></textarea>
      <div class="col-auto">
        <button type="submit" value="Submit" class="btn btn-primary mb-3">Add project</button>
      </div>
    </div>
  </form>

To wire this up on our server side and handle the post request sucessfully, we needed a matching route in our server.js file to store the new project on our sequelize database:

app.post('/create-project', async (req, res) => {
    const project = await Project.create({
        name: req.body.name,
        summary: req.body.summary,
    })
    res.redirect(`/projects/${(project.id)}`)
})

The above code allows the server to receive and process the form data. We first specified the form data will be 'posted' to the /create-project route, which is why we needed to create a new route matching the exact URL path.

We repeated similar steps for our app to allow users to delete tasks. On our server we defined a new route and used that to perform the delete operation.

app.get('/:id/delete', async (req, res) => {
    const id =  req.params.id
    const task = await Task.findByPk(req.params.id)
    const projectID= task.ProjectId
    await task.destroy()
    res.redirect(`/projects/${projectID}`)
})

We needed a new edit route for users to also be able to update their tasks, but there was more involved in this process. We needed to provide the user with the form to populate with the current values. We then needed a new update route to post the new values to (the post request).


app.get('/:id/edit', async (req, res) => {
    const task = await Task.findByPk(req.params.id)
    const users= await User.findAll()
    res.render('editTask', {task, users})
})

app.post('/:id/edit', async (req, res) => {
    const task = await Task.findByPk(req.params.id)
    await task.update(req.body)
    res.redirect(`/projects/${task.ProjectId}`)
})

๐Ÿ’ก Drag and Drop Feature - projectBoard.js

In order to have a visual element to our task status management, we used the event listeners, ondragstart, ondrop, and the html attribute draggable to support our drag and drop feature from projectBoard.js. Below is an exerpt to identify the change in column ids depending on the position of the task the user is dragging the task between the three presented columns. We performed an async patch request using ajax to update and store on the server side with the change in column id.

const updateColumn = await fetch(`/${draggable.id}/updatecolumn`, {
    method: 'PATCH',
    headers: {
        'Content-Type': 'application/json'
      },
    body: JSON.stringify({
    column: column.id,   
    taskId: draggable.id 
    
    })  
  })
  .then(response =>  {
      const res = response.json()    
})
.then(res => console.log(res))
}  

๐Ÿง Challenges and Key Learnings

Relational data mapping felt like a challenge with sequelize and using foreign keys to make associations on the server side.

๐Ÿ”ฎ Future Features

Adopting mobile first responsive design and using JWT Auth for serverside user authentication.

๐Ÿ† Wins

Working as a team and successfully wiring up the submit requests on the frontend and establishing route handlers to update the database felt like a sucessful win. Iterating through 3 different columns and storing the positions of the tasks dependent on the draggable status in the server was also satisfying.

About


Languages

Language:JavaScript 40.6%Language:Handlebars 36.3%Language:CSS 23.1%