commit
2e2efda125
28 changed files with 490 additions and 97 deletions
2
app.rb
2
app.rb
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
7
app/models/note.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
class Tag < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
has_and_belongs_to_many :tasks
|
||||
has_and_belongs_to_many :notes
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,4 +5,6 @@ class Task < ActiveRecord::Base
|
|||
|
||||
scope :complete, -> { where(completed: true) }
|
||||
scope :incomplete, -> { where(completed: false) }
|
||||
|
||||
validates :name, presence: true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
79
app/routes/notes_routes.rb
Normal file
79
app/routes/notes_routes.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' %>
|
||||
|
|
|
|||
13
app/views/notes/_edit_note_modal.erb
Normal file
13
app/views/notes/_edit_note_modal.erb
Normal 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
61
app/views/notes/_form.erb
Normal 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
28
app/views/notes/_note.erb
Normal 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
18
app/views/notes/index.erb
Normal 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' %>
|
||||
|
|
@ -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' %>
|
||||
|
|
|
|||
|
|
@ -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' %>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 } %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
db/migrate/20231116112552_create_notes.rb
Normal file
10
db/migrate/20231116112552_create_notes.rb
Normal 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
|
||||
|
|
@ -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
|
||||
5
db/migrate/20231117170940_add_title_to_notes.rb
Normal file
5
db/migrate/20231117170940_add_title_to_notes.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddTitleToNotes < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :notes, :title, :string
|
||||
end
|
||||
end
|
||||
5
db/migrate/20231117174412_add_project_to_notes.rb
Normal file
5
db/migrate/20231117174412_add_project_to_notes.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddProjectToNotes < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_reference :notes, :project, foreign_key: true
|
||||
end
|
||||
end
|
||||
22
db/schema.rb
22
db/schema.rb
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue