book_reading_tracker_gem 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7f25d1c0dfc83b1badfa94890012e70c1d9001603cab9a6a753bef8b456f848e
4
+ data.tar.gz: d6c826406fb592038d48f1e404b9d3bfafbc93ebbd3f6c532fd712bfb3223968
5
+ SHA512:
6
+ metadata.gz: b59cfb93ae0890836b20aef5cfc854b5b2a97eee9c7b628105c2609f08c156543b80fa376bad2e3e64b134e319f8e4e5200feb53d29facbf16fe006974b004b2
7
+ data.tar.gz: 1f5b1608bc853e24ce88ada7d0d95ba3b7400950f1b3e22adddcc600199fe5d1683f63eff03e668f3d73720195aa11d5982d45e522dca61e0d6ddd9fc8ef7635
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'dotenv/load'
5
+ require 'logger'
6
+
7
+ class DatabaseConnection
8
+ def self.connect
9
+ # Kiểm tra xem đã connect chưa để tránh connect lại
10
+ return if ActiveRecord::Base.connected?
11
+
12
+ database_mode = ENV.fetch('DATABASE_MODE', 'supabase')
13
+
14
+ database_url = case database_mode
15
+ when 'supabase'
16
+ ENV.fetch('DATABASE_URL_SUPABASE',
17
+ 'postgresql://postgres.rfvveqejqtxfszsgdzml:Admin123%40@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres')
18
+ when 'local'
19
+ ENV.fetch('DATABASE_URL_LOCAL',
20
+ 'postgresql://vantrong:Admin123%40@localhost:5432/book_reading_tracker')
21
+ end
22
+
23
+ if database_url.nil?
24
+ puts "DATABASE_URL not found for mode #{database_mode}"
25
+ return
26
+ end
27
+
28
+ begin
29
+ ActiveRecord::Base.establish_connection(database_url)
30
+ ActiveRecord::Base.logger = Logger.new($stdout)
31
+ puts "Successfully connected to the database in #{database_mode} mode!"
32
+ rescue StandardError => e
33
+ puts "Failed to connect to the database: #{e.message}"
34
+ end
35
+ end
36
+
37
+ def self.disconnect
38
+ return unless ActiveRecord::Base.connected?
39
+
40
+ begin
41
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
42
+ # Clear prepared statements ( để tranh lỗi khi chạy câu lệnh này mà tiếp tục chạy lệnh khác)
43
+ conn.execute('DISCARD ALL;')
44
+ end
45
+ ActiveRecord::Base.connection_pool.disconnect!
46
+ puts 'Successfully disconnected from the database!'
47
+ rescue StandardError => e
48
+ puts "Error while disconnecting: #{e.message}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'dotenv/load'
5
+ require 'logger'
6
+ require 'pg'
7
+ require_relative '../config/database_connection'
8
+ require_relative '../db/seeds'
9
+
10
+ module BookReadingTrackerGem
11
+ class DatabaseTasks
12
+ def initialize
13
+ @db_name = ENV.fetch('DATABASE_NAME', nil)
14
+ @database_mode = ENV.fetch('DATABASE_MODE', 'local')
15
+ @database_url = if @database_mode == 'supabase'
16
+ ENV.fetch('DATABASE_URL_SUPABASE',
17
+ 'postgresql://postgres.rfvveqejqtxfszsgdzml:Admin123%40@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres')
18
+ else
19
+ ENV.fetch('DATABASE_URL_LOCAL',
20
+ 'postgresql://vantrong:Admin123%40@localhost:5432/book_reading_tracker')
21
+ end
22
+ end
23
+
24
+ def connect
25
+ DatabaseConnection.connect
26
+ end
27
+
28
+ def migration_context
29
+ migrations_paths = File.expand_path('../db/migrate', __dir__)
30
+ @migration_context ||= ActiveRecord::MigrationContext.new(migrations_paths)
31
+ end
32
+
33
+ def create
34
+ connect
35
+
36
+ begin
37
+ connection = PG.connect(dbname: 'postgres')
38
+ if database_exists?(connection, @db_name)
39
+ puts "Lỗi: Database '#{@db_name}' đã tồn tại."
40
+ else
41
+ connection.exec("CREATE DATABASE #{@db_name}")
42
+ puts "Đã tạo database: #{@db_name} thành công."
43
+ end
44
+ ensure
45
+ connection&.close
46
+ end
47
+ rescue StandardError => e
48
+ puts "Lỗi khi tạo database: #{e.message}"
49
+ end
50
+
51
+ def database_exists?(connection, db_name)
52
+ result = connection.exec("SELECT 1 FROM pg_database WHERE datname = '#{db_name}'")
53
+ result.ntuples.positive?
54
+ end
55
+
56
+ def drop
57
+ begin
58
+ connection = PG.connect(dbname: 'postgres')
59
+ connection.exec("SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '#{@db_name}' AND pid <> pg_backend_pid();")
60
+
61
+ if database_exists?(connection, @db_name)
62
+ connection.exec("DROP DATABASE #{@db_name}")
63
+ puts "Đã xóa database: #{@db_name}."
64
+ else
65
+ puts "Lỗi khi xóa database: Database #{@db_name} không tồn tại."
66
+ end
67
+ ensure
68
+ connection&.close
69
+ end
70
+ rescue StandardError => e
71
+ puts "Lỗi khi xóa database: #{e.message}"
72
+ end
73
+
74
+ def migrate
75
+ connect
76
+
77
+ begin
78
+ connection = PG.connect(@database_url)
79
+ unless database_exists?(connection, @db_name)
80
+ puts "Lỗi: Database '#{@db_name}' không tồn tại."
81
+ return
82
+ end
83
+
84
+ migration_context.migrate
85
+ puts 'Migrate thành công.'
86
+ ensure
87
+ connection&.close
88
+ end
89
+ rescue StandardError => e
90
+ puts "Lỗi khi migrate: #{e.message}"
91
+ end
92
+
93
+ def reset
94
+ drop
95
+ create
96
+ migrate
97
+ seed
98
+ puts 'Reset database thành công.'
99
+ rescue StandardError => e
100
+ puts "Lỗi khi reset database: #{e.message}"
101
+ end
102
+
103
+ def seed
104
+ connect
105
+
106
+ begin
107
+ connection = PG.connect(@database_url)
108
+ unless database_exists?(connection, @db_name)
109
+ puts "Lỗi: Database '#{@db_name}' không tồn tại."
110
+ return
111
+ end
112
+
113
+ Seeder.run
114
+ puts 'Seed dữ liệu thành công.'
115
+ ensure
116
+ connection&.close
117
+ end
118
+ rescue StandardError => e
119
+ puts "Lỗi khi seed dữ liệu: #{e.message}"
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBooks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :books do |t|
6
+ t.string :title, null: false
7
+ t.text :description
8
+ t.string :isbn
9
+ t.integer :published_year
10
+ t.timestamps
11
+ end
12
+ add_index :books, :title
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAuthors < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :authors do |t|
6
+ t.string :author_name, null: false
7
+ t.text :biography
8
+ t.timestamps
9
+ end
10
+ add_index :authors, :author_name
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBookAuthors < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :book_authors do |t|
6
+ t.references :book, null: false, foreign_key: true
7
+ t.references :author, null: false, foreign_key: true
8
+ t.index %i[book_id author_id], unique: true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCategories < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :categories do |t|
6
+ t.string :category_name, null: false
7
+ t.timestamps
8
+ end
9
+ add_index :categories, :category_name
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBookCategories < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :book_categories do |t|
6
+ t.references :book, null: false, foreign_key: true
7
+ t.references :category, null: false, foreign_key: true
8
+ t.index %i[book_id category_id], unique: true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateReadingProgresses < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :reading_progresses do |t|
6
+ t.references :book, null: false, foreign_key: true
7
+ t.string :status, null: false, default: 'unread'
8
+ t.integer :pages_read, default: 0
9
+ t.integer :total_pages, default: 0
10
+ t.date :started_at
11
+ t.timestamps
12
+ end
13
+ add_index :reading_progresses, :status
14
+ end
15
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/book_reading_tracker_gem/models/book'
4
+ require_relative '../lib/book_reading_tracker_gem/models/author'
5
+ require_relative '../lib/book_reading_tracker_gem/models/book_author'
6
+ require_relative '../lib/book_reading_tracker_gem/models/category'
7
+ require_relative '../lib/book_reading_tracker_gem/models/book_category'
8
+ require_relative '../lib/book_reading_tracker_gem/models/reading_progress'
9
+
10
+ class Seeder
11
+ def self.run
12
+ dependencies = [ReadingProgress, BookCategory, BookAuthor, Category, Author, Book]
13
+ dependencies.each(&:destroy_all)
14
+
15
+ book1 = Book.create!(title: 'Ruby Basics', description: 'Learn Ruby', isbn: '1234567890', published_year: 2021)
16
+ book2 = Book.create!(title: 'Rails Advanced', description: 'Mastering Rails', isbn: '0987654321',
17
+ published_year: 2022)
18
+
19
+ author1 = Author.create!(author_name: 'John Doe', biography: 'Ruby developer')
20
+ author2 = Author.create!(author_name: 'Jane Smith', biography: 'Rails expert')
21
+
22
+ category1 = Category.create!(category_name: 'Programming')
23
+ category2 = Category.create!(category_name: 'Web Development')
24
+
25
+ BookAuthor.create!(book: book1, author: author1)
26
+ BookAuthor.create!(book: book1, author: author2)
27
+ BookAuthor.create!(book: book2, author: author2)
28
+
29
+ BookCategory.create!(book: book1, category: category1)
30
+ BookCategory.create!(book: book2, category: category2)
31
+
32
+ ReadingProgress.create!(book: book1, status: 'reading', pages_read: 50, total_pages: 200,
33
+ started_at: Date.today - 7)
34
+ ReadingProgress.create!(book: book2, status: 'unread', pages_read: 0, total_pages: 300)
35
+
36
+ puts 'Đã seed dữ liệu thành công!'
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/book_reading_tracker_gem'
5
+ BookReadingTrackerGem::CLI.start(ARGV)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../services/book_service'
5
+ require_relative '../services/author_service'
6
+ require_relative '../services/category_service'
7
+
8
+ module BookReadingTrackerGem
9
+ class CLI < Thor
10
+ # Quản lý sách
11
+ desc 'add_book TITLE --author AUTHOR --pages PAGES [--description DESCRIPTION] [--isbn ISBN] [--published_year YEAR]',
12
+ 'Thêm sách mới vào danh sách'
13
+ option :author, required: true
14
+ option :pages, required: true, type: :numeric
15
+ option :description, required: false
16
+ option :isbn, required: false
17
+ option :published_year, required: false, type: :numeric
18
+ def add_book(title)
19
+ # ruby bin/book add_book "Ruby Programming" --author "David Heinemeier Hansson" --pages 300 --description "Learn Ruby with Rails" --isbn "978-0134596882" --published_year 2023
20
+ BookService.add_book(
21
+ title,
22
+ options[:author],
23
+ options[:pages],
24
+ options[:description],
25
+ options[:isbn],
26
+ options[:published_year]
27
+ )
28
+ end
29
+
30
+ desc 'remove_book ID', 'Xóa sách khỏi danh sách'
31
+ # ruby bin/book remove_book 1
32
+ def remove_book(id)
33
+ BookService.remove_book(id)
34
+ end
35
+
36
+ desc 'progress_book ID --page PAGE', 'Cập nhật tiến độ đọc sách'
37
+ option :page, required: true, type: :numeric
38
+ def progress_book(id)
39
+ # ruby bin/book progress_book 2 --page 150
40
+ BookService.update_progress(id, options[:page])
41
+ end
42
+
43
+ desc 'list_books', 'Hiển thị danh sách sách'
44
+ # ruby bin/book list_books
45
+ def list_books
46
+ BookService.list_books
47
+ end
48
+
49
+ desc 'stats', 'Hiển thị thống kê sách'
50
+ # ruby bin/book stats
51
+ def stats
52
+ BookService.stats
53
+ end
54
+
55
+ desc 'show_progress ID', 'Xem tiến độ đọc sách'
56
+ # ruby bin/book show_progress "Ruby Programming"
57
+ def show_progress(id)
58
+ BookService.show_progress(id)
59
+ end
60
+
61
+ # Quản lý tác giả
62
+ desc 'add_author NAME [--biography BIOGRAPHY]', 'Thêm tác giả mới'
63
+ option :biography, required: false
64
+ def add_author(name)
65
+ # ruby bin/book add_author "J.K. Rowling" --biography "Tác giả của Harry Potter"
66
+ AuthorService.add_author(name, options[:biography])
67
+ end
68
+
69
+ desc 'list_authors', 'Hiển thị danh sách tác giả'
70
+ def list_authors
71
+ # ruby bin/book list_authors
72
+ AuthorService.list_authors
73
+ end
74
+
75
+ # Quản lý danh mục
76
+ desc 'add_category NAME', 'Thêm danh mục mới'
77
+ def add_category(name)
78
+ # ruby bin/book add_category "Fiction"
79
+ CategoryService.add_category(name)
80
+ end
81
+
82
+ desc 'list_categories', 'Hiển thị danh sách danh mục'
83
+ # ruby bin/book list_categories
84
+ def list_categories
85
+ CategoryService.list_categories
86
+ end
87
+
88
+ # Override Thor's error handling for invalid options
89
+ def self.exit_on_failure?
90
+ true
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ class Author < ActiveRecord::Base
5
+ has_many :book_authors67
6
+ has_many :books, through: :book_authors
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ class Book < ActiveRecord::Base
5
+ has_many :book_authors
6
+ has_many :authors, through: :book_authors
7
+
8
+ has_many :book_categories
9
+ has_many :categories, through: :book_categories
10
+
11
+ has_one :reading_progress
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ class BookAuthor < ActiveRecord::Base
5
+ belongs_to :book
6
+ belongs_to :author
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ class BookCategory < ActiveRecord::Base
5
+ belongs_to :book
6
+ belongs_to :category
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ class Category < ActiveRecord::Base
5
+ has_many :book_categories
6
+ has_many :books, through: :book_categories
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ class ReadingProgress < ActiveRecord::Base
5
+ belongs_to :book
6
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/author'
4
+ require_relative '../../../config/database_connection'
5
+ require_relative '../utils/helpers'
6
+
7
+ module BookReadingTrackerGem
8
+ class AuthorService
9
+ def self.add_author(name, biography = nil)
10
+ DatabaseConnection.connect
11
+ Author.create!(author_name: name, biography: biography)
12
+ puts "Add author: #{name} successfully."
13
+ rescue StandardError => e
14
+ puts "Error adding author: #{e.message}"
15
+ ensure
16
+ DatabaseConnection.disconnect
17
+ end
18
+
19
+ def self.list_authors
20
+ DatabaseConnection.connect
21
+ authors = Author.all
22
+
23
+ if authors.empty?
24
+ puts 'Authors not found.'
25
+ return
26
+ end
27
+
28
+ header = %w[id author_name biography]
29
+
30
+ rows = authors.map do |author|
31
+ [
32
+ author.id,
33
+ author.author_name || 'N/A',
34
+ author.biography || 'N/A'
35
+ ]
36
+ end
37
+
38
+ CommonUtils.render_table(header, rows)
39
+ rescue StandardError => e
40
+ puts "Error listing authors: #{e.message}"
41
+ ensure
42
+ DatabaseConnection.disconnect
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/book'
4
+ require_relative '../models/reading_progress'
5
+ require_relative '../../../config/database_connection'
6
+ require_relative '../utils/helpers'
7
+ module BookReadingTrackerGem
8
+ class BookService
9
+ def self.add_book(title, author, total_pages, description = nil, isbn = nil, published_year = nil)
10
+ DatabaseConnection.connect
11
+ book = Book.create!(
12
+ title: title,
13
+ description: description,
14
+ isbn: isbn,
15
+ published_year: published_year
16
+ )
17
+ book.authors.create!(author_name: author)
18
+ book.create_reading_progress!(total_pages: total_pages)
19
+ puts "Add book: #{book.title} successfully"
20
+ rescue StandardError => e
21
+ puts "Error adding book: #{e.message}"
22
+ ensure
23
+ DatabaseConnection.disconnect
24
+ end
25
+
26
+ def self.remove_book(id)
27
+ DatabaseConnection.connect
28
+ book = Book.find_by(id: id)
29
+ if book
30
+ book.reading_progress&.destroy
31
+ book.book_authors.destroy_all
32
+ book.destroy
33
+ puts "Removed book: #{id}."
34
+ else
35
+ puts "Book not found: #{id}."
36
+ end
37
+ rescue StandardError => e
38
+ puts "Error removing book: #{e.message}"
39
+ ensure
40
+ DatabaseConnection.disconnect
41
+ end
42
+
43
+ def self.update_status(id, status)
44
+ valid_statuses = %w[unread reading read]
45
+ unless valid_statuses.include?(status)
46
+ puts "Invalid status. Accepted: #{valid_statuses.join(', ')}"
47
+ return
48
+ end
49
+
50
+ DatabaseConnection.connect
51
+ book = Book.find_by(id: id)
52
+ if book.nil?
53
+ puts "Book not found: #{id}"
54
+ return
55
+ end
56
+
57
+ if book.reading_progress.nil?
58
+ puts "No reading progress for '#{book.title}'."
59
+ return
60
+ end
61
+
62
+ book.reading_progress.update!(status: status)
63
+ puts "Updated status of '#{book.title}' to '#{status}'."
64
+ rescue StandardError => e
65
+ puts "Error updating status: #{e.message}"
66
+ ensure
67
+ DatabaseConnection.disconnect
68
+ end
69
+
70
+ def self.update_progress(id, pages_read)
71
+ if pages_read.negative?
72
+ puts 'Pages read cannot be negative.'
73
+ return
74
+ end
75
+
76
+ DatabaseConnection.connect
77
+ book = Book.find_by(id: id)
78
+
79
+ if book.nil?
80
+ puts "Book not found: #{id}"
81
+ return
82
+ end
83
+
84
+ reading_progress = book.reading_progress
85
+ if reading_progress.nil?
86
+ puts "No reading progress for '#{book.title}'."
87
+ return
88
+ end
89
+
90
+ total_pages = reading_progress.total_pages
91
+
92
+ if pages_read > total_pages
93
+ puts "Pages read: #{pages_read} cannot exceed total pages: #{total_pages}."
94
+ return
95
+ end
96
+
97
+ reading_progress.update!(pages_read: pages_read)
98
+ puts "Updated reading progress for '#{book.title}' to page #{pages_read}."
99
+
100
+ if pages_read.zero?
101
+ update_status(id, 'unread')
102
+ elsif pages_read < total_pages
103
+ update_status(id, 'reading')
104
+ elsif pages_read == total_pages
105
+ update_status(id, 'read')
106
+ end
107
+ rescue StandardError => e
108
+ puts "Error updating progress: #{e.message}"
109
+ ensure
110
+ DatabaseConnection.disconnect
111
+ end
112
+
113
+ def self.list_books
114
+ DatabaseConnection.connect
115
+ books = Book.includes(:authors).all
116
+
117
+ if books.empty?
118
+ puts 'No books in the system.'
119
+ return
120
+ end
121
+
122
+ header = %w[id title description isbn published_year authors created_at updated_at]
123
+
124
+ rows = books.map do |book|
125
+ # Giới hạn lại thông tin để tránh bảng rộng bị -> dọc
126
+ author_names = (book.authors.map(&:author_name).join(', ').then { |a| a.empty? ? 'N/A' : "#{a[0..20]}..." })
127
+
128
+ # Giới hạn mô tả để tránh bảng quá rộng
129
+ description = "#{(book.description || 'N/A')[0..30]}..."
130
+ title = "#{(book.title || 'N/A')[0..20]}..."
131
+
132
+ [
133
+ book.id,
134
+ title,
135
+ description,
136
+ book.isbn || 'N/A',
137
+ book.published_year || 'N/A',
138
+ author_names,
139
+ book.created_at,
140
+ book.updated_at
141
+ ]
142
+ end
143
+
144
+ # Gọi hàm render_table từ CommonUtils với chế độ ép ngang
145
+ CommonUtils.render_table(header, rows)
146
+ rescue StandardError => e
147
+ puts "Error listing books: #{e.message}"
148
+ ensure
149
+ DatabaseConnection.disconnect
150
+ end
151
+
152
+ def self.stats
153
+ DatabaseConnection.connect
154
+ total_books = Book.count
155
+ read_books = ReadingProgress.where(status: 'read').count
156
+ reading_books = ReadingProgress.where(status: 'reading').count
157
+ unread_books = ReadingProgress.where(status: 'unread').count
158
+
159
+ puts "Total books: #{total_books}"
160
+ puts "Books read: #{read_books}"
161
+ puts "Books reading: #{reading_books}"
162
+ puts "Books unread: #{unread_books}"
163
+ rescue StandardError => e
164
+ puts "Error generating stats: #{e.message}"
165
+ ensure
166
+ DatabaseConnection.disconnect
167
+ end
168
+
169
+ def self.show_progress(id)
170
+ DatabaseConnection.connect
171
+ book = Book.includes(:reading_progress).find_by(id: id)
172
+
173
+ if book.nil?
174
+ puts "Book not found: #{id}"
175
+ return
176
+ end
177
+
178
+ if book.reading_progress.nil?
179
+ puts "No reading progress for '#{book.title}'."
180
+ else
181
+ header = %w[id book_id status pages_read total_pages read created_at updated_at]
182
+ progress = book.reading_progress
183
+ pages_read = progress.pages_read
184
+ total_pages = progress.total_pages
185
+ read = "#{pages_read}/#{total_pages}"
186
+
187
+ rows = [
188
+ [
189
+ book.id,
190
+ book.title,
191
+ progress.status,
192
+ pages_read,
193
+ total_pages,
194
+ read,
195
+ book.reading_progress.created_at,
196
+ book.reading_progress.updated_at
197
+ ]
198
+ ]
199
+
200
+ CommonUtils.render_table(header, rows)
201
+ end
202
+ rescue StandardError => e
203
+ puts "Error showing progress: #{e.message}"
204
+ ensure
205
+ DatabaseConnection.disconnect
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/category'
4
+ require_relative '../../../config/database_connection'
5
+
6
+ module BookReadingTrackerGem
7
+ class CategoryService
8
+ def self.add_category(name)
9
+ DatabaseConnection.connect
10
+ Category.create!(category_name: name)
11
+ puts "Add category '#{name}' successfully."
12
+ rescue StandardError => e
13
+ puts "Error adding category: #{e.message}"
14
+ ensure
15
+ DatabaseConnection.disconnect
16
+ end
17
+
18
+ def self.list_categories
19
+ DatabaseConnection.connect
20
+ categories = Category.all
21
+
22
+ if categories.empty?
23
+ puts 'Categories not found.'
24
+ return
25
+ end
26
+
27
+ header = %w[id category_name created_at updated_at]
28
+ rows = categories.map do |category|
29
+ [
30
+ category.id,
31
+ category.category_name || 'N/A',
32
+ category.created_at,
33
+ category.updated_at
34
+ ]
35
+ end
36
+
37
+ CommonUtils.render_table(header, rows)
38
+ rescue StandardError => e
39
+ puts "Error listing categories: #{e.message}"
40
+ ensure
41
+ DatabaseConnection.disconnect
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-table'
4
+
5
+ module CommonUtils
6
+ def self.render_table(header, rows)
7
+ if rows.empty?
8
+ puts 'No data available.'
9
+ else
10
+ table = TTY::Table.new(header, rows)
11
+ puts table.render(:ascii, resize: false)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BookReadingTrackerGem
4
+ VERSION = '0.1.1'
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'book_reading_tracker_gem/version'
4
+ require_relative 'book_reading_tracker_gem/clis/cli'
5
+ require_relative 'book_reading_tracker_gem/services/book_service'
6
+ require_relative 'book_reading_tracker_gem/services/author_service'
7
+ require_relative 'book_reading_tracker_gem/services/category_service'
8
+ require_relative 'book_reading_tracker_gem/models/book'
9
+ require_relative 'book_reading_tracker_gem/models/author'
10
+ require_relative 'book_reading_tracker_gem/models/category'
11
+ require_relative 'book_reading_tracker_gem/models/book_author'
12
+ require_relative 'book_reading_tracker_gem/models/book_category'
13
+ require_relative 'book_reading_tracker_gem/models/reading_progress'
14
+ require_relative '../config/database_connection'
15
+
16
+ module BookReadingTrackerGem
17
+ class Error < StandardError; end
18
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: book_reading_tracker_gem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Doan Vo Van Trong
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-03-28 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: BookReadingTrackerGem is a simple command-line Ruby gem that helps users
13
+ manage their reading progress, books, authors, and categories. Built with Thor and
14
+ ActiveRecord, and backed by a PostgreSQL (Supabase) database.
15
+ email:
16
+ - trong.doan@tomosia.com
17
+ executables:
18
+ - book_reading_tracker_gem
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - config/database_connection.rb
23
+ - db/book_reading_tracker_gem.rake.rb
24
+ - db/migrate/20250326100001_create_books.rb
25
+ - db/migrate/20250326100002_create_authors.rb
26
+ - db/migrate/20250326100003_create_book_authors.rb
27
+ - db/migrate/20250326100004_create_categories.rb
28
+ - db/migrate/20250326100005_create_book_categories.rb
29
+ - db/migrate/20250326100006_create_reading_progresses.rb
30
+ - db/seeds.rb
31
+ - exe/book_reading_tracker_gem
32
+ - lib/book_reading_tracker_gem.rb
33
+ - lib/book_reading_tracker_gem/clis/cli.rb
34
+ - lib/book_reading_tracker_gem/models/author.rb
35
+ - lib/book_reading_tracker_gem/models/book.rb
36
+ - lib/book_reading_tracker_gem/models/book_author.rb
37
+ - lib/book_reading_tracker_gem/models/book_category.rb
38
+ - lib/book_reading_tracker_gem/models/category.rb
39
+ - lib/book_reading_tracker_gem/models/reading_progress.rb
40
+ - lib/book_reading_tracker_gem/services/author_service.rb
41
+ - lib/book_reading_tracker_gem/services/book_service.rb
42
+ - lib/book_reading_tracker_gem/services/category_service.rb
43
+ - lib/book_reading_tracker_gem/utils/helpers.rb
44
+ - lib/book_reading_tracker_gem/version.rb
45
+ homepage: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ allowed_push_host: https://rubygems.org
50
+ homepage_uri: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem
51
+ source_code_uri: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem.git
52
+ changelog_uri: https://github.com/vantrong24052003/Book-Reading-Tracker-Gem/blob/main/CHANGELOG.md
53
+ rubygems_mfa_required: 'true'
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.1.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.6.6
69
+ specification_version: 4
70
+ summary: A CLI gem to track your book reading progress, authors, and categories.
71
+ test_files: []