Initial commit

This commit is contained in:
Chris Veleris 2023-11-13 11:47:56 +02:00
commit 3bd7628013
47 changed files with 1542 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.sqlite3
*.sqlite3-shm
*.sqlite3-wal
certs/

20
Gemfile Normal file
View file

@ -0,0 +1,20 @@
source 'https://rubygems.org'
gem 'sinatra'
gem 'puma'
gem 'rake'
# DB
gem 'sinatra-activerecord'
gem 'sqlite3'
# Authentication
gem 'bcrypt'
# Other
gem 'rerun'
gem 'byebug'
# Development
gem 'faker'
gem 'rubocop'

109
Gemfile.lock Normal file
View file

@ -0,0 +1,109 @@
GEM
remote: https://rubygems.org/
specs:
activemodel (7.1.1)
activesupport (= 7.1.1)
activerecord (7.1.1)
activemodel (= 7.1.1)
activesupport (= 7.1.1)
timeout (>= 0.4.0)
activesupport (7.1.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.19)
bigdecimal (3.1.4)
byebug (11.1.3)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
drb (2.2.0)
ruby2_keywords
faker (3.2.2)
i18n (>= 1.8.11, < 2)
ffi (1.16.3)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
language_server-protocol (3.17.0.3)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
minitest (5.20.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
mutex_m (0.2.0)
nio4r (2.5.9)
parallel (1.23.0)
parser (3.2.2.4)
ast (~> 2.4.1)
racc
puma (6.4.0)
nio4r (~> 2.0)
racc (1.7.3)
rack (2.2.8)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
rainbow (3.1.1)
rake (13.1.0)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (2.8.2)
rerun (0.14.0)
listen (~> 3.0)
rexml (3.2.6)
rubocop (1.57.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sinatra (3.1.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.1.0)
tilt (~> 2.0)
sinatra-activerecord (2.0.27)
activerecord (>= 4.1)
sinatra (>= 1.0)
sqlite3 (1.6.8-arm64-darwin)
tilt (2.3.0)
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
PLATFORMS
arm64-darwin-22
DEPENDENCIES
bcrypt
byebug
faker
puma
rake
rerun
rubocop
sinatra
sinatra-activerecord
sqlite3
BUNDLED WITH
2.4.21

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) [2023] [Chris Veleris]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# tu | du | di
`tu|du|di` is a task and project management web application built with Sinatra. It allows users to efficiently manage their tasks and projects, categorize them into different areas, and track due dates. `tu|du|di` is designed to be intuitive and easy to use, providing a seamless experience for personal productivity.
## Features
- **Task Management**: Create, update, and delete tasks. Mark tasks as completed and view them by different filters (Today, Upcoming, Someday).
- **Project Tracking**: Organize tasks into projects. Each project can contain multiple tasks.
- **Area Categorization**: Group projects into areas for better organization and focus.
- **Due Date Tracking**: Set due dates for tasks and view them based on due date categories.
- **Responsive Design (in progress) **: Accessible from various devices, ensuring a consistent experience across desktops, tablets, and mobile phones.
## Getting Started
### Prerequisites
Before you begin, ensure you have met the following requirements:
- Ruby (version 3.2.2 or higher)
- Sinatra
- SQLite3
- Puma
### Installation
To install `tu|du|di`, follow these steps:
1. Clone the repository:
```bash
git clone https://github.com/chrisvel/tu-du-di.git
```
2. Navigate to the project directory:
```bash
cd tu-du-di
```
3. Install the required gems:
```bash
bundle install
```
#### SSL setup
1. Create and enter the directory:
```bash
mkdir certs
```
2. Navigate to the certs directory:
```bash
cd certs
```
2. Create the key and cert:
```bash
openssl genrsa -out server.key 2048
openssl req -new -x509 -key server.key -out server.crt -days 365
```
### Usage
To start the application, run the following command in your terminal:
```bash
puma -C app/config/puma.rb
```
Open your browser and navigate to `http://localhost:9292` to access the application.
## Contributing
Contributions to `tu|du|di` are welcome. To contribute:
1. Fork the repository.
2. Create a new branch (`git checkout -b feature/AmazingFeature`).
3. Make your changes.
4. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
5. Push to the branch (`git push origin feature/AmazingFeature`).
6. Open a pull request.
## License
This project is licensed under the [MIT License](LICENSE).
## Contact
If you have any questions or comments about `tu|du|di`, please feel free to [open an issue](https://github.com/chrisvel/tu-du-di/issues) or contact the developer directly.
---
README created by [Chris Veleris](https://github.com/chrisvel) for `tu|du|di`.

12
Rakefile Normal file
View file

@ -0,0 +1,12 @@
require 'irb'
require 'sinatra/activerecord'
require 'sinatra/activerecord/rake'
require './app'
desc 'Start an interactive console'
task :console do
ARGV.clear
IRB.start
end

90
app.rb Normal file
View file

@ -0,0 +1,90 @@
require 'sinatra'
require 'sinatra/activerecord'
require 'securerandom'
require './app/models/user'
require './app/models/area'
require './app/models/project'
require './app/models/task'
require './app/helpers/authentication_helper'
require './app/routes/authentication_routes'
require './app/routes/tasks_routes'
require './app/routes/projects_routes'
require './app/routes/areas_routes'
helpers AuthenticationHelper
use Rack::MethodOverride
set :database_file, './app/config/database.yml'
set :views, proc { File.join(root, 'app/views') }
set :public_folder, 'public'
configure do
enable :sessions
set :sessions, httponly: true, secure: production?, expire_after: 2_592_000
set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(64) }
set :session_secret,
'740cca863278d6cbacb64dbdd41cfdb1598e8208ce9b9d29b0a1e7c1e1367ca1241d8048849ee88784731d43879c94f5b9f0a639135828d590a447acb2d98e1c'
end
use Rack::Protection
before do
require_login
end
helpers do
def current_path
request.path_info
end
def partial(page, options = {})
erb page, options.merge!(layout: false)
end
def priority_class(task)
return 'text-success' if task.completed
case task.priority
when 'Medium' then 'text-warning'
when 'High' then 'text-danger'
else 'text-secondary'
end
end
def nav_link(path, query_params = {}, project_id = nil)
current_uri = request.path_info
current_query = request.query_string
current_params = Rack::Utils.parse_nested_query(current_query)
is_project_page = current_uri.include?('/project/') && path.include?('/project/')
is_active = if is_project_page
current_uri == path && (!project_id || current_uri.end_with?("/#{project_id}"))
elsif !query_params.empty?
current_uri == path && query_params.all? { |k, v| current_params[k] == v }
else
current_uri == path && current_params.empty?
end
classes = 'nav-link py-1 px-3'
classes += ' active bg-dark' if is_active
classes += ' link-dark' unless is_active
classes
end
end
get '/' do
redirect '/inbox' if logged_in?
erb :inbox
end
get '/inbox' do
erb :inbox
end

17
app/config/database.yml Normal file
View file

@ -0,0 +1,17 @@
# config/database.yml
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3

5
app/config/puma.rb Normal file
View file

@ -0,0 +1,5 @@
ssl_bind '0.0.0.0', '9292', {
key: 'certs/server.key',
cert: 'certs/server.crt',
verify_mode: 'none'
}

View file

@ -0,0 +1,15 @@
module AuthenticationHelper
def logged_in?
!!session[:user_id]
end
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def require_login
return if ['/login', '/logout', '/signup'].include? request.path
redirect '/login' unless logged_in?
end
end

4
app/models/area.rb Normal file
View file

@ -0,0 +1,4 @@
class Area < ActiveRecord::Base
belongs_to :user
has_many :projects, dependent: :destroy
end

5
app/models/project.rb Normal file
View file

@ -0,0 +1,5 @@
class Project < ActiveRecord::Base
belongs_to :user
belongs_to :area, optional: true
has_many :tasks, dependent: :destroy
end

6
app/models/task.rb Normal file
View file

@ -0,0 +1,6 @@
class Task < ActiveRecord::Base
belongs_to :user
belongs_to :project, optional: true
default_scope { where(completed: false) }
end

9
app/models/user.rb Normal file
View file

@ -0,0 +1,9 @@
class User < ActiveRecord::Base
has_secure_password
has_many :areas
has_many :projects
has_many :tasks
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true
end

View file

@ -0,0 +1,42 @@
class Sinatra::Application
post '/areas/create' do
area = current_user.areas.create(name: params[:name])
if area.persisted?
redirect '/'
else
@errors = 'There was a problem creating the area.'
redirect '/'
end
end
patch '/areas/:id' do
area = current_user.areas.find_by(id: params[:id])
if area
area.name = params[:name]
if area.save
redirect '/'
else
@errors = 'There was a problem updating the area.'
erb :some_template
end
else
status 404
"Area not found or doesn't belong to the current user."
end
end
delete '/area/:id' do
area = current_user.areas.find_by(id: params[:id])
if area
area.destroy
redirect '/'
else
status 404
@errors = 'Area not found or not owned by the current user.'
end
end
end

View file

@ -0,0 +1,22 @@
class Sinatra::Application
get '/login' do
erb :login
end
post '/login' do
@user = User.find_by(email: params[:email])
if @user&.authenticate(params[:password])
session[:user_id] = @user.id
redirect '/'
else
logger.warn "Invalid credentials for user with email #{params[:email]}"
@errors = ['Invalid credentials']
erb :login
end
end
get '/logout' do
session.clear
redirect '/login'
end
end

View file

@ -0,0 +1,57 @@
class Sinatra::Application
get '/projects' do
@projects_with_tasks = current_user.projects.includes(:tasks).order(:name)
erb :'projects/index'
end
get '/project/:id' do
@project = current_user.projects.includes(:tasks).find_by(id: params[:id])
halt 404, 'Project not found' unless @project
erb :'projects/show'
end
post '/project/create' do
project = current_user.projects.new(name: params[:name], area_id: params[:area_id].presence)
if project.save
redirect '/'
else
@errors = 'There was a problem creating the project.'
redirect '/'
end
end
patch '/project/:id' do
project = current_user.projects.find_by(id: params[:id])
if project
project.name = params[:name]
project.description = params[:description]
project.area_id = params[:area_id].presence
if project.save
redirect "/project/#{project.id}"
else
@errors = 'There was a problem updating the project.'
erb :edit_project
end
else
status 404
"Project not found or doesn't belong to the current user."
end
end
delete '/project/:id' do
project = current_user.projects.find_by(id: params[:id])
if project
project.destroy
redirect '/projects'
else
status 404
"Project not found or doesn't belong to the current user."
end
end
end

108
app/routes/tasks_routes.rb Normal file
View file

@ -0,0 +1,108 @@
class Sinatra::Application
get '/tasks' do
case params[:due_date]
when 'today'
today = Date.today
@tasks = current_user.tasks.where(due_date: today.beginning_of_day..today.end_of_day, project: nil)
@projects_with_tasks = current_user.projects
.joins(:tasks)
.where(tasks: { due_date: today.beginning_of_day..today.end_of_day })
.distinct.order('projects.name ASC')
when 'upcoming'
one_week_from_today = Date.today + 7.days
@tasks = current_user.tasks.where(due_date: Date.today..one_week_from_today, project: nil)
@projects_with_tasks = current_user.projects.includes(:tasks).where(tasks: { due_date: Date.today..one_week_from_today }).order('projects.name ASC')
when 'never'
@tasks = current_user.tasks.where(due_date: nil, project: nil)
@projects_with_tasks = current_user.projects.includes(:tasks).where(tasks: { due_date: nil }).order('projects.name ASC')
else
@tasks = current_user.tasks.where(project: nil)
@projects_with_tasks = current_user.projects.includes(:tasks).order('projects.name ASC')
end
@tasks ||= []
@projects_with_tasks ||= []
erb :'tasks/index'
end
post '/task/create' do
task_attributes = {
name: params[:name],
priority: params[:priority],
due_date: params[:due_date],
user_id: current_user.id
}
if params[:project_id].empty?
task = current_user.tasks.build(task_attributes)
else
project = current_user.projects.find_by(id: params[:project_id])
halt 400, 'Invalid project.' unless project
task = project.tasks.build(task_attributes)
end
if task.save
redirect request.referrer || '/'
else
halt 400, 'There was a problem creating the task.'
end
end
patch '/task/:id' do
task = current_user.tasks.find_by(id: params[:id])
halt 404, 'Task not found.' unless task
task_attributes = {
name: params[:name],
priority: params[:priority],
due_date: params[:due_date]
}
if params[:project_id] && !params[:project_id].empty?
project = current_user.projects.find_by(id: params[:project_id])
halt 400, 'Invalid project.' unless project
task.project = project
else
task.project = nil
end
if task.update(task_attributes)
redirect '/'
else
halt 400, 'There was a problem updating the task.'
end
end
patch '/task/:id/toggle_completion' do
content_type :json
task = current_user.tasks.find(params[:id])
if task
task.completed = !task.completed
if task.save
task.to_json
else
status 422
{ error: 'Unable to update task' }.to_json
end
else
status 400
{ error: 'Task not found' }.to_json
end
end
delete '/task/:id' do
task = current_user.tasks.find_by(id: params[:id])
halt 404, 'Task not found.' unless task
if task.destroy
redirect '/'
else
halt 400, 'There was a problem deleting the task.'
end
end
end

View file

@ -0,0 +1,19 @@
<div class="modal modal-lg fade" id="editAreaModal<%= area.id %>" tabindex="-1" aria-labelledby="editAreaModalLabel<%= area.id %>" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editAreaModalLabel">Edit Area</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<%= partial :'areas/_form', locals: {
area: area,
form_id: 'editAreaForm',
action_url: "/areas/#{area.id}",
method: 'patch',
button_text: 'Update Area'
} %>
</div>
</div>
</div>
</div>

12
app/views/areas/_form.erb Normal file
View file

@ -0,0 +1,12 @@
<form id="<%= form_id %>" action="<%= action_url %>" method="post">
<% unless area.new_record? %>
<input type="hidden" name="_method" value="<%= method %>">
<% end %>
<div class="mb-3">
<label for="areaName" class="form-label">Area Name:</label>
<input type="text" class="form-control" id="areaName" name="name" value="<%= area.name %>" required>
</div>
<button type="submit" class="btn btn-primary"><%= button_text %></button>
</form>

View file

@ -0,0 +1,19 @@
<div class="modal modal-lg fade" id="newAreaModal" tabindex="-1" aria-labelledby="newAreaModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newAreaModalLabel">New Area</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<%= partial :'areas/_form', locals: {
area: Area.new,
form_id: 'newAreaForm',
action_url: '/areas/create',
method: 'post',
button_text: 'Create Area'
} %>
</div>
</div>
</div>
</div>

21
app/views/inbox.erb Normal file
View file

@ -0,0 +1,21 @@
<h2 class="mb-5"><i class="bi bi-inbox-fill ms-3 me-2"></i> Inbox</h2>
<div class="bg-light py-2 px-3 mx-3 d-flex align-items-center border" data-bs-toggle="collapse" data-bs-target="#newTaskForm" aria-expanded="false" aria-controls="newTaskForm" style="cursor: pointer;">
<i class="fs-4 bi bi-plus-circle-fill text-primary me-2"></i> <span class="fs-6">New task</span>
</div>
<div class="collapse" id="newTaskForm">
<div class="card border-0 bg-white shadow-sm mt-2 p-4 mx-3">
<%= partial :'tasks/_form', locals: { task: Task.new } %>
</div>
</div>
<div class="mx-3 my-2">
<% standalone_tasks = current_user.tasks.where(project_id: nil) %>
<% if standalone_tasks.any? %>
<% standalone_tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
<%= partial :'tasks/_form', locals: { task: task } %>
</div>
<%= partial :'tasks/_task', locals: { task: task } %>
<% end %>
<% end %>
</div>
<%= partial :'tasks/_edit_task_modal' %>

24
app/views/layout.erb Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>tu|du|di</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/app.css">
</head>
<body class="container-fluid bg-light p-0">
<% if current_user %>
<div class="row flex-nowrap">
<%= partial :'sidebar' %>
<div class="col-md-9 col-lg-10 px-md-4 pt-4 mb-3 main-content" style="background: #fbfbfb">
<%= yield %>
<%= partial :'projects/_new_project_modal' %>
</div>
</div>
<% end %>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script> </body>
<script src="/js/app.js"></script>
</html>

27
app/views/login.erb Normal file
View file

@ -0,0 +1,27 @@
<div class="container mt-5">
<h2 class="mb-4 text-center" style="margin-top: 200px;">Login</h2>
<% if @errors %>
<div class="alert alert-danger w-50 mx-auto" role="alert">
<ul class="mb-0">
<% @errors.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="card mx-auto w-50">
<div class="card-body">
<form action="/login" method="post">
<div class="mb-3">
<label for="email" class="form-label">Email:</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary mt-4">Login</button>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,13 @@
<div class="modal modal-lg fade" id="editProjectModal" tabindex="-1" aria-labelledby="editProjectModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editProjectModalLabel">Edit Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<%= partial :'projects/_form', locals: { project: @project, form_action: "/project/#{@project.id}", form_id: 'editProjectForm' } %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,28 @@
<% form_action ||= "/projects/#{project.id}" %>
<% form_id ||= 'projectForm' %>
<% form_method = project.new_record? ? 'post' : 'patch' %>
<form id="<%= form_id %>" action="<%= form_action %>" method="post">
<% unless project.new_record? %>
<input type="hidden" name="_method" value="<%= form_method %>">
<% end %>
<div class="mb-3">
<label for="projectName" class="form-label">Project Name:</label>
<input type="text" class="form-control" id="projectName" name="name" value="<%= project.name %>" required>
</div>
<div class="mb-3">
<label for="projectDescription" class="form-label">Description:</label>
<textarea class="form-control" id="projectDescription" name="description" rows="3"><%= project.description %></textarea>
</div>
<div class="mb-3">
<label for="projectArea" class="form-label">Area (optional):</label>
<select class="form-select" id="projectArea" name="area_id">
<option value="">No Area</option>
<% current_user.areas.each do |area| %>
<option value="<%= area.id %>" <%= 'selected' if project.area_id == area.id %>><%= area.name %></option>
<% end %>
</select>
</div>
<div class="">
<button type="submit" class="btn btn-primary"><%= project.new_record? ? 'Create Project' : 'Update Project' %></button>
</div>
</form>

View file

@ -0,0 +1,13 @@
<div class="modal modal-lg fade" id="newProjectModal" tabindex="-1" aria-labelledby="newProjectModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newProjectModalLabel">New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<%= partial :'projects/_form', locals: { project: Project.new, form_action: '/project/create', form_id: 'newProjectForm' } %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,29 @@
<h2 class="mb-5"><i class="bi bi-hexagon ms-3 me-2"></i>Areas & Projects</h2>
<div class="bg-light py-2 px-3 mx-3 d-flex align-items-center border" data-bs-toggle="collapse" data-bs-target="#newTaskForm" aria-expanded="false" aria-controls="newTaskForm" style="cursor: pointer;">
<i class="fs-4 bi bi-plus-circle-fill text-primary me-2"></i> <span class="fs-6">New task</span>
</div>
<div class="collapse" id="newTaskForm">
<div class="card bg-white shadow-sm border-0 p-4">
<%= partial :'tasks/_form', locals: { task: Task.new } %>
</div>
</div>
<div class="mx-3 my-2">
<% if @projects_with_tasks.any? %>
<% @projects_with_tasks.each do |project| %>
<% if project.tasks.any? %>
<h5 class="mt-4 ms-4 mb-2">
<div class="px-2 py-1 mb-1 border border-dark d-inline-block"><%= project.name.upcase %></div>
</h5>
<% project.tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
<%= partial :'tasks/_form', locals: { task: task } %>
</div>
<div class="ms-4">
<%= partial :'tasks/_task', locals: { task: task } %>
</div>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
<%= partial :'tasks/_edit_task_modal' %>

View file

@ -0,0 +1,61 @@
<% if current_user %>
<div class="container-fluid">
<div class="row flex-nowrap">
<%= partial :'sidebar' %>
<div class="col-md-9 col-lg-10 px-md-4 pt-4 mb-3" style="background: #fbfbfb">
<div class="d-flex justify-content-between align-items-center mb-5">
<h2 class="mb-0"><i class="bi bi-hexagon ms-3 me-2"></i><%= @project.name.upcase %></h2>
<div class="dropdown">
<button class="btn btn-link text-secondary" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical fs-6"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editProjectModal" data-project-id="<%= @project.id %>">
Edit
</a>
</li>
<li>
<% if @project %>
<form id="delete_project_<%= @project.id %>" action="/project/<%= @project.id %>" method="post" class="d-none">
<input type="hidden" name="_method" value="delete">
</form>
<% end %>
<a class="dropdown-item" href="#" onclick="deleteProject('<%= @project.id %>')">Delete</a>
</li>
</ul>
</div>
</div>
<% unless @project.description.blank? %>
<div class="bg-secondary-subtle px-4 py-3 mb-4 mx-3 rounded">
<%= @project.description %>
</div>
<% end %>
<div class="bg-light py-2 px-3 mx-3 d-flex align-items-center border" data-bs-toggle="collapse" data-bs-target="#newTaskForm" aria-expanded="false" aria-controls="newTaskForm" style="cursor: pointer;">
<i class="fs-4 bi bi-plus-circle-fill text-primary me-2"></i> <span class="fs-6">New task</span>
</div>
<div class="collapse" id="newTaskForm">
<div class="card border-0 bg-white shadow-sm mt-2 p-4 mx-3">
<%= partial :'tasks/_form', locals: { task: Task.new(project: @project) } %>
</div>
</div>
<div class="mx-3 my-2">
<% if @project.tasks.any? %>
<% @project.tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
<%= partial :'tasks/_form', locals: { task: task } %>
</div>
<div>
<%= partial :'tasks/_task', locals: { task: task } %>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
<%= partial :'projects/_edit_project_modal', locals: { project: @project } %>
<%= partial :'tasks/_edit_task_modal' %>
<% else %>
<p>Please log in to view your dashboard.</p>
<% end %>

103
app/views/sidebar.erb Normal file
View file

@ -0,0 +1,103 @@
<div class="d-flex flex-column flex-shrink-0 p-3 bg-light vh-100 fixed-sidebar" style="width: 300px;"> <a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none">
<span class="fs-4">tu|du|di</span>
</a>
<hr>
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a href="/inbox" class="<%= nav_link('/inbox') %>" aria-current="page">
<i class="bi bi-inbox-fill me-1"></i> Inbox
</a>
</li>
<li>
<a href="/tasks?due_date=today" class="<%= nav_link('/tasks', 'due_date' => 'today') %>">
<i class="bi bi-calendar-day-fill me-1"></i> Today
</a>
</li>
<li>
<a href="/tasks?due_date=upcoming" class="<%= nav_link('/tasks', 'due_date' => 'upcoming') %>">
<i class="bi bi-calendar3 me-1"></i> Upcoming
</a>
</li>
<li>
<a href="/projects" class="<%= nav_link('/projects') %>">
<i class="bi bi-hexagon-fill me-1"></i> Areas & Projects
</a>
</li>
<li>
<a href="/tasks?due_date=anytime" class="<%= nav_link('/tasks', 'due_date' => 'anytime') %>">
<i class="bi bi-sun-fill me-1"></i> Anytime
</a>
</li>
<li>
<a href="/tasks?due_date=never" class="<%= nav_link('/tasks', 'due_date' => 'never') %>">
<i class="bi bi-moon-stars-fill me-1"></i> Someday
</a>
</li>
<li class="border-top my-3"></li>
</ul>
<div class="px-3 mb-auto">
<% current_user.areas.includes(:projects).order('name ASC').each do |area| %>
<div class="area-item d-flex justify-content-between align-items-center">
<a href="#area_<%= area.id %>_projects" class="nav-link link-dark flex-grow-1" data-bs-toggle="collapse" aria-expanded="true">
<i class="bi bi-folder me-1 fs-6"></i>
<i class="bi bi-folder2-open me-1 fs-6 d-none"></i>
<%= area.name %>
</a>
<div class="dropdown area-options">
<button class="btn btn-link text-secondary" type="button" id="dropdownMenuButton<%= area.id %>" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton<%= area.id %>">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editAreaModal<%= area.id %>">Edit</a></li>
<li>
<form action="/area/<%= area.id %>" method="post" onsubmit="return confirm('Are you sure you want to delete this area?');">
<input type="hidden" name="_method" value="delete">
<button type="submit" class="dropdown-item">Delete</button>
</form>
</li>
</ul>
</div>
</div>
<% if area.projects.any? %>
<div class="collapse show" id="area_<%= area.id %>_projects">
<ul class="nav nav-pills flex-column">
<% area.projects.each do |project| %>
<li class="nav-item">
<a href="/project/<%= project.id %>" class="<%= nav_link("/project/#{project.id}") %>">
<i class="bi bi-hexagon me-1"></i> <%= project.name.length > 22 ? project.name[0...22] + '...' : project.name %>
</a>
</li>
<% end %>
</ul>
</div>
<% end %>
<% end %>
</div>
<div class="">
<div class="dropdown">
<button class="btn btn-dark dropdown-toggle w-100" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
New Section
</button>
<ul class="dropdown-menu w-100" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#newProjectModal">Project</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#newAreaModal">Area</a></li>
</ul>
</div>
</div>
<hr>
<div class="dropdown">
<a href="#" class="d-flex justify-content-center align-items-center link-dark text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
<strong><%= current_user.email %></strong>
</a>
<ul class="dropdown-menu dropdown-menu-dark small text-center" aria-labelledby="dropdownUser1">
<li><a class="dropdown-item" href="#">Profile</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout">Sign out</a></li>
</ul>
</div>
</div>
<%= partial :'projects/_new_project_modal' %>
<%= partial :'areas/_new_area_modal' %>
<% current_user.areas.each do |area| %>
<%= partial :'areas/_edit_area_modal', locals: { area: area } %>
<% end %>

View file

@ -0,0 +1,13 @@
<div class="modal modal-lg fade" id="editTaskModal" tabindex="-1" aria-labelledby="editTaskModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editTaskModalLabel">Edit Task</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="editTaskFormContainer"></div>
</div>
</div>
</div>
</div>

64
app/views/tasks/_form.erb Normal file
View file

@ -0,0 +1,64 @@
<% action_url = task.new_record? ? '/task/create' : "/task/#{task.id}" %>
<% method = task.new_record? ? 'post' : 'patch' %>
<form action="<%= action_url %>" method="post" class="">
<% unless task.new_record? %>
<input type="hidden" name="_method" value="<%= method %>">
<% end %>
<fieldset>
<div class="row mb-3">
<div class="col-md-12">
<label for="task_name" class="form-label">Task:</label>
<input type="text" id="task_name" name="name" value="<%= task.name %>" class="form-control" required>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label for="task_project" class="form-label">Project (optional):</label>
<select id="task_project" name="project_id" class="form-select">
<option value="">No Project</option>
<% current_user.projects.each do |project| %>
<option value="<%= project.id %>" <%= 'selected' if task.project_id == project.id %>><%= project.name %></option>
<% end %>
</select>
</div>
<div class="col-md-4">
<label for="task_priority" class="form-label">Priority:</label>
<select id="task_priority" name="priority" class="form-select">
<option value="Low" <%= 'selected' if task.priority == 'Low' %>>Low</option>
<option value="Medium" <%= 'selected' if task.priority == 'Medium' %>>Medium</option>
<option value="High" <%= 'selected' if task.priority == 'High' %>>High</option>
</select>
</div>
<div class="col-md-4">
<label for="task_due_date" class="form-label">Due Date:</label>
<input type="date" id="task_due_date" name="due_date" value="<%= task.due_date.strftime('%Y-%m-%d') if task.due_date %>" class="form-control">
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<%= task.new_record? ? 'Create Task' : 'Update Task' %>
</button>
<% unless task.new_record? %>
<button type="button" class="btn btn-outline-danger" onclick="deleteTask('<%= task.id %>')">
<i class="bi bi-trash"></i>
</button>
<% end %>
</div>
</fieldset>
</form>
<% if !task.new_record? %>
<form id="delete_task_<%= task.id %>" action="/task/<%= task.id %>" method="post" class="d-none">
<input type="hidden" name="_method" value="delete">
</form>
<% end %>
<script>
function deleteTask(taskId) {
if (confirm('Are you sure you want to delete this task?')) {
var form = document.getElementById('delete_task_' + taskId);
form.submit();
}
}
</script>

View file

@ -0,0 +1,9 @@
<% if params[:due_date] == 'today' %>
<h2 class="mb-5"><i class="bi bi-calendar-fill ms-3 me-2"></i> Today</h2>
<% elsif params[:due_date] == 'upcoming' %>
<h2 class="mb-5"><i class="bi bi-calendar3 ms-3 me-2"></i> Upcoming</h2>
<% elsif params[:due_date] == 'never' %>
<h2 class="mb-5"><i class="bi bi-moon-stars-fill ms-3 me-2"></i> Someday</h2>
<% elsif params[:due_date] == 'anytime' %>
<h2 class="mb-5"><i class="bi bi-sun-fill ms-3 me-2"></i> Anytime</h2>
<% end %>

25
app/views/tasks/_task.erb Normal file
View file

@ -0,0 +1,25 @@
<div class="border-0 bg-white shadow-sm px-3 py-2 mb-1 d-flex align-items-center task-item <%= 'opacity-50' if task.completed %>" data-task-id="<%= task.id %>">
<div class="flex-grow-1 d-flex align-items-center">
<span onclick="toggleTaskCompletion(event, <%= task.id %>)" class="toggle-completion">
<i class="fs-6 bi <%= task.completed ? 'bi-check-circle-fill' : 'bi-circle' %> <%= priority_class task %> me-2"></i>
</span>
<%= task.name %>
</div>
<div class="ms-3 d-flex align-items-center">
<% if task.due_date %>
<% if task.due_date.to_date == Date.today %>
<span class="badge bg-success rounded-pill ms-2">
<i class="bi bi-calendar me-2"></i> TODAY
</span>
<% elsif task.due_date && task.due_date.to_date < Date.today %>
<span class="badge bg-danger rounded-pill ms-2">
<i class="bi bi-calendar me-2"></i> <%= task.due_date.strftime("%Y-%m-%d") %>
</span>
<% else %>
<span class="badge bg-info rounded-pill ms-2">
<i class="bi bi-clock me-2"></i> <%= task.due_date.strftime("%Y-%m-%d") %>
</span>
<% end %>
<% end %>
</div>
</div>

40
app/views/tasks/index.erb Normal file
View file

@ -0,0 +1,40 @@
<%= partial :'tasks/_header' %>
<div class="bg-light py-2 px-3 mx-3 d-flex align-items-center border" data-bs-toggle="collapse" data-bs-target="#newTaskForm" aria-expanded="false" aria-controls="newTaskForm" style="cursor: pointer;">
<i class="fs-4 bi bi-plus-circle-fill text-primary me-2"></i> <span class="fs-6">New task</span>
</div>
<div class="collapse" id="newTaskForm">
<div class="card border-0 bg-white shadow-sm mt-2 p-4 mx-3">
<%= partial :'tasks/_form', locals: { task: Task.new } %>
</div>
</div>
<div class="mx-3 my-2">
<% if @tasks.any? %>
<% @tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
<%= partial :'tasks/_form', locals: { task: task } %>
</div>
<%= partial :'tasks/_task', locals: { task: task } %>
<% end %>
<% end %>
</div>
<div class="mx-3 my-2">
<% if @projects_with_tasks.any? %>
<% @projects_with_tasks.each do |project| %>
<% if project.tasks.any? %>
<h5 class="mt-4 ms-4 mb-2">
<div class="px-2 py-1 mb-1 border border-dark d-inline-block"><%= project.name.upcase %></div>
</h5>
<% project.tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
<%= partial :'tasks/_form', locals: { task: task } %>
</div>
<div class="ms-4">
<%= partial :'tasks/_task', locals: { task: task } %>
</div>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
<%= partial :'tasks/_edit_task_modal' %>

2
config.ru Normal file
View file

@ -0,0 +1,2 @@
require './app'
run Sinatra::Application

View file

@ -0,0 +1,11 @@
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.timestamps null: false
end
end
end

View file

@ -0,0 +1,10 @@
class CreateAreas < ActiveRecord::Migration[7.1]
def change
create_table :areas do |t|
t.string :name
t.references :user, null: false, foreign_key: true
t.timestamps null: false
end
end
end

View file

@ -0,0 +1,11 @@
class CreateProjects < ActiveRecord::Migration[7.1]
def change
create_table :projects do |t|
t.string :name
t.references :user, null: false, foreign_key: true
t.references :area, foreign_key: true
t.timestamps null: false
end
end
end

View file

@ -0,0 +1,13 @@
class CreateTasks < ActiveRecord::Migration[7.1]
def change
create_table :tasks do |t|
t.string :name
t.string :priority
t.datetime :due_date
t.references :user, null: false, foreign_key: true
t.references :project, foreign_key: true
t.timestamps null: false
end
end
end

View file

@ -0,0 +1,7 @@
class AddFieldsToTasks < ActiveRecord::Migration[7.1]
def change
add_column :tasks, :today, :boolean, default: false
add_column :tasks, :description, :text
add_column :tasks, :completed, :boolean, default: false
end
end

View file

@ -0,0 +1,5 @@
class AddFieldsToProjects < ActiveRecord::Migration[7.1]
def change
add_column :projects, :description, :text
end
end

View file

@ -0,0 +1,13 @@
class AddCascadeDeleteToProjectsAndTasks < ActiveRecord::Migration[7.1]
def change
# Remove the existing foreign key from projects to areas
remove_foreign_key :projects, :areas
# Add the new foreign key with on_delete: :cascade
add_foreign_key :projects, :areas, on_delete: :cascade
# Remove the existing foreign key from tasks to projects
remove_foreign_key :tasks, :projects
# Add the new foreign key with on_delete: :cascade
add_foreign_key :tasks, :projects, on_delete: :cascade
end
end

61
db/schema.rb Normal file
View file

@ -0,0 +1,61 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2023_11_10_163101) do
create_table "areas", force: :cascade do |t|
t.string "name"
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_areas_on_user_id"
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.integer "user_id", null: false
t.integer "area_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "description"
t.index ["area_id"], name: "index_projects_on_area_id"
t.index ["user_id"], name: "index_projects_on_user_id"
end
create_table "tasks", force: :cascade do |t|
t.string "name"
t.string "priority"
t.datetime "due_date"
t.integer "user_id", null: false
t.integer "project_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "today", default: false
t.text "description"
t.boolean "completed", default: false
t.index ["project_id"], name: "index_tasks_on_project_id"
t.index ["user_id"], name: "index_tasks_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_foreign_key "areas", "users"
add_foreign_key "projects", "areas", on_delete: :cascade
add_foreign_key "projects", "users"
add_foreign_key "tasks", "projects", on_delete: :cascade
add_foreign_key "tasks", "users"
end

76
public/css/app.css Normal file
View file

@ -0,0 +1,76 @@
body {
font-family: 'Poppins', sans-serif;
font-size: 0.85rem;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.625rem;
}
h3 {
font-size: 1.375rem;
}
h4 {
font-size: 1.125rem;
}
h5 {
font-size: 0.925rem;
}
h6 {
font-size: 0.825rem;
}
.dropdown-menu,
.btn,
.form-control,
.form-select {
font-size: 0.85rem;
padding: .375rem .75rem;
}
.btn-lg {
font-size: 0.95rem;
padding: .5rem 1rem;
}
.btn-sm {
font-size: 0.75rem;
padding: .25rem .5rem;
}
.task-item:hover {
cursor: pointer;
}
/* sidebar */
.area-item .area-options {
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.area-item:hover .area-options {
visibility: visible;
opacity: 1;
}
.fixed-sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
overflow-y: auto;
}
/* main */
.main-content {
margin-left: 300px;
}

178
public/js/app.js Normal file
View file

@ -0,0 +1,178 @@
document.addEventListener("DOMContentLoaded", function () {
attachEventListeners();
});
function attachEventListeners() {
attachCollapseListeners();
attachTaskClickListeners();
attachProjectModalListeners();
attachAreaModalListeners();
}
function attachCollapseListeners() {
document.querySelectorAll('.collapse').forEach(collapseElement => {
collapseElement.addEventListener('show.bs.collapse', () => toggleFolderIcon(collapseElement, true));
collapseElement.addEventListener('hide.bs.collapse', () => toggleFolderIcon(collapseElement, false));
});
}
function toggleFolderIcon(collapseElement, isOpening) {
const closedFolderIcon = collapseElement.previousElementSibling.querySelector('.bi-folder');
const openFolderIcon = collapseElement.previousElementSibling.querySelector('.bi-folder2-open');
closedFolderIcon.classList.toggle('d-none', isOpening);
openFolderIcon.classList.toggle('d-none', !isOpening);
}
function attachTaskClickListeners() {
document.querySelectorAll('.task-item').forEach(taskElement => {
taskElement.addEventListener('click', event => {
if (!event.target.closest('.toggle-completion')) {
openEditTaskModal(taskElement.dataset.taskId);
}
});
});
}
function openEditTaskModal(taskId) {
const formContainer = document.getElementById('edit_task_form_' + taskId);
if (!formContainer) {
console.error('Edit form not found for task: ' + taskId);
return;
}
const formHtml = formContainer.innerHTML;
document.getElementById('editTaskFormContainer').innerHTML = formHtml;
new bootstrap.Modal(document.getElementById('editTaskModal')).show();
}
function attachProjectModalListeners() {
document.querySelectorAll('[data-bs-toggle="modal"][data-project-id]').forEach(button => {
button.addEventListener('click', () => openProjectModalForEdit(button.getAttribute('data-project-id')));
});
}
function openProjectModalForEdit(projectId) {
fetch('/project/' + projectId)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok.');
}
return response.json();
})
.then(projectData => {
document.getElementById('projectName').value = projectData.name;
document.getElementById('projectDescription').value = projectData.description || '';
var projectForm = document.getElementById('projectForm');
if (projectForm) {
projectForm.action = '/projects/' + projectId;
projectForm.method = 'patch';
}
var modal = new bootstrap.Modal(document.getElementById('editProjectModal'));
modal.show();
})
.catch(error => {
console.error('Error fetching project data:', error);
});
}
function attachAreaModalListeners() {
document.querySelectorAll('.open-new-area-modal').forEach(button => {
button.addEventListener('click', () => new bootstrap.Modal(document.getElementById('newAreaModal')).show());
});
document.querySelectorAll('.open-edit-area-modal').forEach(button => {
button.addEventListener('click', () => openEditAreaModal(button.dataset.areaId));
});
}
function openNewProjectModal() {
const modal = new bootstrap.Modal(document.getElementById('newProjectModal'));
modal.show();
}
function deleteProject(projectId) {
if (confirm('Are you sure you want to delete this project?')) {
const form = document.getElementById('delete_project_' + projectId);
form.submit();
}
}
function openNewAreaModal() {
const modal = new bootstrap.Modal(document.getElementById('newAreaModal'));
modal.show();
}
function deleteArea(areaId) {
if (confirm('Are you sure you want to delete this area?')) {
const form = document.getElementById('delete_area_' + areaId);
form.submit();
}
}
function openEditAreaModal(areaId) {
fetchAreaDataAndPopulateModal(areaId);
const modal = new bootstrap.Modal(document.getElementById('editAreaModal'));
modal.show();
}
function fetchAreaDataAndPopulateModal(areaId) {
fetch('/areas/' + areaId + '/data')
.then(response => response.json())
.then(areaData => {
populateAreaEditForm(areaData);
})
.catch(error => console.error('Error fetching area data:', error));
}
function populateAreaEditForm(areaData) {
document.getElementById('editAreaName').value = areaData.name;
}
function toggleTaskCompletion(event, taskId) {
event.stopPropagation();
fetch('/task/' + taskId + '/toggle_completion', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({_method: 'patch'})
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => updateTaskCompletionStatus(taskId, data))
.catch(error => console.error('There has been a problem with your fetch operation:', error));
}
function updateTaskCompletionStatus(taskId, data) {
const iconSpan = document.querySelector('.task-item[data-task-id="' + taskId + '"] .toggle-completion');
const taskIcon = iconSpan.querySelector('.bi');
const taskDiv = iconSpan.closest('.task-item');
if (data.completed) {
taskIcon.classList.remove('bi-circle', 'text-warning', 'text-danger');
taskIcon.classList.add('bi-check-circle-fill', 'text-success');
taskDiv.classList.add('opacity-50');
} else {
taskIcon.classList.remove('bi-check-circle-fill', 'text-success');
taskIcon.classList.add('bi-circle');
taskDiv.classList.remove('opacity-50');
applyPriorityColor(taskIcon, data.priority);
}
}
function applyPriorityColor(taskIcon, priority) {
taskIcon.classList.remove('text-warning', 'text-danger', 'text-secondary');
switch (priority) {
case 'Medium':
taskIcon.classList.add('text-warning');
break;
case 'High':
taskIcon.classList.add('text-danger');
break;
default:
taskIcon.classList.add('text-secondary');
}
}