First of all, welcome to Wisemen! We are happy to have you here and we hope you will have a great time working with us.

In this onboarding you will learn how frontend development happens at Wisemen. You will learn how to work with Vue, Vite, Tailwind, Figma, Github, Jira and more.

This onboarding is designed to be completed in roughly 3-4 days. This does not mean you have to complete it in 3-4 days. People with more experience will be able to complete it faster than people with less experience.

In this codelab we are going to create a simple to-do app. This app will be used as example to teach you how we structure our projects, which tools and libraries we use.

We also expect you to make pull request of your work so your buddy can review your code and keep track of your progress. The way we do this will be explained in the onboarding.

Good luck on becoming the front-end developer you are meant to be!

IDE

There are 2 different IDE's you can use to work with Vue. You can use either Visual Studio Code or WebStorm. The Choice is yours, so choose wisely.

WebStorm

WebStorm is a JavaScript IDE with complete set of tools for client-side and server-side development and testing. It provides code completion, on-the-fly error detection, powerful navigation and refactoring for JavaScript, TypeScript, CSS, HTML and more.

Webstorm is a paid IDE. You can get a license from Wisemen. Ask your buddy!

Download WebStorm

Handy Plugins:

Visual Studio Code

Visual Studio Code is a source-code editor developed by Microsoft for Windows, Linux and macOS. It includes support for debugging, syntax highlighting, intelligent code completion, snippets, and code refactoring.

Visual Studio Code is a free IDE. You can download it from the website.

Download vscode

Handy Plugins:

Choose the IDE you want to work with and download it.

Node.js

Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser.

Node.js is necessary to run the Vue project. You can download it from the website.

Download Node.js

NPM vs PNPM

The difference between NPM and PNPM is that PNPM uses symlinks to link packages to your project. This means that if you have multiple projects that use the same package, it will only be installed once on your computer. This saves a lot of disk space. PNPM is also faster than NPM because it uses symlinks.

Figma

Our designers work with Figma. Figma is a vector graphics editor and prototyping tool which is browser-based or can be installed on macOS or Windows. We recommend you to install the desktop app.

Download Figma

Take a look around in Figma and try to get familiar with the tool. You will be using it a lot in the future. You can view all of our designs here:

Wisemen Figma

To access the designs you need to log in with your Wisemen account: wireframes

Source control

BitBucket repository

Bitbucket is a web-based version control repository hosting service owned by Atlassian, for source code and development projects that use the Git revision control system.

Some of our older projects are still hosted on BitBucket.

Wisemen BitBucket

If you are not yet familiar with Bitbucket and/or Git, Here is great article to get you started: Bitbucket Git tutorial

We also expect you to make pull request of your work so your buddy can review your code and keep track of your progress. In the article above you can find a section about pull requests to get you started!

GitHub

GitHub is another web-based version control repository hosting service owned by Microsoft, for source code and development projects that use the Git revision control system.

Just like BitBucket, some of our projects will be hosted on GitHub. GitHub will be used for new projects.

If you are not yet familiar with GitHub, Here is great article to get you started: GitHub Git tutorial

Same as with BitBucket, we expect you to make pull request of your work so your buddy can review your code and keep track of your progress.

Jira access

For this onboarding you will be working with Jira to track your progress. You can find the Jira board here: Jira Todo

Jira is used to track the progress of your project and manage the tasks that need to be done. All the requirements for the to-do app are in the Jira. You will be creating tasks in the Jira to keep track of your progress.

The Jira contains all the requirements for creating the to-do app.

ToDo: Add link to Jira

You will be creating a simple to-do app. The app can be used to create, edit and delete to-do's. The backend is already created and you can find the documentation here:

Backend documentation

Username: appwise
Password: password

Requirements

Designs

The designs for the to-do app can be found in Figma. Login with your Wisemen google account to view the designs. You can find the designs here:

Figma designs

1. A Vue3 project

We use the latest version of Vue for this project. Vue3 is the latest version and has some new features and improvements over Vue2. You can read more about Vue3 here: Vue3 website Make sure you use the CLI version to create the project.

Create new project using the Vue CLI:

pnpm create vue@latest

Use the following settings:

2. Vite

Vite is a new breed of frontend build tool that significantly improves the frontend development experience. It consists of two major parts:

2.1 Config

Vite is configured using a vite.config.js file in the root of your project. This file is written in CommonJS format and should export a plain JavaScript object. Make sure to change this file from .js to .ts

2.2 Plugins

Vite supports a plugin system that allows you to customize the behavior of Vite itself and integrate with other tools. Plugins can be configured in the vite.config.js file.

3. Package.json

The package.json file is used to give information to pnpm that allows it to identify the project as well as handle the project's dependencies. pnpm can install the packages you specify in your package.json file. The main use of the package.json file is to list the packages that your project depends on and to ensure that your colleagues get the same packages when they do pnpm install.

3.1 Scripts

The scripts property is used to specify a list of scripts that can be run using npm run . It's written as a JSON object where each key is the name of a script and the value is the command to run for. Most common scripts are start and build.

3.2 Dependencies vs Dev Dependencies

Dev dependencies are dependencies that are only used during development and are not required for production. Dependencies are required for production.

4. Tailwind CSS

Tailwind CSS is a utility-first CSS framework for rapidly building custom user interfaces. It's completely customizable, completely extensible, and amazingly feature-rich.

4.1 Tailwind config

Tailwinds config file is used to configure the framework. You can add custom colors, fonts, breakpoints and more. This is where you can customize the framework to your needs.

👉 Tailwind website

5. ESLint config

ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code, with the goal of making code more consistent and avoiding bugs. Is helps a lot with code formatting and makes it easier to write code. Also in team projects it helps to keep the code consistent.

6. Internationalization (i18n)

i18n is a short name for internationalization. It is a process of designing and developing a software application so that it can be adapted to various languages and regions without engineering changes.

Within the company we use Vue i18n to translate our applications. It's important to understand the power of this tool since it will save you a lot of time when creating multilingual applications.

7. What the Typescript

TypeScript is a tool that helps developers write code with fewer bugs. TypeScript is a superset of JavaScript, meaning any valid JavaScript code is also valid TypeScript code. It helps a lot with type checking and makes it easier to write code.

7.1 Typescript config

The TypeScript config file is used to configure the TypeScript compiler. You can add custom types, change the compiler options and more.

8. ⚠️ Google fonts ⚠️

It's important to know that we cannot use Google fonts CDN in our projects. This rule is only for public websites of our clients. We have alternative ways of using Google fonts in our projects.

9. @ Alias for src folder

In our vue imports, we can use the @ alias to import files from the src folder. This is a lot easier than using relative paths. For example:

import {Button} from '@/components

instead of:

import {Button} from '../../components'

This alias is configured in the vite.config.js file. Maybe do a little research about how you can configure your vite environment to accept the usage of this alias.

10. Important files

10.1 .env

The .env file is used to store environment variables. These variables can be used in your application. It is mainly used to separate development and production variables. For example, you can use a different API url in development than in production. Most of our projects have 3 different .env files: .env.development, .env.staging and .env.production. Locally you can override these variables by creating a .env.local file. This file will be ignored by git.

⚠️ Using an .env is not required for this project.

10.2 .gitignore

The .gitignore file is used to tell git which files it should ignore. For example, you don't want to commit your node_modules folder to git. This file is used to tell git to ignore this folder.

That's it for now! 🎉 Soak it all in and let's get started with the project setup! 🚀

Folder structure

For this project we will be using a ‘split-by-module' folder structure. Although ‘split-by-module' is mainly recommended for medium to large applications, we will still use it here. As you won't be working on small applications for long 😉;

You can read more about it here: Folder structure

Assets

Assets are files that are used throughout your application. This can be images, fonts, icons, etc.

Components

Components are the building blocks of Vue.js applications. They are self-contained pieces of code that can be reused throughout your application.

You can read more about it here: Components

Composable

Composable look like a util function, but the main difference is that they can contain state and leverage the reactivity of Vue.js.

You can read more about it here: Composables

Configs

Configs are used to store configuration values for plugins/packages that are used throughout your application.

Constants

Constants are used to store hardcoded values that are used throughout your application.

Icons

Icons are used to store the icons that are used throughout your application.

Libs

Libs are used to store the libraries that are used throughout your application.

Middlewares

Middlewares acts like a bridge between the backend and the frontend. They are used to transform the data that is received from the backend or route protection.

Models

Models are used to store the types and interfaces that are used throughout your application.

Modules

Modules are collections of components, views, stores, services, etc. that are used to create a specific feature of your application. Here is a list of some important folders inside the modules folder:

Plugins

Plugins are used to add functionality to your Vue.js application. They can be used to add third-party libraries, add global components, etc.

Routes

The routes is the core of Vue.js applications. It is used to navigate between different views.

You can read more about it here:

Stores

Stores are used to store the state of your application. This is useful when you want to share data between different components. This folder is only for global stores, if you have a store that is only used in a specific component, you should store it inside the component folder. only global stores should be located in this folder.

You can read more about it here:

Transformers

Transformers are used to transform the data that is received from the backend. This is useful when you want to transform the data into a format that is easier to work with. This should be a single file per module.

Transitions

Transitions are used to add animations to your application. This is useful when you want to add a smooth transition between different views or components.

Utils

Utils are reusable pieces of code that can be throughout your application. They contain no state and are not tied to a specific component.

You can read more about it here: Utils

Views

Views are the pages of your application. They are the components that are rendered when a specific route is visited.

You can read more about it here: Views

Worth mentioning

Queries & Mutations (Tanstack)

For fetching data from the backend we use Vue Query. Vue Query is a Vue plugin that makes it easy to fetch, cache and update asynchronous data in your components without the hassle of setting up a dedicated global store.

We also use their mutations to update data in the backend.

You can read more about it here:

Services & Http

Services are used to fetch data from the backend. These backend calls are made using the Http client.

You can read more about it here:

Internationalization (i18n)

Locales are used to store the translations of your application. This is useful when you want to support multiple languages.

You can read more about it here: Locales

Types & Interfaces

At Wisemen we use Typescript to type all of our code. This is useful when you want to make sure that your code is correct and leverage the power of intellisense. It will also help you to avoid bugs, improve your code quality and make your code more readable.

Lastly, your team will be able to understand your code better and don't have to make assumptions about the code.

Consult the front-end bible to find out more about types and interfaces. For example

Now that we have a basic understanding of the project structure and the different kinds of elements that a frontend should contain, let's get started with building the actual application.

The most important aspect of programming is separation of concerns. and DRY (Don't Repeat Yourself). This means that you should separate your code into different layers and files. This will make your code more readable, reusable and easier to maintain.

You can easily do your calls in the component itself, but this will make your component less readable and harder to maintain in the future.

That's why we will start with creating a service that will be used to send the login request to the backend.

Environment variables

To make sure that we don't hardcode the base url of the backend in our service, we will use environment variables.

VITE_BASE_URL=https://onboarding-todo-api.development.appwi.se/api/v1
VITE_CLIENT_ID=ENTER_YOUR_CLIENT_ID_HERE
VITE_CLIENT_SECRET=ENTER_YOUR_CLIENT

Creating the HTTP client

const httpClient: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json;charset=UTF-8',
  },
})

Creating the auth service

interface AuthService {
  login: (username: string, password: string) => Promise<void>
  getCurrentUser: () => Promise<CurrentUser>
}

export const authService: AuthService = {
  login: async (username: string, password: string): Promise<AuthTokens> => {
    const formData = encodeQueryData({
      client_id: import.meta.env.VITE_CLIENT_ID,
      client_secret: import.meta.env.VITE_CLIENT_SECRET,
      grant_type: 'password',
      password: password,
      username: username,
      scope: "read write"
    })

    const config = {
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    }
    
    const response = await httpClient.post('/auth/token', formData, config)
    return response.data
  },
  getCurrentUser: async (): Promise<CurrentUser> => {
    const response = await httpClient.get('/users/me')
    return response.data
  },
}

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

Creating the auth store

The store will help us to save the tokens after a successful login. That's why we will always use a store to do our backend calls and never directly use the service in the component. (separation of concerns)

export const useAuthStore = defineStore('auth', () => {
  const currentUser = ref<User | null>(null)
  const accessToken = useLocalStorage<string | null>(null)
  
  const isAuthenticated = computed<boolean>(() => currentUser.value === null)
  
  async function getCurrentUser(): Promise<User> {
    if (currentUser.value !== null) {
      return currentUser.value
    }
    
    currentUser.value = authService.getCurrentUser()
    return currentUser.value!
  }
  
  function setCurrentUser(user: User | null): void {
    currentUser.value = user
  }
  
  async function login(data: AuthLoginForm): Promise<void> {
    const response = await authService.login(data.username, data.password)
    accessToken.value = response.accessToken
  }
  
  function logout(): void {
    authService.logout()
    setCurrentUser(null)
  }
  
  return {
    currentUser,
	isAuthenticated,
	getCurrentUser,
	setCurrentUser,
	login,
	logout,
  }
})

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

The router is the core of Vue.js applications. It is used to navigate between different views. It is also used to handle authentication and permissions for specific routes using "guards". This is useful when you want to protect a route from being accessed by unauthenticated users.

Creating the router

const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'login',
    component: async () => import('@/modules/auth/views/AuthLoginView.vue'),
  },
  {
    path: '/',
    name: 'index',
    meta: { requiresAuth: true },
    children: [
      {
        path: '/todos',
        name: 'todos',
      },
    ],
  },
  {
    name: 'error',
    path: '/:pathMatch(.*)*',
    component: async () => import('@/views/ErrorNotFoundView.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes: routes
})

Router guards

router.beforeEach(async (to, from, next) => {
  // Add your guards here
  
  next()
})

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

Now that we have created the store, service and router, we can start with creating the login view. Views are the "Smart components" in our application. They are allowed to import stores, routers, dumb components, etc.

Our Login view will orchestrate the login flow. It will use the authStore to login the user and the router to navigate to the TodoOverviewView after a successful login.

Creating your Login view (smart) component

Implementing the login flow

<script setup lang="ts">
const authStore = useAuthStore()
const router = useRouter()
  
async function handleLogin(data: { username: string; password: string }): Promise<void> {
  await authStore.login(data)
  router.push({ name: 'todos' })
}
</script>

<template>
<div>
    <AuthLoginForm @submit="handleLogin" />
</div>
</template>

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

Now that we have created the login flow, we can start with creating the todo view. After completing the login functionality, you should now have a good understanding of how we're going to create the todo view.

Model

We are going to start by creating a file called todo.model.ts in the src/modules/todos/models folder.

This file will contain an interface that will represent a single todo with the following properties:

export interface Todo {
  uuid: string
  title: string
  description: string
  deadline: string
  isCompleted: boolean
}

Service

After creating the model that we want to use in our frontend, we are going to create a file ‘todo.service.ts' in the src/modules/todos/services folder.

This service will contain a function TodoService that returns another function called getAll.

interface TodoService {
  getAll: () => Promise<Todo[]>
}

export const todoService: TodoService = {
  getAll: async (): Promise<Todo[]> => {
    const response = await httpClient.get('/todos')
    return response.items
  },
}

Query

Next up we are going to create a query called useTodoIndexQuery. This query will be used to call the getAll function from our service and fetch the todos from the backend.

The reason we use queries is so that we can easily fetch, cache and update asynchronous data in our components without the hassle of setting up a dedicated global store.

export function useTodoIndexQuery() {
  return useQuery({
    queryKey: 'todos',
    queryFn: async () => {
      const data = await todoService.getAll()
      return data
    },
  })
}

List component

Once we have created the query, we can start with creating a list component that will be used to display the todo's.

<script setup lang="ts">
const props = defineProps<{
  todos: Todo[]
  isLoading: boolean
}>()
</script>

<template>
<div>
  <div v-if="props.todos.length > 0">
    <ul>
      <li v-for="todo in props.todos" :key="todo.uuid">
        {{ todo.title }}
      </li>
    </ul>
  </div>
  <p v-else> No todo's found </p>
  <p v-if="props.isLoading">Loading...</p>
</div>
</template>

View

The last step is to combine all of our pieces in a (smart) component that will be used to display the todo's. This file should be named TodoOverviewView and we will put this in our src/modules/todos/views folder.

This view will combine the query and list component we have created before.

<script setup lang="ts">
import { useTodoIndexQuery } from '@/modules/todos/services/todoIndex.query'

const { data: todos, isLoading } = useTodoIndexQuery()
</script>

<template>
<div>
    <TodoList :todos="todos" :is-loading="isLoading" />
</div>
</template>

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

Now that we have created the todo view and have a list of our existing todo's, we can start with creating new todo's.

The creation of a todo will be done in a modal. This modal will be displayed when the user clicks on the Create todo button. Modals are allowed to be smart components. The modal will contain a form that allows to enter the required information for creating a new todo.

Form model

We are going to start by creating a file called todoForm.model.ts in the src/modules/todos/models folder.

This file will contain a form schema that will be used to create a new todo

export const todoFormSchema = z.object({
  title: z.string(),
  description: z.string(),
  deadline: z.string(),
})

export type TodoForm = z.infer<typeof formSchema>

Service

After creating the model that we want to add a new function to our existing service that will be used to create a new todo.

interface TodoService {
  ...
  create: (form: TodoForm) => Promise<void>
}

export const todoService: TodoService = {
  ...
  create: async (form: TodoForm): Promise<void> => {
    await httpClient.post('/todos', form)
  },
}

Mutation

Next up we are going to create a mutation called useTodoCreateMutation.

This mutation will be used to call the create function from our service and create a new todo in the backend.

export function useTodoCreateMutation() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationKey: 'createTodo',
    mutationFn: async (form: TodoForm) => {
      await todoService.create(form)
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries('todos')
    },
  })
}

Modal (smart) component

Once we have created the mutation, we can start with creating a modal component that will be used to create the todo's.

<script setup lang="ts">
import { useForm } from 'formango' 
const todoCreateMutation = useTodoCreateMutation()

const { onSubmitForm, form } = useForm({
  schema: todoFormSchema,
  initialState: {
    title: '',
    description: '',
    deadline: '',
  },
})

const title = form.register('title')

function onSubmit(): void {
  form.submit()
}

onSubmitForm(async (formData: TodoCreateForm) => {
  try {
    await todoCreateMutation.mutateAsync(formData) // notice the async keyword here, it's very important
  } catch (error) {
    console.error(error)
  }
})
</script>

<template>
<form @submit.prevent="onSubmit">
    <AppInput v-bind="title" />
    <button type="submit">Submit</button>
</form>

Example of a custom input component:

<script setup lang="ts">
const props = defineProps<{
  isDisabled?: boolean
  placeholder?: string
}>()

const emit = defineEmits<{
  blur: []
}>()

const model = defineModel<string | null>() // New macro to define a model https://vuejs.org/guide/components/v-model.html

</script>

<template>
  <div>
    <input
        v-model="model"
        :disabled="props.isDisabled"
        :placeholder="props.placeholder"
        @blur="() => emit('blur')"
    />
  </div>
</template>

View

To finish up, we are going to update our TodoOverviewView by adding a button that will open the TodoModal when clicked.

<script setup lang="ts">
...
const isModalOpen = ref<boolean>(false) 
...
</script>

<template>
<div>
    <TodoList :todos="todos" :is-loading="isLoading" />
    <button @click="onCreateButtonClick">Create todo</button>
    <TodoModal v-if="isModalOpen" @close="handleClose" />
</div>
</template>

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

The last step is to allow the user to update and delete existing todo's. This will be done by clicking on the edit button of a todo. We are going to extend the functionality of the TodoModal component to allow the user to update a todo. To achieve this, we need to know if the modal is opened in create or update mode. The easiest way to do this is to check if a todo uuid is passed to the modal.

Service

Now it's time to add a new function to our existing service that will be used to update a todo.

interface TodoService {
  update: (uuid: TodoUuid, form: TodoForm) => Promise<void>
  deleteByUuid: (uuid: TodoUuid) => Promise<void>
}

export const todoService: TodoService = {
  update: async (uuid: TodoUuid, form: TodoForm): Promise<void> => {
    await httpClient.post(`/todos/${uuid}`, form)
  },
  deleteByUuid: async (uuid: TodoUuid): Promise<void> => {
    await httpClient.delete(`/todos/${uuid}`)
  },
}

Mutation

Once we have added the update and deleteByUuid functions to our service, we can start with creating the mutations.

export function useTodoUpdateMutation() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationKey: 'updateTodo',
    mutationFn: async (uuid: TodoUuid, form: TodoForm) => {
      await todoService.update(uuid, form)
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries('todos')
    },
  })
}

Modal (smart) component

Now it's time to extend the functionality of the TodoModal component to allow the user to update a todo.

<script setup lang="ts">

const props = defineProps<{
  todo: Todo | null
}>()

const updateMutation = useTodoUpdateMutation()
const deleteMutation = useTodoDeleteMutation()

const { onSubmitForm, form } = useForm({
  schema: todoFormSchema,
  initialValues: {
    title: props.todo?.title || '',
    description: props.todo?.description || '',
    deadline: props.todo?.deadline || '',
  },
})

const title = form.register('title')
...

function onSubmit(): void {
  form.submit()
}

onSubmitForm(async (formData: TodoForm) => {
  try {
    if (props.todo) {
      await updateMutation.mutateAsync(props.todo.uuid, formData)
    } else {
      await createMutation.mutateAsnyc(formData)
    }
  } catch (error) {
    console.error(error)
  }
})

function handleDelete(uuid: TodoUuid): void {
  deleteMutation.mutateAsync(uuid)
}

</script>

<template>
<div>
    <form @submit.prevent="onSubmit">
        <input v-model="title.value" />
        ...
        <button type="submit">Submit</button>
    </form>
    <button @click="handleDelete(props.uuid)">Delete</button>
</div>
</template>

💡Don't forget to make a pull request of your work so your buddy can review your code and keep track of your progress. Keeping your PR's small and frequent is a good practice.

Congratulations! You have successfully completed our Vue.js workshop. 🥳🤩

Make sure that your project has been pushed to your repository and that you have created a pull request. Fix any remarks that you have received from your mentor and wait for the final feedback.

Also make sure your styling is consistent with the designs and adjust where necessary.