From de19e675c8a6efff766b57ba37bfeee42c621b14 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Mon, 26 Jan 2026 14:00:27 +0630 Subject: [PATCH 01/22] added Project model with project_name and Task model with title&status --- app/models/project.rb | 2 ++ app/models/task.rb | 2 ++ db/migrate/20260126072711_create_projects.rb | 9 +++++++++ db/migrate/20260126072814_create_tasks.rb | 10 ++++++++++ test/fixtures/projects.yml | 7 +++++++ test/fixtures/tasks.yml | 9 +++++++++ test/models/project_test.rb | 7 +++++++ test/models/task_test.rb | 7 +++++++ 8 files changed, 53 insertions(+) create mode 100644 app/models/project.rb create mode 100644 app/models/task.rb create mode 100644 db/migrate/20260126072711_create_projects.rb create mode 100644 db/migrate/20260126072814_create_tasks.rb create mode 100644 test/fixtures/projects.yml create mode 100644 test/fixtures/tasks.yml create mode 100644 test/models/project_test.rb create mode 100644 test/models/task_test.rb diff --git a/app/models/project.rb b/app/models/project.rb new file mode 100644 index 0000000..9c7fe3f --- /dev/null +++ b/app/models/project.rb @@ -0,0 +1,2 @@ +class Project < ApplicationRecord +end diff --git a/app/models/task.rb b/app/models/task.rb new file mode 100644 index 0000000..3c23424 --- /dev/null +++ b/app/models/task.rb @@ -0,0 +1,2 @@ +class Task < ApplicationRecord +end diff --git a/db/migrate/20260126072711_create_projects.rb b/db/migrate/20260126072711_create_projects.rb new file mode 100644 index 0000000..fdfd571 --- /dev/null +++ b/db/migrate/20260126072711_create_projects.rb @@ -0,0 +1,9 @@ +class CreateProjects < ActiveRecord::Migration[8.1] + def change + create_table :projects do |t| + t.string :project_name + + t.timestamps + end + end +end diff --git a/db/migrate/20260126072814_create_tasks.rb b/db/migrate/20260126072814_create_tasks.rb new file mode 100644 index 0000000..8ca7137 --- /dev/null +++ b/db/migrate/20260126072814_create_tasks.rb @@ -0,0 +1,10 @@ +class CreateTasks < ActiveRecord::Migration[8.1] + def change + create_table :tasks do |t| + t.string :title + t.string :status + + t.timestamps + end + end +end diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml new file mode 100644 index 0000000..0f9d978 --- /dev/null +++ b/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + project_name: MyString + +two: + project_name: MyString diff --git a/test/fixtures/tasks.yml b/test/fixtures/tasks.yml new file mode 100644 index 0000000..e41d61c --- /dev/null +++ b/test/fixtures/tasks.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + status: MyString + +two: + title: MyString + status: MyString diff --git a/test/models/project_test.rb b/test/models/project_test.rb new file mode 100644 index 0000000..5df4ca4 --- /dev/null +++ b/test/models/project_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ProjectTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/task_test.rb b/test/models/task_test.rb new file mode 100644 index 0000000..29982eb --- /dev/null +++ b/test/models/task_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TaskTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end -- 2.49.1 From e0ac034974abb481169ad41b8395ad0a3b6c38f7 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Mon, 26 Jan 2026 14:36:16 +0630 Subject: [PATCH 02/22] migrated db and defined Proj & Task relationship --- app/helpers/articles_helper.rb | 2 ++ app/models/project.rb | 1 + app/models/task.rb | 1 + app/views/articles/index.html.erb | 2 ++ db/schema.rb | 26 ++++++++++++++++++++++++++ 5 files changed, 32 insertions(+) create mode 100644 app/helpers/articles_helper.rb create mode 100644 app/views/articles/index.html.erb create mode 100644 db/schema.rb diff --git a/app/helpers/articles_helper.rb b/app/helpers/articles_helper.rb new file mode 100644 index 0000000..2968277 --- /dev/null +++ b/app/helpers/articles_helper.rb @@ -0,0 +1,2 @@ +module ArticlesHelper +end diff --git a/app/models/project.rb b/app/models/project.rb index 9c7fe3f..16ed525 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,2 +1,3 @@ class Project < ApplicationRecord + has_many :tasks end diff --git a/app/models/task.rb b/app/models/task.rb index 3c23424..1d95805 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1,2 +1,3 @@ class Task < ApplicationRecord + balongs_to :project end diff --git a/app/views/articles/index.html.erb b/app/views/articles/index.html.erb new file mode 100644 index 0000000..ab9c552 --- /dev/null +++ b/app/views/articles/index.html.erb @@ -0,0 +1,2 @@ +

Articles#index

+

Find me in app/views/articles/index.html.erb

diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..2e3d7dd --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,26 @@ +# 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[8.1].define(version: 2026_01_26_072814) do + create_table "projects", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "project_name" + t.datetime "updated_at", null: false + end + + create_table "tasks", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "status" + t.string "title" + t.datetime "updated_at", null: false + end +end -- 2.49.1 From 754a76fd5d448546daa4138dc4ffc78fdbf8b2a3 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Mon, 26 Jan 2026 15:05:12 +0630 Subject: [PATCH 03/22] db migration --- db/migrate/20260126082816_add_description_to_projects.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20260126082816_add_description_to_projects.rb diff --git a/db/migrate/20260126082816_add_description_to_projects.rb b/db/migrate/20260126082816_add_description_to_projects.rb new file mode 100644 index 0000000..2692093 --- /dev/null +++ b/db/migrate/20260126082816_add_description_to_projects.rb @@ -0,0 +1,5 @@ +class AddDescriptionToProjects < ActiveRecord::Migration[8.1] + def change + add_column :projects, :description, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e3d7dd..7379b52 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_01_26_072814) do +ActiveRecord::Schema[8.1].define(version: 2026_01_26_082816) do create_table "projects", force: :cascade do |t| t.datetime "created_at", null: false + t.string "description" t.string "project_name" t.datetime "updated_at", null: false end -- 2.49.1 From 6bbbc67a3cdd0d4e8bacf59efd6cb0077f906408 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Mon, 26 Jan 2026 16:45:15 +0630 Subject: [PATCH 04/22] Deleted bots --- .github/dependabot.yml | 12 --- .github/workflows/ci.yml | 124 -------------------------- .kamal/hooks/docker-setup.sample | 3 - .kamal/hooks/post-app-boot.sample | 3 - .kamal/hooks/post-deploy.sample | 14 --- .kamal/hooks/post-proxy-reboot.sample | 3 - .kamal/hooks/pre-app-boot.sample | 3 - .kamal/hooks/pre-build.sample | 51 ----------- .kamal/hooks/pre-connect.sample | 47 ---------- .kamal/hooks/pre-deploy.sample | 122 ------------------------- .kamal/hooks/pre-proxy-reboot.sample | 3 - .kamal/secrets | 20 ----- 12 files changed, 405 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/ci.yml delete mode 100755 .kamal/hooks/docker-setup.sample delete mode 100755 .kamal/hooks/post-app-boot.sample delete mode 100755 .kamal/hooks/post-deploy.sample delete mode 100755 .kamal/hooks/post-proxy-reboot.sample delete mode 100755 .kamal/hooks/pre-app-boot.sample delete mode 100755 .kamal/hooks/pre-build.sample delete mode 100755 .kamal/hooks/pre-connect.sample delete mode 100755 .kamal/hooks/pre-deploy.sample delete mode 100755 .kamal/hooks/pre-proxy-reboot.sample delete mode 100644 .kamal/secrets diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 83610cf..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: -- package-ecosystem: bundler - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 6824190..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: [ main ] - -jobs: - scan_ruby: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Scan for common Rails security vulnerabilities using static analysis - run: bin/brakeman --no-pager - - - name: Scan for known security vulnerabilities in gems used - run: bin/bundler-audit - - scan_js: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Scan for security vulnerabilities in JavaScript dependencies - run: bin/importmap audit - - lint: - runs-on: ubuntu-latest - env: - RUBOCOP_CACHE_ROOT: tmp/rubocop - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Prepare RuboCop cache - uses: actions/cache@v4 - env: - DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} - with: - path: ${{ env.RUBOCOP_CACHE_ROOT }} - key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} - restore-keys: | - rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- - - - name: Lint code for consistent style - run: bin/rubocop -f github - - test: - runs-on: ubuntu-latest - - # services: - # redis: - # image: valkey/valkey:8 - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Run tests - env: - RAILS_ENV: test - # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test - - system-test: - runs-on: ubuntu-latest - - # services: - # redis: - # image: valkey/valkey:8 - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Run System Tests - env: - RAILS_ENV: test - # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test:system - - - name: Keep screenshots from failed system tests - uses: actions/upload-artifact@v4 - if: failure() - with: - name: screenshots - path: ${{ github.workspace }}/tmp/screenshots - if-no-files-found: ignore diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample deleted file mode 100755 index 2fb07d7..0000000 --- a/.kamal/hooks/docker-setup.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample deleted file mode 100755 index 70f9c4b..0000000 --- a/.kamal/hooks/post-app-boot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample deleted file mode 100755 index fd364c2..0000000 --- a/.kamal/hooks/post-deploy.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -# A sample post-deploy hook -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) -# KAMAL_RUNTIME - -echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample deleted file mode 100755 index 1435a67..0000000 --- a/.kamal/hooks/post-proxy-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample deleted file mode 100755 index 45f7355..0000000 --- a/.kamal/hooks/pre-app-boot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample deleted file mode 100755 index c5a5567..0000000 --- a/.kamal/hooks/pre-build.sample +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh - -# A sample pre-build hook -# -# Checks: -# 1. We have a clean checkout -# 2. A remote is configured -# 3. The branch has been pushed to the remote -# 4. The version we are deploying matches the remote -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) - -if [ -n "$(git status --porcelain)" ]; then - echo "Git checkout is not clean, aborting..." >&2 - git status --porcelain >&2 - exit 1 -fi - -first_remote=$(git remote) - -if [ -z "$first_remote" ]; then - echo "No git remote set, aborting..." >&2 - exit 1 -fi - -current_branch=$(git branch --show-current) - -if [ -z "$current_branch" ]; then - echo "Not on a git branch, aborting..." >&2 - exit 1 -fi - -remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) - -if [ -z "$remote_head" ]; then - echo "Branch not pushed to remote, aborting..." >&2 - exit 1 -fi - -if [ "$KAMAL_VERSION" != "$remote_head" ]; then - echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 - exit 1 -fi - -exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample deleted file mode 100755 index 77744bd..0000000 --- a/.kamal/hooks/pre-connect.sample +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env ruby - -# A sample pre-connect check -# -# Warms DNS before connecting to hosts in parallel -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) -# KAMAL_RUNTIME - -hosts = ENV["KAMAL_HOSTS"].split(",") -results = nil -max = 3 - -elapsed = Benchmark.realtime do - results = hosts.map do |host| - Thread.new do - tries = 1 - - begin - Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) - rescue SocketError - if tries < max - puts "Retrying DNS warmup: #{host}" - tries += 1 - sleep rand - retry - else - puts "DNS warmup failed: #{host}" - host - end - end - - tries - end - end.map(&:value) -end - -retries = results.sum - hosts.size -nopes = results.count { |r| r == max } - -puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample deleted file mode 100755 index 05b3055..0000000 --- a/.kamal/hooks/pre-deploy.sample +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env ruby - -# A sample pre-deploy hook -# -# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. -# -# Fails unless the combined status is "success" -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_COMMAND -# KAMAL_SUBCOMMAND -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) - -# Only check the build status for production deployments -if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" - exit 0 -end - -require "bundler/inline" - -# true = install gems so this is fast on repeat invocations -gemfile(true, quiet: true) do - source "https://rubygems.org" - - gem "octokit" - gem "faraday-retry" -end - -MAX_ATTEMPTS = 72 -ATTEMPTS_GAP = 10 - -def exit_with_error(message) - $stderr.puts message - exit 1 -end - -class GithubStatusChecks - attr_reader :remote_url, :git_sha, :github_client, :combined_status - - def initialize - @remote_url = github_repo_from_remote_url - @git_sha = `git rev-parse HEAD`.strip - @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) - refresh! - end - - def refresh! - @combined_status = github_client.combined_status(remote_url, git_sha) - end - - def state - combined_status[:state] - end - - def first_status_url - first_status = combined_status[:statuses].find { |status| status[:state] == state } - first_status && first_status[:target_url] - end - - def complete_count - combined_status[:statuses].count { |status| status[:state] != "pending"} - end - - def total_count - combined_status[:statuses].count - end - - def current_status - if total_count > 0 - "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." - else - "Build not started..." - end - end - - private - def github_repo_from_remote_url - url = `git config --get remote.origin.url`.strip.delete_suffix(".git") - if url.start_with?("https://github.com/") - url.delete_prefix("https://github.com/") - elsif url.start_with?("git@github.com:") - url.delete_prefix("git@github.com:") - else - url - end - end -end - - -$stdout.sync = true - -begin - puts "Checking build status..." - - attempts = 0 - checks = GithubStatusChecks.new - - loop do - case checks.state - when "success" - puts "Checks passed, see #{checks.first_status_url}" - exit 0 - when "failure" - exit_with_error "Checks failed, see #{checks.first_status_url}" - when "pending" - attempts += 1 - end - - exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS - - puts checks.current_status - sleep(ATTEMPTS_GAP) - checks.refresh! - end -rescue Octokit::NotFound - exit_with_error "Build status could not be found" -end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample deleted file mode 100755 index 061f805..0000000 --- a/.kamal/hooks/pre-proxy-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets deleted file mode 100644 index b3089d6..0000000 --- a/.kamal/secrets +++ /dev/null @@ -1,20 +0,0 @@ -# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, -# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either -# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. - -# Example of extracting secrets from 1password (or another compatible pw manager) -# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) -# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) -# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) - -# Example of extracting secrets from Rails credentials -# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) - -# Use a GITHUB_TOKEN if private repositories are needed for the image -# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) - -# Grab the registry password from ENV -# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD - -# Improve security by using a password manager. Never check config/master.key into git! -RAILS_MASTER_KEY=$(cat config/master.key) -- 2.49.1 From f72af5d45f1d17e199ab8e1587189b1025cd2bc3 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Mon, 26 Jan 2026 21:50:14 +0630 Subject: [PATCH 05/22] Added enum to Task model. Added project to project to tasks. Changed Status data type in Tasks. --- Gemfile | 2 ++ Gemfile.lock | 3 +++ app/models/task.rb | 4 +++- db/migrate/20260126091632_change_status_type_in_tasks.rb | 6 ++++++ db/migrate/20260126094649_add_project_to_tasks.rb | 5 +++++ db/schema.rb | 8 ++++++-- 6 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20260126091632_change_status_type_in_tasks.rb create mode 100644 db/migrate/20260126094649_add_project_to_tasks.rb diff --git a/Gemfile b/Gemfile index c4aca79..9a38f1f 100644 --- a/Gemfile +++ b/Gemfile @@ -64,3 +64,5 @@ group :test do gem "capybara" gem "selenium-webdriver" end + +gem "tailwindcss", "~> 0.1.1" diff --git a/Gemfile.lock b/Gemfile.lock index 6700a33..a486f73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -351,6 +351,7 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.2.0) + tailwindcss (0.1.1) thor (1.5.0) thruster (0.1.17) thruster (0.1.17-aarch64-linux) @@ -415,6 +416,7 @@ DEPENDENCIES solid_queue sqlite3 (>= 2.1) stimulus-rails + tailwindcss (~> 0.1.1) thruster turbo-rails tzinfo-data @@ -552,6 +554,7 @@ CHECKSUMS sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + tailwindcss (0.1.1) sha256=cdd453b13267d6441cba86cc6bf2b91e06bf01171983d90701b8d9b634d3be75 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 thruster (0.1.17) sha256=6f3f1de43e22f0162d81cbc363f45ee42a1b8460213856c1a899cbf0d3297235 thruster (0.1.17-aarch64-linux) sha256=1b3a34b2814185c2aeaf835b5ecff5348cdcf8e77809f7a092d46e4b962a16ba diff --git a/app/models/task.rb b/app/models/task.rb index 1d95805..d994ea3 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1,3 +1,5 @@ class Task < ApplicationRecord - balongs_to :project + belongs_to :project + + enum :status, { to_do: 0, in_progress: 1, done: 2 } end diff --git a/db/migrate/20260126091632_change_status_type_in_tasks.rb b/db/migrate/20260126091632_change_status_type_in_tasks.rb new file mode 100644 index 0000000..5ac5381 --- /dev/null +++ b/db/migrate/20260126091632_change_status_type_in_tasks.rb @@ -0,0 +1,6 @@ +class ChangeStatusTypeInTasks < ActiveRecord::Migration[8.1] + def change + remove_column :tasks, :status, :string + add_column :tasks, :status, :integer, default: 0, null: false + end +end diff --git a/db/migrate/20260126094649_add_project_to_tasks.rb b/db/migrate/20260126094649_add_project_to_tasks.rb new file mode 100644 index 0000000..031681a --- /dev/null +++ b/db/migrate/20260126094649_add_project_to_tasks.rb @@ -0,0 +1,5 @@ +class AddProjectToTasks < ActiveRecord::Migration[8.1] + def change + add_reference :tasks, :project, null: false, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7379b52..2ea0ef3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_01_26_082816) do +ActiveRecord::Schema[8.1].define(version: 2026_01_26_094649) do create_table "projects", force: :cascade do |t| t.datetime "created_at", null: false t.string "description" @@ -20,8 +20,12 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_26_082816) do create_table "tasks", force: :cascade do |t| t.datetime "created_at", null: false - t.string "status" + t.integer "project_id", null: false + t.integer "status", default: 0, null: false t.string "title" t.datetime "updated_at", null: false + t.index ["project_id"], name: "index_tasks_on_project_id" end + + add_foreign_key "tasks", "projects" end -- 2.49.1 From fd0a6fb40835379e62cd9f86080460504ff9378e Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 10:18:48 +0630 Subject: [PATCH 06/22] Database seeding --- db/seeds.rb | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/db/seeds.rb b/db/seeds.rb index 4fbd6ed..24e2157 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,3 +7,43 @@ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| # MovieGenre.find_or_create_by!(name: genre_name) # end +# db/seeds.rb + +Task.destroy_all +Project.destroy_all + +project1 = Project.create!( + project_name: "project one", + description: "This is project one" +) + +project2 = Project.create!( + project_name: "project two", + description: "This is project two" +) + +Task.create!( + title: "first task of project one", + status: :to_do, + project: project1 +) + +Task.create!( + title: "second task of project one", + status: :in_progress, + project: project1 +) + +Task.create!( + title: "third task of project one", + status: :done, + project: project1 +) + +Task.create!( + title: "first task of project two", + status: :to_do, + project: project2 +) + +puts "Seeding complete" \ No newline at end of file -- 2.49.1 From 6c3180f203fb81c12381a5ea206705114d136765 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 11:43:00 +0630 Subject: [PATCH 07/22] Nested routes, Listed tasks associated with projects --- app/controllers/projects_controller.rb | 22 ++++++++++++++++++++++ app/helpers/projects_helper.rb | 2 ++ app/views/projects/index.html.erb | 7 +++++++ app/views/projects/new.html.erb | 17 +++++++++++++++++ app/views/projects/show.html.erb | 10 ++++++++++ config/routes.rb | 7 +++++++ 6 files changed, 65 insertions(+) create mode 100644 app/controllers/projects_controller.rb create mode 100644 app/helpers/projects_helper.rb create mode 100644 app/views/projects/index.html.erb create mode 100644 app/views/projects/new.html.erb create mode 100644 app/views/projects/show.html.erb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb new file mode 100644 index 0000000..1565773 --- /dev/null +++ b/app/controllers/projects_controller.rb @@ -0,0 +1,22 @@ +class ProjectsController < ApplicationController + def index + @projects = Project.all + end + + def show + @project = Project.find(params[:id]) + end + + def new + @project = Project.new + end + + def create + @project = Project.new(project_params) + if @project.save + redirect_to @project + else + render :new + end + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb new file mode 100644 index 0000000..db5c5ce --- /dev/null +++ b/app/helpers/projects_helper.rb @@ -0,0 +1,2 @@ +module ProjectsHelper +end diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb new file mode 100644 index 0000000..127fe18 --- /dev/null +++ b/app/views/projects/index.html.erb @@ -0,0 +1,7 @@ +

Projects

+ +
    + <% @projects.each do |project| %> +
  • <%= project.project_name %>

  • + <% end %> +
diff --git a/app/views/projects/new.html.erb b/app/views/projects/new.html.erb new file mode 100644 index 0000000..e9feebd --- /dev/null +++ b/app/views/projects/new.html.erb @@ -0,0 +1,17 @@ +

New Project

+ +<%= form_with model: @project do |form| %> +
+ <%= form.label :project_name %>
+ <%= form.text_field :project_name %> +
+ +
+ <%= form.label :description %>
+ <%= form.text_area :description %> +
+ +
+ <%= form.submit %> +
+<% end %> \ No newline at end of file diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb new file mode 100644 index 0000000..fbb2f53 --- /dev/null +++ b/app/views/projects/show.html.erb @@ -0,0 +1,10 @@ +

<%= @project.project_name %>

+

<%= @project.description %>

+ +
    +<% @project.tasks.each do |task| %> +
  • + <%= task.title %> (<%= task.status %>) +
  • + <% end %> +
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 48254e8..a656a21 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,11 @@ Rails.application.routes.draw do + + root "projects#index" + + resources :projects + + + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. -- 2.49.1 From ec595da8892473dd738f35c54473bc67927cbd13 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 16:50:25 +0630 Subject: [PATCH 08/22] Added New Task form --- app/views/tasks/new.html.erb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/views/tasks/new.html.erb diff --git a/app/views/tasks/new.html.erb b/app/views/tasks/new.html.erb new file mode 100644 index 0000000..890e5bb --- /dev/null +++ b/app/views/tasks/new.html.erb @@ -0,0 +1,22 @@ +

Add new task for "<%= @project.project_name %>"

+ +<%= form_with model: [@project, Task.new], local: true do |task| %> +

+ <%= task.label :title %>
+ <%= task.text_field :title %> + <% @project.tasks.each do |t| %> + <% t.errors.full_messages_for(:title).each do |message| %> +

<%= message %>

+ <% end %> + <% end %> +

+ +

+ <%= task.label :status %>
+ <%= task.select :status, Task.statuses.keys %> +

+ +

+ <%= task.submit "Create Task" %> +

+<% end %> \ No newline at end of file -- 2.49.1 From 142e14743acf683a4d82382e7d072fd119270c10 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 16:51:35 +0630 Subject: [PATCH 09/22] CRUD implementation for Projects and Tasks --- app/controllers/projects_controller.rb | 26 +++++++++++++ app/controllers/tasks_controller.rb | 53 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 app/controllers/tasks_controller.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 1565773..3cbf145 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -19,4 +19,30 @@ class ProjectsController < ApplicationController render :new end end + + def edit + @project = Project.find(params[:id]) + end + + def update + @project = Project.find(params[:id]) + if @project.update(project_params) + redirect_to @project + else + render :edit + end + end + + def destroy + @project = Project.find(params[:id]) + @project.destroy + redirect_to projects_path + end + + + private + + def project_params + params.require(:project).permit(:project_name, :description) + end end diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb new file mode 100644 index 0000000..3e56d2d --- /dev/null +++ b/app/controllers/tasks_controller.rb @@ -0,0 +1,53 @@ +class TasksController < ApplicationController + def index + @project = Project.find(params[:project_id]) + @tasks = @project.tasks + end + + def show + @project = Project.find(params[:project_id]) + @task = @project.tasks.find(params[:id]) + end + + def new + @project = Project.find(params[:project_id]) + @task = @project.tasks.new + end + + def create + @project = Project.find(params[:project_id]) + @task = @project.tasks.new(task_params) + if @task.save + redirect_to project_task_path(@project, @task) + else + render :new + end + end + + def edit + @project = Project.find(params[:project_id]) + @task = @project.tasks.find(params[:id]) + end + + def update + @project = Project.find(params[:project_id]) + @task = @project.tasks.find(params[:id]) + if @task.update(task_params) + redirect_to project_task_path(@project, @task) + else + render :edit + end + end + + def destroy + @project = Project.find(params[:project_id]) + @task = @project.tasks.find(params[:id]) + @task.destroy + redirect_to project_tasks_path(@project) + end + + private + def task_params + params.require(:task).permit(:title, :description, :status) + end +end -- 2.49.1 From cfca2bec27f39816b22d7980327c06577d6f1bdb Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 16:53:13 +0630 Subject: [PATCH 10/22] Configured route for Tasks --- config/routes.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index a656a21..d7c9dbc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,9 @@ Rails.application.routes.draw do root "projects#index" - resources :projects + resources :projects do + resources :tasks + end -- 2.49.1 From fbb09e40157f0d011334f1edb68ea6fa4c83fd00 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 16:55:07 +0630 Subject: [PATCH 11/22] Validate form data --- app/models/project.rb | 4 +++- app/models/task.rb | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 16ed525..1259445 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,3 +1,5 @@ class Project < ApplicationRecord - has_many :tasks + has_many :tasks, dependent: :destroy + + validates :project_name, :description, presence: true end diff --git a/app/models/task.rb b/app/models/task.rb index d994ea3..5f0de52 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -2,4 +2,6 @@ class Task < ApplicationRecord belongs_to :project enum :status, { to_do: 0, in_progress: 1, done: 2 } + + validates :title, presence: true end -- 2.49.1 From 910c1912e0eaa3f1cf1c21a45db2296cf3c308b2 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Tue, 27 Jan 2026 16:57:50 +0630 Subject: [PATCH 12/22] Added CRUD links for projects/tasks and validation display --- app/views/projects/index.html.erb | 7 ++++++- app/views/projects/new.html.erb | 6 ++++++ app/views/projects/show.html.erb | 8 +++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 127fe18..6c15d46 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -2,6 +2,11 @@
    <% @projects.each do |project| %> -
  • <%= project.project_name %>

  • +
  • <%= project.project_name %>

  • + + <%= link_to "Destroy", project_path(project), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> <% end %>
diff --git a/app/views/projects/new.html.erb b/app/views/projects/new.html.erb index e9feebd..62a2795 100644 --- a/app/views/projects/new.html.erb +++ b/app/views/projects/new.html.erb @@ -4,11 +4,17 @@
<%= form.label :project_name %>
<%= form.text_field :project_name %> + <% @project.errors.full_messages_for(:project_name).each do |message| %> +

<%= message %>

+ <% end %>
<%= form.label :description %>
<%= form.text_area :description %> + <% @project.errors.full_messages_for(:description).each do |message| %> +

<%= message %>

+ <% end %>
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index fbb2f53..d44d6b8 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -6,5 +6,11 @@
  • <%= task.title %> (<%= task.status %>)
  • + <%= link_to "Destroy", project_task_path(@project, task), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> <% end %> - \ No newline at end of file + + +<%= link_to "Add Task", new_project_task_path(@project) %> \ No newline at end of file -- 2.49.1 From 0f5d105be3497c55f32873d594c42062d3c46a05 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Wed, 28 Jan 2026 14:12:24 +0630 Subject: [PATCH 13/22] Used Turbo Frames/Streams for adding/deleting tasks without refresh entire browser --- app/controllers/tasks_controller.rb | 65 +++++++------------- app/helpers/tasks_helper.rb | 2 + app/views/projects/index.html.erb | 3 + app/views/projects/show.html.erb | 29 +++++---- app/views/tasks/_form.html.erb | 13 ++++ app/views/tasks/create.turbo_stream.erb | 13 ++++ app/views/tasks/destroy.turbo_stream.erb | 1 + app/views/tasks/new.html.erb | 22 ------- test/controllers/projects_controller_test.rb | 8 +++ test/controllers/tasks_controller_test.rb | 7 +++ 10 files changed, 87 insertions(+), 76 deletions(-) create mode 100644 app/helpers/tasks_helper.rb create mode 100644 app/views/tasks/_form.html.erb create mode 100644 app/views/tasks/create.turbo_stream.erb create mode 100644 app/views/tasks/destroy.turbo_stream.erb delete mode 100644 app/views/tasks/new.html.erb create mode 100644 test/controllers/projects_controller_test.rb create mode 100644 test/controllers/tasks_controller_test.rb diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 3e56d2d..52b5f46 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -1,53 +1,34 @@ class TasksController < ApplicationController - def index - @project = Project.find(params[:project_id]) - @tasks = @project.tasks - end + before_action :set_project - def show - @project = Project.find(params[:project_id]) - @task = @project.tasks.find(params[:id]) - end + def create + @task = @project.tasks.build(task_params) - def new - @project = Project.find(params[:project_id]) - @task = @project.tasks.new - end - - def create - @project = Project.find(params[:project_id]) - @task = @project.tasks.new(task_params) - if @task.save - redirect_to project_task_path(@project, @task) + if @task.save + respond_to do |format| + format.turbo_stream + format.html { redirect_to @project, notice: "Task created!" } + end else - render :new + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.replace("new_task", partial: "tasks/form", locals: { project: @project, task: @task }) } + format.html { render "projects/show", status: :unprocessable_entity } end end + end - def edit - @project = Project.find(params[:project_id]) - @task = @project.tasks.find(params[:id]) - end + def destroy + @task = @project.tasks.find(params[:id]) + @task.destroy + end - def update - @project = Project.find(params[:project_id]) - @task = @project.tasks.find(params[:id]) - if @task.update(task_params) - redirect_to project_task_path(@project, @task) - else - render :edit - end - end + private - def destroy - @project = Project.find(params[:project_id]) - @task = @project.tasks.find(params[:id]) - @task.destroy - redirect_to project_tasks_path(@project) - end + def set_project + @project = Project.find(params[:project_id]) + end - private - def task_params - params.require(:task).permit(:title, :description, :status) - end + def task_params + params.require(:task).permit(:title, :status) + end end diff --git a/app/helpers/tasks_helper.rb b/app/helpers/tasks_helper.rb new file mode 100644 index 0000000..ce894d0 --- /dev/null +++ b/app/helpers/tasks_helper.rb @@ -0,0 +1,2 @@ +module TasksHelper +end diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 6c15d46..6951f83 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -10,3 +10,6 @@ } %> <% end %> + +
    +

    <%= link_to "Add Project", new_project_path %>

    \ No newline at end of file diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index d44d6b8..1583b79 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -1,16 +1,21 @@

    <%= @project.project_name %>

    <%= @project.description %>

    -
      -<% @project.tasks.each do |task| %> -
    • - <%= task.title %> (<%= task.status %>) -
    • - <%= link_to "Destroy", project_task_path(@project, task), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> - <% end %> -
    + +
      + <% @project.tasks.each do |task| %> +
    • + <%= task.title %> (<%= task.status %>) + <%= link_to "Destroy", project_task_path(@project, task), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> +
    • + <% end %> +
    +
    -<%= link_to "Add Task", new_project_task_path(@project) %> \ No newline at end of file + + + <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %> + \ No newline at end of file diff --git a/app/views/tasks/_form.html.erb b/app/views/tasks/_form.html.erb new file mode 100644 index 0000000..f3c7ef7 --- /dev/null +++ b/app/views/tasks/_form.html.erb @@ -0,0 +1,13 @@ +<%= form_with model: [project, task], local: false do |f| %> +

    + <%= f.label :title %>
    + <%= f.text_field :title %> +

    + +

    + <%= f.label :status %>
    + <%= f.select :status, Task.statuses.keys %> +

    + + <%= f.submit "Create Task" %> +<% end %> diff --git a/app/views/tasks/create.turbo_stream.erb b/app/views/tasks/create.turbo_stream.erb new file mode 100644 index 0000000..24cb5e2 --- /dev/null +++ b/app/views/tasks/create.turbo_stream.erb @@ -0,0 +1,13 @@ +<%= turbo_stream.append "tasks_list" do %> +
  • + <%= @task.title %> (<%= @task.status %>) + <%= link_to "Destroy", project_task_path(@project, @task), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> +
  • +<% end %> + +<%= turbo_stream.replace "new_task" do %> + <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %> +<% end %> diff --git a/app/views/tasks/destroy.turbo_stream.erb b/app/views/tasks/destroy.turbo_stream.erb new file mode 100644 index 0000000..84da1cb --- /dev/null +++ b/app/views/tasks/destroy.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.remove dom_id(@task) %> \ No newline at end of file diff --git a/app/views/tasks/new.html.erb b/app/views/tasks/new.html.erb deleted file mode 100644 index 890e5bb..0000000 --- a/app/views/tasks/new.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -

    Add new task for "<%= @project.project_name %>"

    - -<%= form_with model: [@project, Task.new], local: true do |task| %> -

    - <%= task.label :title %>
    - <%= task.text_field :title %> - <% @project.tasks.each do |t| %> - <% t.errors.full_messages_for(:title).each do |message| %> -

    <%= message %>

    - <% end %> - <% end %> -

    - -

    - <%= task.label :status %>
    - <%= task.select :status, Task.statuses.keys %> -

    - -

    - <%= task.submit "Create Task" %> -

    -<% end %> \ No newline at end of file diff --git a/test/controllers/projects_controller_test.rb b/test/controllers/projects_controller_test.rb new file mode 100644 index 0000000..4305777 --- /dev/null +++ b/test/controllers/projects_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class ProjectsControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get projects_index_url + assert_response :success + end +end diff --git a/test/controllers/tasks_controller_test.rb b/test/controllers/tasks_controller_test.rb new file mode 100644 index 0000000..a60ed4a --- /dev/null +++ b/test/controllers/tasks_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TasksControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end -- 2.49.1 From 7c9e164a9a77b27265e96553c7243f93fbfc008d Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Wed, 28 Jan 2026 14:59:13 +0630 Subject: [PATCH 14/22] Added complete button --- app/controllers/tasks_controller.rb | 10 ++++++++++ app/views/projects/show.html.erb | 21 +++++++++++++-------- app/views/tasks/_task.html.erb | 16 ++++++++++++++++ app/views/tasks/complete.turbo_stream.erb | 10 ++++++++++ app/views/tasks/create.turbo_stream.erb | 8 +------- config/routes.rb | 6 +++++- 6 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 app/views/tasks/_task.html.erb create mode 100644 app/views/tasks/complete.turbo_stream.erb diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 52b5f46..203770d 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -22,6 +22,16 @@ class TasksController < ApplicationController @task.destroy end + def complete + @task = @project.tasks.find(params[:id]) + @task.update(status: "done") + + respond_to do |format| + format.turbo_stream + format.html { redirect_to @project } + end +end + private def set_project diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 1583b79..123e6c2 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -4,18 +4,23 @@
      <% @project.tasks.each do |task| %> -
    • - <%= task.title %> (<%= task.status %>) - <%= link_to "Destroy", project_task_path(@project, task), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> -
    • + +
    • + <%= task.title %> (<%= task.status %>) + + <%= link_to "Destroy", project_task_path(@project, task), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> + + <% unless task.status == "done" %> + <%= button_to "Complete", complete_project_task_path(@project, task), method: :patch, form: { data: { turbo_frame: dom_id(task) } } %> + <% end %> +
    • +
      <% end %>
    - +
    +

    Add New Task

    <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %>
    \ No newline at end of file diff --git a/app/views/tasks/_task.html.erb b/app/views/tasks/_task.html.erb new file mode 100644 index 0000000..77098b5 --- /dev/null +++ b/app/views/tasks/_task.html.erb @@ -0,0 +1,16 @@ + +
  • + <%= task.title %> (<%= task.status %>) + + <%= link_to "Destroy", project_task_path(task.project, task), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> + + <% unless task.status == "completed" %> + <%= button_to "Complete", complete_project_task_path(task.project, task), + method: :patch, + form: { data: { turbo_frame: dom_id(task) } } %> + <% end %> +
  • +
    diff --git a/app/views/tasks/complete.turbo_stream.erb b/app/views/tasks/complete.turbo_stream.erb new file mode 100644 index 0000000..058bf2a --- /dev/null +++ b/app/views/tasks/complete.turbo_stream.erb @@ -0,0 +1,10 @@ +<%= turbo_stream.replace dom_id(@task) do %> +
  • + <%= @task.title %> (<%= @task.status %>) + + <%= link_to "Destroy", project_task_path(@project, @task), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> +
  • +<% end %> diff --git a/app/views/tasks/create.turbo_stream.erb b/app/views/tasks/create.turbo_stream.erb index 24cb5e2..f63494f 100644 --- a/app/views/tasks/create.turbo_stream.erb +++ b/app/views/tasks/create.turbo_stream.erb @@ -1,11 +1,5 @@ <%= turbo_stream.append "tasks_list" do %> -
  • - <%= @task.title %> (<%= @task.status %>) - <%= link_to "Destroy", project_task_path(@project, @task), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> -
  • + <%= render partial: "tasks/task", locals: { task: @task } %> <% end %> <%= turbo_stream.replace "new_task" do %> diff --git a/config/routes.rb b/config/routes.rb index d7c9dbc..fa9fbf6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,11 @@ Rails.application.routes.draw do root "projects#index" resources :projects do - resources :tasks + resources :tasks do + member do + patch :complete + end + end end -- 2.49.1 From e3da79930c0e2a177123394e4bc6f33b33aeb08d Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Wed, 28 Jan 2026 15:27:17 +0630 Subject: [PATCH 15/22] Render 'complete' button only if the task's status is not 'done'. --- app/controllers/tasks_controller.rb | 23 ++++++++++++++++------- app/views/tasks/_task.html.erb | 17 ++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 203770d..b4d7211 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -18,19 +18,28 @@ class TasksController < ApplicationController end def destroy - @task = @project.tasks.find(params[:id]) + @task = @project.tasks.find_by(id: params[:id]) + return unless @task + + task_dom_id = "task_#{@task.id}" @task.destroy + + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.remove(task_dom_id) } + format.html { redirect_to @project } + end end + def complete - @task = @project.tasks.find(params[:id]) - @task.update(status: "done") + @task = @project.tasks.find(params[:id]) + @task.update(status: "done") - respond_to do |format| - format.turbo_stream - format.html { redirect_to @project } + respond_to do |format| + format.turbo_stream + format.html { redirect_to @project } + end end -end private diff --git a/app/views/tasks/_task.html.erb b/app/views/tasks/_task.html.erb index 77098b5..750ef9e 100644 --- a/app/views/tasks/_task.html.erb +++ b/app/views/tasks/_task.html.erb @@ -2,15 +2,14 @@
  • <%= task.title %> (<%= task.status %>) - <%= link_to "Destroy", project_task_path(task.project, task), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> + <%= link_to "Destroy", project_task_path(task.project, task), + data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> - <% unless task.status == "completed" %> - <%= button_to "Complete", complete_project_task_path(task.project, task), - method: :patch, - form: { data: { turbo_frame: dom_id(task) } } %> + <% unless task.status == "done" %> + <%= button_to "Complete", + complete_project_task_path(task.project, task), + method: :patch, + form: { data: { turbo_frame: dom_id(task) } } %> <% end %>
  • - + \ No newline at end of file -- 2.49.1 From 8a7d1716e268ac3f2a40ce93dbaf9b93a285cc4f Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Thu, 29 Jan 2026 11:13:04 +0630 Subject: [PATCH 16/22] Use find_by and task_dom_id to prevent errors when deleting tasks. --- app/controllers/tasks_controller.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 203770d..f72a3c2 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -18,8 +18,16 @@ class TasksController < ApplicationController end def destroy - @task = @project.tasks.find(params[:id]) + @task = @project.tasks.find_by(id: params[:id]) + return unless @task + + task_dom_id = "task_#{@task.id}" @task.destroy + + respond_to do |format| + format.turbo_stream { render turbo_stream: turbo_stream.remove(task_dom_id) } + format.html { redirect_to @project } + end end def complete -- 2.49.1 From 8a94b0795596e7a530d90a7157efb042c713aed6 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Fri, 30 Jan 2026 09:30:32 +0630 Subject: [PATCH 17/22] Fixed turbo frame and stream bug --- app/views/projects/show.html.erb | 26 +++++++++-------------- app/views/tasks/_task.html.erb | 21 +++++++++++------- app/views/tasks/complete.turbo_stream.erb | 9 +------- app/views/tasks/create.turbo_stream.erb | 6 +++--- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 123e6c2..908d09d 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -1,26 +1,20 @@

    <%= @project.project_name %>

    <%= @project.description %>

    - +<%= turbo_frame_tag "tasks" do %>
      - <% @project.tasks.each do |task| %> - -
    • - <%= task.title %> (<%= task.status %>) + <% # First: pending or in-progress tasks %> + <%= render @project.tasks.reject { |t| t.status == "done" } %> - <%= link_to "Destroy", project_task_path(@project, task), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> - - <% unless task.status == "done" %> - <%= button_to "Complete", complete_project_task_path(@project, task), method: :patch, form: { data: { turbo_frame: dom_id(task) } } %> - <% end %> -
    • -
      + <% done_tasks = @project.tasks.select { |t| t.status == "done" } %> + <% if done_tasks.any? %> + <%= render done_tasks %> <% end %>
    -
    +<% end %> - +<%= turbo_frame_tag "new_task" do %>

    Add New Task

    - <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %> -
    \ No newline at end of file + <%= render "tasks/form", project: @project, task: Task.new %> +<% end %> diff --git a/app/views/tasks/_task.html.erb b/app/views/tasks/_task.html.erb index 750ef9e..7edd2e7 100644 --- a/app/views/tasks/_task.html.erb +++ b/app/views/tasks/_task.html.erb @@ -1,15 +1,20 @@ -
  • - <%= task.title %> (<%= task.status %>) - - <%= link_to "Destroy", project_task_path(task.project, task), - data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> - +
  • +
    <%= task.title %>
    +
    <%= task.status.titleize %>
    +
    <% unless task.status == "done" %> <%= button_to "Complete", complete_project_task_path(task.project, task), method: :patch, - form: { data: { turbo_frame: dom_id(task) } } %> + data: { turbo_frame: dom_id(task) }, + class: "task-btn complete-btn" %> <% end %> -
  • + + <%= link_to "Destroy", + project_task_path(task.project, task), + data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, + class: "task-btn destroy-btn" %> +
    + \ No newline at end of file diff --git a/app/views/tasks/complete.turbo_stream.erb b/app/views/tasks/complete.turbo_stream.erb index 058bf2a..34efcab 100644 --- a/app/views/tasks/complete.turbo_stream.erb +++ b/app/views/tasks/complete.turbo_stream.erb @@ -1,10 +1,3 @@ <%= turbo_stream.replace dom_id(@task) do %> -
  • - <%= @task.title %> (<%= @task.status %>) - - <%= link_to "Destroy", project_task_path(@project, @task), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> -
  • + <%= render @task %> <% end %> diff --git a/app/views/tasks/create.turbo_stream.erb b/app/views/tasks/create.turbo_stream.erb index f63494f..9ec72d4 100644 --- a/app/views/tasks/create.turbo_stream.erb +++ b/app/views/tasks/create.turbo_stream.erb @@ -1,7 +1,7 @@ -<%= turbo_stream.append "tasks_list" do %> - <%= render partial: "tasks/task", locals: { task: @task } %> +<%= turbo_stream.append "tasks" do %> + <%= render @task %> <% end %> <%= turbo_stream.replace "new_task" do %> - <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %> + <%= render "tasks/form", project: @project, task: Task.new %> <% end %> -- 2.49.1 From 7f6123a6e68cf02d7c7c23497adc96642408b49d Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Fri, 30 Jan 2026 09:38:32 +0630 Subject: [PATCH 18/22] Show Done tasks and Fixed turbo frame and stream bug --- app/views/projects/show.html.erb | 26 +++++++++-------------- app/views/tasks/_task.html.erb | 21 +++++++++++------- app/views/tasks/complete.turbo_stream.erb | 9 +------- app/views/tasks/create.turbo_stream.erb | 6 +++--- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 123e6c2..908d09d 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -1,26 +1,20 @@

    <%= @project.project_name %>

    <%= @project.description %>

    - +<%= turbo_frame_tag "tasks" do %>
      - <% @project.tasks.each do |task| %> - -
    • - <%= task.title %> (<%= task.status %>) + <% # First: pending or in-progress tasks %> + <%= render @project.tasks.reject { |t| t.status == "done" } %> - <%= link_to "Destroy", project_task_path(@project, task), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> - - <% unless task.status == "done" %> - <%= button_to "Complete", complete_project_task_path(@project, task), method: :patch, form: { data: { turbo_frame: dom_id(task) } } %> - <% end %> -
    • -
      + <% done_tasks = @project.tasks.select { |t| t.status == "done" } %> + <% if done_tasks.any? %> + <%= render done_tasks %> <% end %>
    -
    +<% end %> - +<%= turbo_frame_tag "new_task" do %>

    Add New Task

    - <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %> -
    \ No newline at end of file + <%= render "tasks/form", project: @project, task: Task.new %> +<% end %> diff --git a/app/views/tasks/_task.html.erb b/app/views/tasks/_task.html.erb index 750ef9e..7edd2e7 100644 --- a/app/views/tasks/_task.html.erb +++ b/app/views/tasks/_task.html.erb @@ -1,15 +1,20 @@ -
  • - <%= task.title %> (<%= task.status %>) - - <%= link_to "Destroy", project_task_path(task.project, task), - data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> - +
  • +
    <%= task.title %>
    +
    <%= task.status.titleize %>
    +
    <% unless task.status == "done" %> <%= button_to "Complete", complete_project_task_path(task.project, task), method: :patch, - form: { data: { turbo_frame: dom_id(task) } } %> + data: { turbo_frame: dom_id(task) }, + class: "task-btn complete-btn" %> <% end %> -
  • + + <%= link_to "Destroy", + project_task_path(task.project, task), + data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, + class: "task-btn destroy-btn" %> + +
    \ No newline at end of file diff --git a/app/views/tasks/complete.turbo_stream.erb b/app/views/tasks/complete.turbo_stream.erb index 058bf2a..34efcab 100644 --- a/app/views/tasks/complete.turbo_stream.erb +++ b/app/views/tasks/complete.turbo_stream.erb @@ -1,10 +1,3 @@ <%= turbo_stream.replace dom_id(@task) do %> -
  • - <%= @task.title %> (<%= @task.status %>) - - <%= link_to "Destroy", project_task_path(@project, @task), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> -
  • + <%= render @task %> <% end %> diff --git a/app/views/tasks/create.turbo_stream.erb b/app/views/tasks/create.turbo_stream.erb index f63494f..9ec72d4 100644 --- a/app/views/tasks/create.turbo_stream.erb +++ b/app/views/tasks/create.turbo_stream.erb @@ -1,7 +1,7 @@ -<%= turbo_stream.append "tasks_list" do %> - <%= render partial: "tasks/task", locals: { task: @task } %> +<%= turbo_stream.append "tasks" do %> + <%= render @task %> <% end %> <%= turbo_stream.replace "new_task" do %> - <%= render partial: "tasks/form", locals: { project: @project, task: Task.new } %> + <%= render "tasks/form", project: @project, task: Task.new %> <% end %> -- 2.49.1 From 85937a3fcd3c965d696e8d4540651073289a21c9 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Fri, 30 Jan 2026 09:46:20 +0630 Subject: [PATCH 19/22] Make project list and its destroy buttons in 1 line --- app/views/projects/index.html.erb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 6951f83..507054a 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -2,12 +2,13 @@
      <% @projects.each do |project| %> -
    • <%= project.project_name %>

    • - - <%= link_to "Destroy", project_path(project), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> +
    • <%= link_to project.project_name, project_path(project) %> + + <%= link_to "Destroy", project_path(project), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> +
    • <% end %>
    -- 2.49.1 From 6716129d4a2b0cb302ff4a8645f3384e975c762b Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Fri, 30 Jan 2026 09:48:34 +0630 Subject: [PATCH 20/22] Added css for body & lists. Make done tasks obvious. --- app/assets/stylesheets/application.css | 42 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index fe93333..54cec86 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,10 +1,32 @@ -/* - * This is a manifest file that'll be compiled into application.css. - * - * With Propshaft, assets are served efficiently without preprocessing steps. You can still include - * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard - * cascading order, meaning styles declared later in the document or manifest will override earlier ones, - * depending on specificity. - * - * Consider organizing styles into separate files for maintainability. - */ +/* application.css */ + +body { + font-family: sans-serif; + background-color: #f9f9f9; + margin: 18px; + padding: 0; +} + +li { + border: 1px solid #ddd; + border-radius: 8px; + padding: 10px; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.task-actions { + display: flex; + gap: 8px; +} + +.done-task { + background-color: #f0f0f0; + color: #888; +} + +.done-task .task-title { + text-decoration: line-through; +} \ No newline at end of file -- 2.49.1 From 4c30a133d364f618d5d00baec6dd4725266b3557 Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Fri, 30 Jan 2026 11:29:17 +0630 Subject: [PATCH 21/22] Remove 'done' from task form. Add newest to top. Move done tasks to bottom --- app/views/projects/show.html.erb | 18 ++++++++++-------- app/views/tasks/_form.html.erb | 4 +++- app/views/tasks/_task.html.erb | 2 -- app/views/tasks/complete.turbo_stream.erb | 2 +- app/views/tasks/create.turbo_stream.erb | 6 +++--- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 908d09d..54609b0 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -2,19 +2,21 @@

    <%= @project.description %>

    <%= turbo_frame_tag "tasks" do %> -
      - <% # First: pending or in-progress tasks %> - <%= render @project.tasks.reject { |t| t.status == "done" } %> - <% done_tasks = @project.tasks.select { |t| t.status == "done" } %> - <% if done_tasks.any? %> - <%= render done_tasks %> +
        + <% @project.tasks.where(status: %w[to_do in_progress]).each do |task| %> + <%= render partial: "tasks/task", locals: { task: task } %> <% end %>
      + +
        + <% @project.tasks.where(status: "done").each do |task| %> + <%= render partial: "tasks/task", locals: { task: task } %> + <% end %> +
      + <% end %> <%= turbo_frame_tag "new_task" do %> -
      -

      Add New Task

      <%= render "tasks/form", project: @project, task: Task.new %> <% end %> diff --git a/app/views/tasks/_form.html.erb b/app/views/tasks/_form.html.erb index f3c7ef7..a6a22ea 100644 --- a/app/views/tasks/_form.html.erb +++ b/app/views/tasks/_form.html.erb @@ -1,3 +1,5 @@ +

      Add New Task

      + <%= form_with model: [project, task], local: false do |f| %>

      <%= f.label :title %>
      @@ -6,7 +8,7 @@

      <%= f.label :status %>
      - <%= f.select :status, Task.statuses.keys %> + <%= f.select :status, Task.statuses.keys.reject { |s| s == "done" } %>

      <%= f.submit "Create Task" %> diff --git a/app/views/tasks/_task.html.erb b/app/views/tasks/_task.html.erb index 7edd2e7..efd7312 100644 --- a/app/views/tasks/_task.html.erb +++ b/app/views/tasks/_task.html.erb @@ -1,4 +1,3 @@ -
    • <%= task.title %>
      <%= task.status.titleize %>
      @@ -17,4 +16,3 @@ class: "task-btn destroy-btn" %>
    • -
      \ No newline at end of file diff --git a/app/views/tasks/complete.turbo_stream.erb b/app/views/tasks/complete.turbo_stream.erb index 34efcab..7b6386f 100644 --- a/app/views/tasks/complete.turbo_stream.erb +++ b/app/views/tasks/complete.turbo_stream.erb @@ -1,3 +1,3 @@ <%= turbo_stream.replace dom_id(@task) do %> - <%= render @task %> + <%= render partial: "tasks/task", locals: { task: @task } %> <% end %> diff --git a/app/views/tasks/create.turbo_stream.erb b/app/views/tasks/create.turbo_stream.erb index 9ec72d4..67cfd7f 100644 --- a/app/views/tasks/create.turbo_stream.erb +++ b/app/views/tasks/create.turbo_stream.erb @@ -1,6 +1,6 @@ -<%= turbo_stream.append "tasks" do %> - <%= render @task %> -<% end %> +<%= turbo_stream.prepend "active_tasks" do %> + <%= render partial: "tasks/task", locals: { task: @task } %> + <% end %> <%= turbo_stream.replace "new_task" do %> <%= render "tasks/form", project: @project, task: Task.new %> -- 2.49.1 From 5b298a1e5963cf1c43cb1f74e8b94dea782e314f Mon Sep 17 00:00:00 2001 From: KaungKaung Date: Fri, 30 Jan 2026 12:04:49 +0630 Subject: [PATCH 22/22] Add done task percentage --- app/views/projects/index.html.erb | 41 ++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 507054a..071eb3a 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -1,16 +1,35 @@

      Projects

      -
        - <% @projects.each do |project| %> -
      • <%= link_to project.project_name, project_path(project) %> +
        +
          + <% @projects.each do |project| %> + <% total_tasks = project.tasks.count %> + <% done_tasks_count = project.tasks.where(status: "done").count %> - <%= link_to "Destroy", project_path(project), data: { - turbo_method: :delete, - turbo_confirm: "Are you sure?" - } %> - - <% end %> -
        +
      • +
        + <%= link_to project.project_name, project_path(project) %> +
        + + +
        + <%= link_to "Destroy", project_path(project), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure?" + } %> +
        + +
        + <% if total_tasks.zero? %> + No tasks yet + <% else %> + Done tasks: <%= ((done_tasks_count.to_f / total_tasks) * 100).round %>% + <% end %> +
        +
      • + <% end %> +
      +
      -

      <%= link_to "Add Project", new_project_path %>

      \ No newline at end of file +

      <%= link_to "Add Project", new_project_path %>

      -- 2.49.1