This is the repository for the Harvard's CS50w Final Project
Pomo.do is a pomodoro timer which divides productivity into spans of 25 minutes and helps keep track of projects, tasks, and subtasks. It is built with Vue 3 and Django.
I often struggle to keep focused on one thing and getting it done. Some of the things that help me with this are To-Do lists and Pomodoro timers, so I decided to make this app that combines both of them.
I separated my app into a server REST API (Django REST Framework) and a front-end client (Vue 3 + Vite).
I wanted to use Django as an API instead of the MVC model, so I decided to challenge myself and created the REST API with Django REST Framework, with the goal of learning common practices and concepts such as: serializers, pagination, responses, auth via tokens. DRF's specifics too such as: viewsets, and APIViews.
Furthermore I wanted to make my project different from the others in this course mainly by using Vue 3 in order to practice using a front-end framework and learn the commonly used concepts like props, two-way data binding and separation of concerns in components, as well as composables. I found this experience to be a refreshing one and it allowed me to see and learn a new way of organizing parts of the app into a better and cleaner structure.
Inside the client,
I also used Vite which provides a dev server and a build tool for modern web projects.
In addition, I used JWT tokens for authentication, which I later explain in this file.
The directory for my main app. Contains all the boilerplate files generated by Django plus the extra file of serializers.py
that belongs to Django Rest Framework.
π
admin.py
The default file created by Django. I registered my models here in order to access them later on the Admin panel.
π
models.py
I created the following models in here:
-
Tag
A tag for assigning a task, to quickly organize them. A task can have a max of 3 tags, this is controlled by the client.
user
(foreign key)name
-
User
Extends the
AbstractUser
model fromdjango.contrib.auth.models
with the following extra fields:current_task_id
The id of the task the logged in user is currently working in. The client sets this id clicking on a created task. If the id is not set, it defaults to 0.current_mode_id
The id of the mode the logged in user has currently active, if it has one. If the mode is empty, it defaults to 0.auto_start_pomos
Part of the pomodoro timer settings, when True it doesn't require the user to start the pomodoro timer manually when the break is over.auto_start_breaks
Part of the pomodoro timer settings, when True it doesn't require the user to start the break timer manually when the pomodoro timer is over
-
Task
A task is the basic block for keeping track of what has to be done. Tasks can have subtasks inside them which are used to keep track of specific things to be done inside the task.
user
(foreign key)title
description
estimated
The pomodoro timer count estimated to finish the task.gone_through
The pomodoro timer count the task has gone through.tags
The tags the task has. This is a many-to-many field so we can access the tasks inside the tag when clicking on a tag on the client.done
(the state of the task)in_project
Controls if the task is created inside a project. This helps us list independent tasks that don't belong to projects in the client.
-
Subtask
A subtask are used primarily on tasks. They are a smaller unit used to keep track of fine grained things to do inside a task.
task
(foreign key)title
description
done
(the state of the subtask)
-
Project
A project's purpose is to encapsulate and contain multiple tasks for an easier task tracking.
name
user
(foreign key)tasks
This is a many-to-many field so that we can have multiple tasks on different projects. It can be blank so that we can a project without any tasks inside it.
-
Stats
The logged in user stats. When a user finishes a pomodoro timer with a current working task active, the
chores_done
for the current day and thegone_through
field for the active working task increase.day
The current day, defaults totimezone.now
and accepts the format ofyyyy-mm-dd
chores_done
The number of chores done in the current day
-
Mode
A customized mode that the user can create. Each user can create a max of 3 modes, this is controlled by the client in the settings.
user
(foreign key)name
The name of the mode e.g. "School classes" or "Short pomo"pomo
The length of the Pomodoro timer defaults to 25 minutes.short_break
The length of the short break timer defaults to 5 minutes.long_break
The length of the long break timer defaults to 15 minutes.
π
serializers.py
This file is required for the Django Rest Framework. Its purpose is to serialize the data in our models. They allow querysets to be converted to native Python datatypes that can be easily be rendered to JSON.
They also allow parsed data from a request to be converted back into a model object and be saved after being validated.
In the classes inside this file I inherited from ModelSerializer
which makes it easier to make a serializer for each of our models, we just need to specify inside the serializer in the class Meta
, the model it will be working with and the fields we want to serialize.
Most of the serializers do the same, except from the following two:
-
In
TaskSerializer
I included an extra option calleddepth
with a value of 1, this allow us to access the data inside the subtasks. -
In
ProjectSerializer
I overrode theget_fields
function to serialize the data inside thetasks
field with theTaskSerializer
. Inside theMeta
class I included the extra option ofdepth
with a value of 2 to access the tags and tasks info inside the project.
π
test_api.py
This file testes the API endpoints of my app. It uses:
- The
Client
class provided bydjango.test
to call the API endpoints - The models contained in my app, and tests them against the responses returned by the endpoint.
- The
serializers
module to compared the response's.json()
data to the serialized data. - The
AuthUtils
helper class to auth in thesetUp()
function of some Test Cases. - The
status
module inside therest_framework
to manage the returned responses status.
π
test_models.py
This file mainly tests the correct functionality of the models inside my app. Their correct creation with the correct data and existence inside each other with the Foreign Key and Many to Many fields.
π
urls.py
The urls of my api application. Here I imported the DefaultRouter
from rest_framework.routers
to register the ViewSets created on the views.py
file which I will later explain.
api/
matching
I prefixed all my routes inside the api app with the route of api/
inside the main/urls.py
file. So when the user visits the route of api/<matching-route>
it routes them to the route inside the the api app.
Then in the urlpatterns
list, inside api/urls.py
, I included the urls registered inside the router variable with the include()
function with the route of ''
so when the user makes a request to the url of api/tasks/
the DefaultRouter of Django Rest Framework controls the route and calls the appropriate ViewSet, in this case, the TaskViewSet
. If the makes a request to api/tags/
the router will handle it and call the appropriate ViewSet, the TagViewSet
, and so on.
After this, I created paths for
- The Current task
- The Current mode
- The Information of a tag
- Registering an user
- Retrieving the current user info
I included the documentation for each of this routes inside the views.py
file.
JWT Auth
I'm using JWT based authentication using the djangorestframework-simplejwt
pip package.
Login Process
To login the user has to make a request to the token/
route with the user credentials, this route will return a JSON object with the refresh
and access
keys. This tokens will be stored in the localStorage
of the client under the jwt
key.
// Request
{
"username": "icarusgk",
"password": "1234"
}
// Response
{
"refresh": "eyJ0eXAiOiJ...",
"access": "eyJ0eXAiOiJ..."
}
From now on, the access
key is included in the Authorization
header of each request we make in the client with the prefix of Bearer
.
When these tokens expire we need to hit the token/refresh/
route with the refresh
token (saved in localStorage
) as the body of the request, this will return a response with a new set of tokens that will replace the ones stored in localStorage
.
// Request made to 'token/refresh/'
{
"refresh": "eyJ0eXAiOiJ..."
}
// Response
{
"refresh": "eyJ0eXAiOiJ...",
"access": "eyJ0eXAiOiJ..."
}
The access
token is valid for 1 week and the refresh
token is valid for a month. After the refresh
token expires the user is logged out from the client.
π
utils_api.py
This file contains the AuthUtils
helper function that can be used inside the setUp()
function of a TestCase in order to auth an user that needs to make a request that has to have an Authorization
header.
π
views.py
The views.py
is where all my server logic is. I've included documentation inside each function inside this file.
I'm using class-based views, inheriting from viewsets.ModelViewSet
to create the views that Django Rest Framework's Default Router will have. The ModelViewSet
class will provide common actions for our models such as list
, retrieve
, create
, update
, partial_update
and destroy
. We just need to provide the queryset, serializer and optionally permission classes or pagination classes. For example:
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all() # <- Queryset
serializer_class = TagSerializer # <- Serializer
permission_classes = [permissions.IsAuthenticated] # <- Permissions
In some cases I overrode the ModelViewSet
functions to suit the needs of my app. The get_queryset
function for instance, I overrode it in order to return the current logged in user's data instead of returning all the users data.
After these ModelViewSet
inherited views, I created APIView
inherited class-based views. I chose this class for its ease of use. I've included the documentation inside the functions for these classes too.
I'm using Vue 3 as the framework for my client with Vite as the bundler and dev server.
This folder contains the assets for my project such as the base.css
for the color variables, the Popper theme variables, the logo and the sound for starting and finishing a timer.
This is the folder where all my components are. It includes four folders: buttons, icons, modals, and slots.
Contains the clickable buttons used to create a task or a project. As well as the info buttons for Tags, Task and Project.
Filename | Description |
---|---|
NewProjectButton.vue |
Displays a modal that shows a form to create a new project. |
NewTaskButton.vue |
Displays a modal that shows a form to create a new task. |
ChoreButton.vue |
Is a wrapper component for the mentioned New Project and New Task buttons. |
Tags.vue |
Displays, adds and removes tags from a Task component. It can display tags without directly interacting with them as is the case when looking at tasks from the TaskView |
Task.vue |
The main button that is showed in the home screen with the task name, a delete and an info icon. When the info icon is clicked it displays a task info in a modal that allows editing. |
Project.vue |
Displays a project's name with an info icon, when the info icon is clicked it displays a modal with the tasks that are inside it. |
The various icons used throughout the app, I made them as a Vue component for its ease of use inside the app.
The modals used throughout the application.
AppModal.vue
π‘ This is a blank slate that can be used to create a modal anywhere in the app.
This is the base modal, it accepts the open
prop to handle whether the modal is open or closed and the isTask
prop which when set to false it aligns the items in the top part of the modal where the tags usually go. (We only pass this prop when the Modal is a task modal.)
This component uses the <Teleport>
Vue built-in component to go the body of the html, we set its z-index to a value of 10 and align it to the center of the screen.
When the app is visited from a phone or a tablet, the modal automatically adjusts itself to the proper size through media queries.
TheProjectModalBody.vue
This file contains the info part of the Project Modal, the tasks list. It uses the Subchores
component to manage the tasks.
β Because subtasks and tasks are similar visually, the Subchores
component can handle tasks and subtasks.
TheTaskModalBody.vue
Contains the info part of the Task Modal, (description, subtasks, and the estimated pomos counter). I decided to create this different file to unclutter the main TheTaskModal.vue
file and practice separation of concerns in components.
These are wrapper components for buttons or labels used inside the app that need tweaking depending where they are used.
SidebarItem.vue
A wrapper component for the icons in the sidebar, it includes a slot for the icon and a title of the icon.
MiniLabel.vue
A wrapper component that displays differently depending on the props being passed.isTask
: For adding and displaying subtasks inside the task modal or tasks inside the project modal.isTag
: For displaying tags inside the task modal.isAdd
: For adding a tag.
AppTitle.vue
A wrapper component for the header displayed on top of the projects and tasks lists . It accepts an icon and a title. I created this file because I wanted to work with Vue slots.
Rest of the files inside the components/
folder
Filename | Description |
---|---|
AddToProjectPopup.vue |
Adds the current opened task to a project, and if the task is already inside the project it highlights it. |
TheAlerts.vue |
The component for showing alerts globally when an action is done, it transitions at the top right of the page. It uses the alert store which I will later talk about. It has three styles: alert, error and info. |
AppChart.vue |
The main component for the displaying the stats in a chart. It uses the apexcharts library. This chart resizes automatically based on the window height and width. It is included in the StatsView . |
CurrentTask.vue |
Displays the current task the user is working on below the timer, it includes a button for removing the task as being the one the user is currently working on. |
Paginate.vue |
The pagination component showed below each of the projects and tasks lists. It conditionally based on the number of pages displays a ... button that moves 3 pages when clicked. |
BaseViewProject.vue |
Displays the project's name and when clicked it opens its modal. It is used in the ProjectView . |
TheProjectModal.vue |
Controls whether the Project Modal is showed and displays its info. |
AppProjects.vue |
The component that shows the ITask , the page count, iterates over the projects and shows the pagination component. |
SaveButton.vue |
The save button used in the Task and Project Modal. |
TheSettings.vue |
Displays the settings modal to control whether to auto start pomos or breaks and create,change and delete modes. |
TheSidebar.vue |
The sidebar displayed at the left of the page, it includes links to the stats and the info of the app. |
Subchore.vue |
Controls the subtask or task being showed, it is the info part of the subtask/task. It contains the tags if its a task inside a project, it handles the title and description input to clear if the task or subtask is not saved. Conditionally shows pomo counter if it's a task. |
Subchores.vue |
This is the most complex component in the project, this is because this component is used in two cases: the parent component is a project or task. If the parent component is a task it handles the creation and deletion of subtasks. If the parent component is a project it handles the creation and deletion of tasks inside the project. |
BaseViewTask.vue |
The component that is displayed when the user is on the TaskView . It shows the tags, the number of pomos done and the number of estimated pomos, as well as the title and description. When clicked it opens the task modal info. |
TheTaskModal.vue |
The whole task modal being displayed with the tags, done, delete and close icons. As well as the title, description and TheTaskModalBody mentioned previously, as well as the add to project and save buttons. |
AppTasks.vue |
The component that shows the ITask , the page count, iterates over the tasks and shows the pagination component. |
TheTimer.vue |
The main timer component, it starts, restarts and stops the timer. It sets the type of timer and displays the Current Task |
PomoCountSetter.vue |
The pomo count setter inside the task modal. |
Title.vue |
The title "Pomodoro Timer" displayed at the top of the app. When clicked it returns to the '/' route. |
UnauthedChart.vue |
The blurred chart showed when the user is not registered/logged out. |
UnauthedLogin.vue |
The component displayed in the Home page and above the blurred chart to Sign up or Login. |
TheUpperMenu.vue |
The menu showed at the top of the app, where the title, user info, settings, and log in / register buttons are displayed. |
UserInfo.vue |
The modal where the user can log out. |
WIP
index.ts
The main router file, it lazily loads the view components.
βIf the user visits the /login
or /register
routes when logged it redirects them to home.
I'm using Pinia as my library of choice for state management.
alerts.ts
π‘ The store for creating alerts that are available throughout the app.
I wanted to practice types and interfaces in TypeScript, so in this store I created the AlertStyle
type with the different types of alerts there can be. Later I created an interface for the options of an alert.
After this, I created the defaultOptions
for any alert, and made the AlertOptions
interface Required
.
Finally I declare the Alert
interface extending the AlertOptions
plus an id and the message the alert will contain.
I found using interfaces more useful because you can extend them to other interfaces.
Inside the state, this store has an items[]
array of type Alert
that has all the alerts currently being displayed. When a new alert is created the notify()
action is called which creates a new alert item with an unique id and pushes it to the items array. When 2 seconds are passed it removes itself from the items array with the remove()
action. The success()
, error()
and info()
actions use the notify()
action with a custom style.
auth.ts
π‘ The store for managing the JWT tokens (saving, removing and replacing them) and handle the log in, register and logout process.
At the top it creates a custom axios
instance for login in and uses this instance for creating an interceptor. This interceptor fires when the get request for the me/
endpoint in the getUser()
store action fails, it then intercepts the request and replaces the JWT tokens.
The getUser()
action is called on each page initial load. If the refresh
token has expired, it calls the logout()
action to log the user out.
Registering
When a user registers, it automatically logs them in calling the login store action.
Logging out
When a user logs out, the localStorage
is removed and the chore store is reset.
chore.ts
π‘ The store responsible for fetching the tasks, projects, tags, modes and stats. As well as managing the project and task pagination initially based on the returned data from tasks and projects.
I used getters
in this store for retrieving the total pages of tasks and projects based on the store's state.
I later then created store actions for moving between pages.
Finally I created actions for increasing the stats, changing the current task, incrementing current_task
gone_through
pomodoros and managing the saving, deleting and updating of tasks and projects too.
modal.ts
Manages a global state of the modal so that only one modal opens a time.
timer.ts
π‘The store for managing a global timer that retains state even when the page is changed. It has a
timerId
as part of the state that is set to the returned value ofwindow.setInterval
in TheTimer.vue'sinitTimer()
function and is used when clearing the Interval in thestopTimer()
function.
I'm using the dayjs
library to easily manage time in the store.
I created a defaultTimer
that is used when a timer mode is not found in localStorage
or the user is not logged in.
Then as part of the state I have a currentTimer
that is set to a string of whichever timer is currently on i.e. (pomo, short_break, long_break
) and I have a dedicated object for each of them in the state too with the timer and seconds as keys.
After this I have the currentMode
which helps keeping track of the timer's name the user is currently in. This with the help of the sessions
and current
state helps keeping track of the timers and assigning the next timer.
The done state and the ongoing variables keep track of the timer to conditionally display a UI elements.
And the auto_start_pomo
and auto_start_breaks
use the auth
store to retrieve the user settings if the user is authenticated, if not they default to false.
I created store getters
for order to retrieve the minutes and seconds from the currentTimer.timer
and the formattedTime
for displaying the time in the pages tab title in the format of mm:ss
As the final part of the store I have the timer actions to set a timer to the data passed, setting a timer to a new mode, decrementing a second from the currentTimer
, setting the next timer based on the sessions
and current
state variables, setting the timer to the default one declared at the top, and setting a timer based on the passed name. I use most of these methods in the TheTimer.vue
component.
The directory where I keep the types for the data I receive and set from and to the server.
index.ts
The file I import all the files in the directory and export them.
The pages that the Vue Router renders on each route.
Route | Component | Description |
---|---|---|
/ |
Home.vue |
Contains the main content of the app uses the auth store to conditionally render components based if the user is signed in or not. |
about/ |
AboutView.vue |
Explains what a pomodoro timer is, how to use the technique, and has info on this course as well. |
register/ |
RegisterView.vue |
Has the form for registering a new user to the app and a link to let an existing user log in. If the registration process is successful the user is automatically logged in as explained in the auth store section. |
login/ |
LoginView.vue |
Has the form for an existing user to log in. And a link to register a new user at the bottom. |
projects/ |
ProjectsView.vue |
Displays 10 projects per page. |
tasks/ |
TasksView.vue |
Displays 10 projects per page. |
stats/ |
StatsView.vue |
This is where the chart displaying the user stats is. It uses the built-in <Suspense> Vue component for fetching the stats. The second icon in the sidebar. |
tags/ |
TagsView.vue |
Displays all the user tags. |
tag/:name |
TagView.vue |
A page that accepts the parameter of :name to display all the tasks that contain that tag. |
App.vue
In this file, I render the whole app.
I have the SideBar
component, the TheUpperMenu
by the right side positioned at the top and the rest of the content is displayed as a view by the router-view
component, being the HomeView
the default one on the /
route.
I added the TheAlerts
component in here so that they were accessible throughout the app.
axios.ts
The config file for my axios
instance. It sets a baseURL
, a Content-Type
header and adds the Authorization
header to each request being sent.
main.ts
π‘ This is the main file of the application, it imports the
createApp
function from thevue
library and with the importedApp
fromApp.vue
we can create the app.
Then we proceed to call the use()
function for being able to use things as Pinia store, router, autoAnimatePlugin
in our app. Then we call the component()
function to register the Popper
component globally in order to use it in any component of our app.
Finally we call the mount()
function and we pass the id of the element when we want to mount the app, in this case #app
.
In the last of the file we fetch all the modes, tags and the user's stats with the fetchAll()
function from the chore
store.
π
.env
I created this file to practice with environment variables inside the Vite server. It contains the server URL.
.prettierrc
I specified the styling rules for the app in here. We can easily format all of the files with these rules in the src/
directory with the command:
prettier --write .
index.html
The mounting point for the Vue app. It has a <div>
with an id of #app
which is where the app will be mounted to.
package.json
This file keeps tracks of the dependencies in the app, as well as managing the scripts, the name and the version of the app.
tsconfig.*.json
TypeScript configuration files required by the app and Vite.
Vite configuration file. It uses the vue()
plugin and an alias of @
for importing from the ./src
directory.
The boilerplate Django django-admin startproject
code.
π
settings.py
The settings for the project. I decided to use environment variables with the package python-dotenv
(listed on the requirements.txt), to retrieve this variables. This variables exists in the .env
file at the root of the project.
π
urls.py
In this file I registered the routes inside the api
application, and the urls needed for the rest_framework module authentication.
Setting up the server
- Create a virtual environment
python -m venv env
- Activate the virtual environment
source env/bin/activate
- Install the requirements
pip install -r requirements.txt
- Run migrations
python manage.py makemigrations api
python manage.py migrate
- Run the server
python manage.py runserver
Setting up the client
- Change directory to the client folder
cd client
- Install the dependencies
npm i
- Run the project
npm run dev
- Go the project URL
http://localhost:3000/