Merge pull request #11 from chrisvel/add_notes

Add notes
This commit is contained in:
Chris 2023-11-20 13:28:51 +02:00 committed by GitHub
commit 2e2efda125
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 490 additions and 97 deletions

2
app.rb
View file

@ -7,6 +7,7 @@ require './app/models/area'
require './app/models/project'
require './app/models/task'
require './app/models/tag'
require './app/models/note'
require './app/helpers/authentication_helper'
@ -14,6 +15,7 @@ require './app/routes/authentication_routes'
require './app/routes/tasks_routes'
require './app/routes/projects_routes'
require './app/routes/areas_routes'
require './app/routes/notes_routes'
helpers AuthenticationHelper

View file

@ -1,4 +1,6 @@
class Area < ActiveRecord::Base
belongs_to :user
has_many :projects, dependent: :destroy
validates :name, presence: true
end

7
app/models/note.rb Normal file
View file

@ -0,0 +1,7 @@
class Note < ActiveRecord::Base
belongs_to :user
belongs_to :project, optional: true
has_and_belongs_to_many :tags
validates :content, presence: true
end

View file

@ -2,7 +2,10 @@ class Project < ActiveRecord::Base
belongs_to :user
belongs_to :area, optional: true
has_many :tasks, dependent: :destroy
has_many :notes, dependent: :destroy
scope :with_incomplete_tasks, -> { joins(:tasks).where(tasks: { completed: false }).distinct }
scope :with_complete_tasks, -> { joins(:tasks).where(tasks: { completed: true }).distinct }
validates :name, presence: true
end

View file

@ -1,4 +1,5 @@
class Tag < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :tasks
has_and_belongs_to_many :notes
end

View file

@ -5,4 +5,6 @@ class Task < ActiveRecord::Base
scope :complete, -> { where(completed: true) }
scope :incomplete, -> { where(completed: false) }
validates :name, presence: true
end

View file

@ -5,6 +5,7 @@ class User < ActiveRecord::Base
has_many :projects
has_many :tasks
has_many :tags, dependent: :destroy
has_many :notes, dependent: :destroy
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true
end

View file

@ -0,0 +1,79 @@
class Sinatra::Application
def update_note_tags(note, tags_json)
return if tags_json.blank?
begin
tag_names = JSON.parse(tags_json).map { |tag| tag['value'] }.uniq
tags = tag_names.map do |name|
current_user.tags.find_or_create_by(name: name)
end
note.tags = tags
rescue JSON::ParserError
puts "Failed to parse JSON for tags: #{tags_json}"
end
end
get '/notes' do
@notes = current_user.notes.includes(:tags).order('title ASC')
erb :'notes/index'
end
post '/note/create' do
note_attributes = {
title: params[:title],
content: params[:content],
user_id: current_user.id
}
if params[:project_id].empty?
note = current_user.notes.build(note_attributes)
else
project = current_user.projects.find_by(id: params[:project_id])
halt 400, 'Invalid project.' unless project
note = project.notes.build(note_attributes)
end
if note.save
update_note_tags(note, params[:tags])
redirect request.referrer || '/'
else
halt 400, 'There was a problem creating the note.'
end
end
patch '/note/:id' do
note = current_user.notes.find_by(id: params[:id])
halt 404, 'Note not found.' unless note
note_attributes = {
title: params[:title],
content: params[:content]
}
if params[:project_id] && !params[:project_id].empty?
project = current_user.projects.find_by(id: params[:project_id])
halt 400, 'Invalid project.' unless project
note.project = project
else
note.project = nil
end
if note.update(note_attributes)
update_note_tags(note, params[:tags])
redirect request.referrer || '/'
else
halt 400, 'There was a problem updating the note.'
end
end
delete '/note/:id' do
note = current_user.notes.find_by(id: params[:id])
halt 404, 'Note not found.' unless note
if note.destroy!
redirect '/notes'
else
halt 400, 'There was a problem deleting the note.'
end
end
end

View file

@ -1,6 +1,6 @@
class Sinatra::Application
get '/projects' do
@projects_with_tasks = current_user.projects.includes(:tasks, :area).order(:name)
@projects_with_tasks = current_user.projects.includes(:tasks, :area).order('name ASC')
erb :'projects/index'
end

View file

@ -10,7 +10,7 @@
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet">
<link rel="stylesheet" href="/css/app.css">
</head>
<body class="container-fluid" style="background: #fcfcfc">
<body class="container-fluid">
<div class="row flex-nowrap">
<% if current_user %>
<%= partial :'sidebar' %>

View file

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

61
app/views/notes/_form.erb Normal file
View file

@ -0,0 +1,61 @@
<% action_url = note.new_record? ? '/note/create' : "/note/#{note.id}" %>
<% method = note.new_record? ? 'post' : 'patch' %>
<form action="<%= action_url %>" method="post">
<% unless note.new_record? %>
<input type="hidden" name="_method" value="<%= method %>">
<% end %>
<fieldset>
<div class="row mb-3">
<div class="col-md-12">
<div class="input-group input-group-lg">
<input type="text" id="note_name_<%= note.id %>" name="title" value="<%= note.title %>" class="form-control" placeholder="+ Add Title" required>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<label for="note_project" class="form-label">Project (optional):</label>
<select id="note_project_<%= note.id %>" name="project_id" class="form-select">
<option value="">No Project</option>
<% current_user.projects.each do |project| %>
<option value="<%= project.id %>" <%= 'selected' if note.project_id == project.id %>><%= project.name %></option>
<% end %>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<textarea rows="10" id="note_content_<%= note.id %>" name="content" class="form-control no-focus-outline" rows="5" placeholder="Note content..."><%= note.content %></textarea>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<input name="tags" id="note_tags_<%= note.id %>" class="form-control" value="<%= note.tags&.map(&:name)&.join(',') %>" placeholder="Add Tags">
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<%= note.new_record? ? 'Create Note' : 'Update Note' %>
</button>
<% unless note.new_record? %>
<button type="submit" class="btn btn-outline-danger" onclick="deleteNote('<%= note.id %>')">
<i class="bi bi-trash"></i>
</button>
<% end %>
</div>
</fieldset>
</form>
<% if !note.new_record? %>
<form id="delete_note_<%= note.id %>" action="/note/<%= note.id %>" method="post" class="d-none">
<input type="hidden" name="_method" value="delete">
</form>
<% end %>
<script>
function deleteNote(noteId) {
if (confirm('Are you sure you want to delete this note?')) {
event.preventDefault();
var form = document.getElementById('delete_note_' + noteId);
form.submit();
}
}
</script>

28
app/views/notes/_note.erb Normal file
View file

@ -0,0 +1,28 @@
<div class="border-0 bg-white shadow-sm mb-1 px-2 py-2 d-flex align-items-center note-item" data-note-id="<%= note.id %>">
<i class="fs-6 bi-journal-text me-2"></i>
<div class="row flex-grow-1 align-items-center">
<div class="col-md-4">
<div class="note-item">
<a href="#" class="link-dark  fs-5 text-decoration-none">
<%= note.title %>
</a>
<% if note.tags %>
<div class="ms-3 opacity-75 d-inline-block">
<% note.tags.each do |tag| %>
<span class="badge bg-primary-subtle text-primary rounded">
<%= tag.name %>
</span>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="col-md-3">
<% if note.project %>
<a href="/project/<%= note.project.id %>" class="badge border text-decoration-none link-dark bg-light">
<%= note.project.name %>
</a>
<% end %>
</div>
</div>
</div>

18
app/views/notes/index.erb Normal file
View file

@ -0,0 +1,18 @@
<h2 class="mb-5"><i class="bi bi-journal-text ms-3 me-2"></i> Notes</h2>
<div class="bg-light py-2 px-3 mx-3 d-flex align-items-center border" data-bs-toggle="collapse" data-bs-target="#newNoteForm" aria-expanded="false" aria-controls="newNoteForm" style="cursor: pointer;">
<i class="fs-4 bi bi-plus-circle-fill text-primary me-2"></i> <span class="fs-6">New note</span>
</div>
<div class="collapse" id="newNoteForm">
<div class="card border-secondary-subtle bg-light shadow-sm mt-2 p-4 mx-3">
<%= partial :'notes/_form', locals: { note: Note.new } %>
</div>
</div>
<div class="mx-3 my-4">
<% @notes.each do |note| %>
<div id="edit_note_form_<%= note.id %>" class="d-none">
<%= partial :'notes/_form', locals: { note: note } %>
</div>
<%= partial :'notes/_note', locals: {note: note} %>
<% end %>
</div>
<%= partial :'notes/_edit_note_modal' %>

View file

@ -1,37 +1,30 @@
<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 mt-2 p-4 mx-3">
<%= partial :'tasks/_form', locals: { task: Task.new } %>
</div>
</div>
<div class="mx-3 my-2">
<h4 class="mt-5 ms-4 fw-bold">Projects</h4>
<div class="row p-3">
<% if @projects_with_tasks.any? %>
<% @projects_with_tasks.each do |project| %>
<h5 class="mt-4 mb-2">
<a class="px-2 py-1 btn btn-outline-dark rounded-0 d-inline-block" href="/project/<%= project.id %>">
<%= project.name.upcase %>
<div class="col-md-3 mb-3">
<a class="text-decoration-none project-card" href="/project/<%= project.id %>">
<div class="card shadow-sm p-0" style="min-height: 177px;">
<div class="card-body p-0">
<div class="bg-light rounded" style="height: 100px;"></div>
<h5 class="card-title px-3 pt-3 pb-0 mb-1"><%= project.name.upcase %></h5>
<p class="card-text px-3 small text-black-50 opacity-75"><%= project.area.name %></p>
</div>
</div>
</a>
</h5>
<% if project.tasks.any? %>
<div class="mb-4">
<% project.tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
<%= partial :'tasks/_form', locals: { task: task } %>
</div>
<div class="">
<%= partial :'tasks/_task', locals: { task: task } %>
</div>
<% end %>
</div>
<% else %>
<div class="px-4 py-2 mb-4 bg-secondary-subtle fw-light opacity-50">
No tasks have been created yet for this project
</div>
<% end %>
</div>
<% end %>
<% end %>
<div class="col-md-3 mb-3">
<a class="text-decoration-none project-card"href="#" data-bs-toggle="modal" data-bs-target="#newProjectModal">
<div class="card shadow-sm border-light p-0" style="min-height: 177px;">
<div class="card-body bg-light rounded px-0 p-0 text-center">
<i class="bi bi-plus" style="font-size: 72px; line-height: 175px; color: #eee"></i>
</div>
</div>
</a>
</div>
</div>
<%= partial :'tasks/_edit_task_modal' %>

View file

@ -1,37 +1,40 @@
<h2 class="mb-5"><i class="bi bi-hexagon ms-3 me-2"></i><%= @project.name.upcase %>
<div class="dropdown d-inline-block">
<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></h2>
<div class="dropdown d-inline-block">
<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>
</h2>
<% 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 mb-4 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;">
<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">
<div class="card border-0 bg-white shadow-sm p-4 mt-3 mx-3">
<%= partial :'tasks/_form', locals: { task: Task.new(project: @project) } %>
</div>
</div>
<h4 class="mt-5 ms-4 fw-bold">Tasks</h4>
<div class="mx-3 my-2">
<% if @project.tasks.any? %>
<% @project.tasks.each do |task| %>
@ -42,7 +45,29 @@
<%= partial :'tasks/_task', locals: { task: task } %>
</div>
<% end %>
<% else %>
<div class="px-4 py-2 mb-4 bg-secondary-subtle fw-light opacity-50">
No tasks have been created yet for this project
</div>
<% end %>
</div>
<h4 class="mt-5 ms-4 fw-bold">Notes</h4>
<div class="mx-3 my-2">
<% if @project.notes.any? %>
<% @project.notes.each do |note| %>
<div id="edit_note_form_<%= note.id %>" class="d-none">
<%= partial :'notes/_form', locals: { note: note } %>
</div>
<%= partial :'notes/_note', locals: {note: note} %>
<% end %>
<% else %>
<div class="px-4 py-2 mb-4 bg-secondary-subtle fw-light opacity-50">
No notes have been created yet for this project
</div>
<% end %>
</div>
<%= partial :'projects/_edit_project_modal', locals: { project: @project } %>
<%= partial :'tasks/_edit_task_modal' %>
<%= partial :'notes/_edit_note_modal' %>

View file

@ -1,4 +1,4 @@
<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">
<div class="d-flex flex-column flex-shrink-0 p-3 vh-100 sidebar 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>
@ -33,18 +33,23 @@
<i class="bi bi-check-circle me-1"></i> Completed
</a>
</li>
<li>
<a href="/notes" class="<%= nav_link('/notes') %>">
<i class="bi bi-journal-text me-1"></i> Notes
</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">
<a href="#area_<%= area.id %>_projects" class="nav-link link-dark flex-grow-1" data-bs-toggle="collapse" aria-expanded="false">
<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">
<button class="btn btn-link pb-1 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 %>">
@ -59,7 +64,7 @@
</div>
</div>
<% if area.projects.any? %>
<div class="collapse show" id="area_<%= area.id %>_projects">
<div class="collapse" id="area_<%= area.id %>_projects">
<ul class="nav nav-pills flex-column">
<% area.projects.each do |project| %>
<li class="nav-item">
@ -73,9 +78,9 @@
<% end %>
<% end %>
</div>
<div class="">
<div class="mt-2">
<div class="dropdown">
<button class="btn btn-dark dropdown-toggle w-100" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-outline-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">
@ -90,7 +95,7 @@
<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><a class="dropdown-item disabled" href="#">Profile</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout">Sign out</a></li>
</ul>

View file

@ -8,23 +8,23 @@
<div class="row mb-3">
<div class="col-md-12">
<div class="input-group input-group-lg">
<input type="text" id="task_name" name="name" value="<%= task.name %>" class="form-control" placeholder="+ Add Task" required>
<input type="text" id="task_name_<%= task.id %>" name="name" value="<%= task.name %>" class="form-control" placeholder="+ Add Task" required>
</div>
</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">
<select id="task_project_<%= task.id %>" 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>
</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">
<select id="task_priority_<%= task.id %>" 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>
@ -32,12 +32,12 @@
</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">
<input type="date" id="task_due_date_<%= task.id %>" name="due_date" value="<%= task.due_date.strftime('%Y-%m-%d') if task.due_date %>" class="form-control">
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<input name="tags" id="task_tags" class="form-control" value="<%= task.tags&.map(&:name)&.join(',') %>" placeholder="Add Tags">
<input name="tags" id="task_tags_<%= task.id %>" class="form-control" value="<%= task.tags&.map(&:name)&.join(',') %>" placeholder="Add Tags">
</div>
</div>
<div class="mt-4">

View file

@ -1,16 +1,26 @@
<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="border-0 bg-white shadow-sm mb-1 px-2 py-2 d-flex align-items-center task-item <%= 'opacity-50' if task.completed %>" data-task-id="<%= task.id %>">
<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>
<div class="row flex-grow-1 align-items-center">
<div class="row flex-grow-1 align-items-top">
<div class="col-md-5">
<%= task.name %>
<% if task.tags %>
<div class="ms-3 opacity-75 d-inline-block">
<% task.tags.each do |tag| %>
<span class="badge bg-primary-subtle text-primary">
<i class="bi bi-tag-fill me-1 opacity-50"></i><%= tag.name %>
</span>
<% end %>
</div>
<% end %>
</div>
<div class="col-md-3">
<% task.tags.each do |tag| %>
<span class="badge bg-primary-subtle text-primary rounded">
<%= tag.name %>
</span>
<% if task.project %>
<a href="/project/<%= task.project.id %>" class="badge border text-decoration-none link-dark bg-light">
<%= task.project.name %>
</a>
<% end %>
</div>
<div class="col-md-3">

View file

@ -8,7 +8,7 @@
<%= partial :'tasks/_form', locals: { task: Task.new } %>
</div>
</div>
<div class="mx-3 mt-4 mb-4">
<div class="mx-3 mt-4">
<% if @tasks.any? %>
<% @tasks.each do |task| %>
<div id="edit_task_form_<%= task.id %>" class="d-none">
@ -18,13 +18,10 @@
<% end %>
<% end %>
</div>
<div class="mx-3 my-2">
<div class="mx-3 mb-2">
<% if @projects_with_tasks.any? %>
<% @projects_with_tasks.each do |project| %>
<% if project.tasks.any? %>
<h5 class="mt-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 } %>

View file

@ -1,13 +1,9 @@
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

View file

@ -0,0 +1,10 @@
class CreateNotes < ActiveRecord::Migration[7.1]
def change
create_table :notes do |t|
t.text :content
t.references :user, null: false, foreign_key: { on_delete: :cascade }
t.timestamps
end
end
end

View file

@ -0,0 +1,8 @@
class CreateJoinTableNotesTags < ActiveRecord::Migration[7.1]
def change
create_join_table :notes, :tags do |t|
t.index :note_id
t.index :tag_id
end
end
end

View file

@ -0,0 +1,5 @@
class AddTitleToNotes < ActiveRecord::Migration[7.1]
def change
add_column :notes, :title, :string
end
end

View file

@ -0,0 +1,5 @@
class AddProjectToNotes < ActiveRecord::Migration[7.1]
def change
add_reference :notes, :project, foreign_key: true
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2023_11_15_092055) do
ActiveRecord::Schema[7.1].define(version: 2023_11_17_174412) do
create_table "areas", force: :cascade do |t|
t.string "name"
t.integer "user_id", null: false
@ -19,6 +19,24 @@ ActiveRecord::Schema[7.1].define(version: 2023_11_15_092055) do
t.index ["user_id"], name: "index_areas_on_user_id"
end
create_table "notes", force: :cascade do |t|
t.text "content"
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "title"
t.integer "project_id"
t.index ["project_id"], name: "index_notes_on_project_id"
t.index ["user_id"], name: "index_notes_on_user_id"
end
create_table "notes_tags", id: false, force: :cascade do |t|
t.integer "note_id", null: false
t.integer "tag_id", null: false
t.index ["note_id"], name: "index_notes_tags_on_note_id"
t.index ["tag_id"], name: "index_notes_tags_on_tag_id"
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.integer "user_id", null: false
@ -69,6 +87,8 @@ ActiveRecord::Schema[7.1].define(version: 2023_11_15_092055) do
end
add_foreign_key "areas", "users"
add_foreign_key "notes", "projects"
add_foreign_key "notes", "users", on_delete: :cascade
add_foreign_key "projects", "areas", on_delete: :cascade
add_foreign_key "projects", "users"
add_foreign_key "tags", "users", on_delete: :cascade

View file

@ -1,6 +1,7 @@
body {
font-family: 'Poppins', sans-serif;
font-size: 0.85rem;
background: #f8f8f8;
}
h1 {
@ -49,6 +50,20 @@ h6 {
cursor: pointer;
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #eee;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: gray;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: gray;
}
/* sidebar */
.area-item .area-options {
visibility: hidden;
@ -73,15 +88,39 @@ h6 {
.main-content {
margin-left: 300px;
background: transparent;
}
.login-content {
margin-left: 200px;
}
/* sidebar */
.sidebar {
background: #ececec;
}
/* tasks, notes */
.task-item:hover, .note-item:hover {
background: #fafafa !important;
}
/* task form */
#task_name::placeholder {
color: #ccc;
opacity: 1; /* Firefox */
}
/* note form */
.no-focus-outline textarea:focus,
.no-focus-outline textarea:-moz-focusring {
outline: none !important;
}
/* project card */
a.project-card .card:hover {
border: 1px solid #777 !important;
}

View file

@ -1,13 +1,20 @@
document.addEventListener("DOMContentLoaded", function () {
attachEventListeners();
new Tagify(document.getElementById('task_tags'));
if (document.getElementById('task_tags_')) {
new Tagify(document.getElementById('task_tags_'));
}
if (document.getElementById('note_tags_')) {
new Tagify(document.getElementById('note_tags_'));
}
});
function attachEventListeners() {
attachCollapseListeners();
manageAreaState();
attachTaskClickListeners();
attachProjectModalListeners();
attachAreaModalListeners();
attachNoteClickListeners();
}
function attachCollapseListeners() {
@ -17,6 +24,44 @@ function attachCollapseListeners() {
});
}
function manageAreaState() {
// Check and set the state of areas on page load
document.querySelectorAll('.area-item a.nav-link').forEach(link => {
const areaId = link.getAttribute('href').replace('#', '');
const areaElement = document.getElementById(areaId);
if (areaElement) {
const isExpanded = localStorage.getItem('#' + areaId) === 'true';
if (isExpanded) {
link.setAttribute('aria-expanded', 'true');
areaElement.classList.add('show');
} else {
link.setAttribute('aria-expanded', 'false');
areaElement.classList.remove('show');
}
}
});
// Save the state of areas when clicked
document.querySelectorAll('.area-item a.nav-link').forEach(link => {
link.addEventListener('click', function (event) {
event.preventDefault(); // Prevent default action to manually control collapse
const areaId = this.getAttribute('href').replace('#', '');
const isExpanded = this.getAttribute('aria-expanded') === 'false'; // Toggle the state based on current state
localStorage.setItem('#' + areaId, isExpanded);
if (isExpanded) {
this.setAttribute('aria-expanded', 'true');
document.getElementById(areaId).classList.add('show');
} else {
this.setAttribute('aria-expanded', 'false');
document.getElementById(areaId).classList.remove('show');
}
});
});
}
// Ensure this function is called when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", manageAreaState);
function toggleFolderIcon(collapseElement, isOpening) {
const closedFolderIcon = collapseElement.previousElementSibling?.querySelector('.bi-folder');
const openFolderIcon = collapseElement.previousElementSibling?.querySelector('.bi-folder2-open');
@ -47,18 +92,40 @@ function openEditTaskModal(taskId) {
const editTaskFormContainer = document.getElementById('editTaskFormContainer');
editTaskFormContainer.innerHTML = formHtml;
new Tagify(editTaskFormContainer.querySelector('#task_tags'));
new Tagify(editTaskFormContainer.querySelector('#task_tags_' + taskId));
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 attachNoteClickListeners() {
document.querySelectorAll('.note-item').forEach(noteElement => {
noteElement.addEventListener('click', event => {
openEditNoteModal(noteElement.dataset.noteId);
});
});
}
function openEditNoteModal(noteId) {
const formContainer = document.getElementById('edit_note_form_' + noteId);
if (!formContainer) {
console.error('Edit form not found for note: ' + noteId);
return;
}
const formHtml = formContainer.innerHTML;
const editNoteFormContainer = document.getElementById('editNoteFormContainer');
editNoteFormContainer.innerHTML = formHtml;
new Tagify(editNoteFormContainer.querySelector('#note_tags_' + noteId));
new bootstrap.Modal(document.getElementById('editNoteModal')).show();
}
function openProjectModalForEdit(projectId) {
fetch('/project/' + projectId)
.then(response => {
@ -141,15 +208,15 @@ function toggleTaskCompletion(event, taskId) {
event.stopPropagation();
fetch('/task/' + taskId + '/toggle_completion', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({_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));
.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) {
@ -171,7 +238,6 @@ function updateTaskCompletionStatus(taskId, data) {
setTimeout(() => taskDiv.remove(), 200);
}
function applyPriorityColor(taskIcon, priority) {
taskIcon.classList.remove('text-warning', 'text-danger', 'text-secondary');
switch (priority) {
@ -185,6 +251,3 @@ function applyPriorityColor(taskIcon, priority) {
taskIcon.classList.add('text-secondary');
}
}