commit 1f637c6bdefb4a32a43169fd01c29282332bb7d1 Author: Kris Forbes Date: Mon Apr 20 08:56:12 2026 -0400 Initial commit of Todoizer web application diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..9d5f193 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,38 @@ +name: Build and Validate +on: + push: + branches: + - main + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Build Docker image + run: docker build -t todoizer-app:latest . + + - name: Run container + run: | + docker run -d --name todoizer -p 4567:4567 todoizer-app:latest + # Wait for application to start + sleep 5 + + - name: Validate web application loads + run: | + HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}\n" http://localhost:4567/) + if [ "$HTTP_STATUS" -ne 200 ]; then + echo "Failed to load web application. HTTP status: $HTTP_STATUS" + docker logs todoizer + exit 1 + else + echo "Web application loaded successfully. HTTP status: $HTTP_STATUS" + fi + + - name: Cleanup + if: always() + run: | + docker stop todoizer || true + docker rm todoizer || true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b506132 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM ruby:3.2-slim + +RUN apt-get update -qq && \ + apt-get install -y build-essential sqlite3 libsqlite3-dev curl && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY Gemfile ./ +RUN bundle install + +COPY . . + +ENV RACK_ENV=production +ENV SESSION_SECRET=todoizer_secret_key_change_me_in_prod + +RUN chmod +x entrypoint.sh + +EXPOSE 4567 + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..cd6ccd1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +gem 'sinatra' +gem 'sinatra-contrib' +gem 'activerecord', '~> 7.1' +gem 'sqlite3', '~> 1.6' +gem 'bcrypt' +gem 'puma' +gem 'rake' diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..bc543aa --- /dev/null +++ b/app.rb @@ -0,0 +1,131 @@ +require 'sinatra' +require 'sinatra/activerecord' +require 'securerandom' +require_relative 'models' + +set :database, {adapter: "sqlite3", database: "db/todoizer.sqlite3"} +set :sessions, true +set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(64) } +set :server, 'puma' +set :port, 4567 +set :bind, '0.0.0.0' + +# Auto-migrate on startup for simplicity +ActiveRecord::Schema.define do + unless ActiveRecord::Base.connection.table_exists?(:users) + create_table :users do |t| + t.boolean :is_temporary, default: false + t.string :username + t.string :password_digest + t.timestamps + end + end + + unless ActiveRecord::Base.connection.table_exists?(:todos) + create_table :todos do |t| + t.references :user, foreign_key: true + t.string :content + t.boolean :is_completed, default: false + t.timestamps + end + end +end + +helpers do + def current_user + @current_user ||= begin + if session[:user_id] + User.find_by(id: session[:user_id]) + else + nil + end + end + end + + def ensure_user + unless current_user + temp_user = User.create!(is_temporary: true) + session[:user_id] = temp_user.id + @current_user = temp_user + end + end +end + +before do + ensure_user unless request.path_info == '/login' || request.path_info == '/signup' +end + +get '/' do + @todos = current_user.todos.order(created_at: :desc) + erb :index +end + +post '/todos' do + if params[:content] && !params[:content].strip.empty? + current_user.todos.create(content: params[:content].strip) + end + redirect '/' +end + +post '/todos/:id/toggle' do + todo = current_user.todos.find_by(id: params[:id]) + todo.update(is_completed: !todo.is_completed) if todo + redirect '/' +end + +post '/todos/:id/delete' do + todo = current_user.todos.find_by(id: params[:id]) + todo.destroy if todo + redirect '/' +end + +get '/signup' do + redirect '/' unless current_user&.is_temporary + erb :signup +end + +post '/signup' do + redirect '/' unless current_user&.is_temporary + + username = params[:username].to_s.strip + password = params[:password].to_s.strip + + if username.empty? || password.empty? + @error = "Username and password are required." + return erb :signup + end + + if User.exists?(username: username) + @error = "Username is already taken." + return erb :signup + end + + current_user.update!( + is_temporary: false, + username: username, + password: password + ) + + redirect '/' +end + +get '/login' do + redirect '/' if current_user && !current_user.is_temporary + erb :login +end + +post '/login' do + user = User.find_by(username: params[:username], is_temporary: false) + if user && user.authenticate(params[:password]) + session[:user_id] = user.id + redirect '/' + else + @error = "Invalid username or password" + erb :login + end +end + +post '/logout' do + session.clear + redirect '/' +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..76a6edf --- /dev/null +++ b/config.ru @@ -0,0 +1,2 @@ +require './app' +run Sinatra::Application diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..f783c34 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +mkdir -p db + +# Puma will run app.rb which auto-migrates the database +exec bundle exec puma -C config.ru -b tcp://0.0.0.0:4567 diff --git a/models.rb b/models.rb new file mode 100644 index 0000000..b0690c2 --- /dev/null +++ b/models.rb @@ -0,0 +1,15 @@ +require 'active_record' +require 'bcrypt' + +class User < ActiveRecord::Base + has_secure_password validations: false + has_many :todos, dependent: :destroy + + validates :username, presence: true, uniqueness: true, unless: :is_temporary + validates :password, presence: true, unless: :is_temporary +end + +class Todo < ActiveRecord::Base + belongs_to :user + validates :content, presence: true +end diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..286745d --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,102 @@ +body { + font-family: 'Outfit', sans-serif; + background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%); + min-height: 100vh; + color: #f8fafc; +} + +.text-gradient { + background: linear-gradient(to right, #60a5fa, #a78bfa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.glass-card { + background: rgba(30, 41, 59, 0.7); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.1) !important; + border-radius: 24px; +} + +.add-btn { + background: linear-gradient(to right, #3b82f6, #6366f1); + border: none; +} + +.add-btn:hover { + background: linear-gradient(to right, #2563eb, #4f46e5); +} + +.glow-effect { + box-shadow: 0 0 15px rgba(99, 102, 241, 0.5); + transition: box-shadow 0.3s ease; +} + +.glow-effect:hover { + box-shadow: 0 0 25px rgba(99, 102, 241, 0.8); +} + +.todo-item { + transition: all 0.2s ease; +} + +.todo-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.checkbox-custom { + width: 24px; + height: 24px; + border-radius: 50%; + border: 2px solid #64748b; + display: inline-block; + position: relative; + transition: all 0.2s ease; + cursor: pointer; +} + +.checkbox-custom.checked { + background-color: #10b981; + border-color: #10b981; +} + +.checkbox-custom.checked::after { + content: '✓'; + position: absolute; + color: white; + font-size: 14px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.todo-text { + word-break: break-word; +} + +.delete-btn { + opacity: 0; + transition: opacity 0.2s ease; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.todo-item:hover .delete-btn { + opacity: 1; +} + +.temp-warning { + background: linear-gradient(to right, #fde047, #fcd34d); + color: #713f12; +} + +.form-control:focus { + box-shadow: 0 0 0 0.25rem rgba(99, 102, 241, 0.25); + border-color: #6366f1; +} diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 0000000..00a5e95 --- /dev/null +++ b/views/index.erb @@ -0,0 +1,51 @@ +
+
+
+
+

What needs to be done?

+ +
+
+ + +
+
+ +
+ <% if @todos.empty? %> +
+

You're all caught up!

+ Add a task above to get started. +
+ <% else %> + <% @todos.each do |todo| %> +
+
+
+ +
+ <%= Rack::Utils.escape_html(todo.content) %> +
+
+ +
+
+ <% end %> + <% end %> +
+
+
+ + <% if current_user&.is_temporary %> +
+
+
Temporary Session
+

Your tasks will be lost if you clear your cookies. Create a free account to save them!

+
+ Sign Up Now +
+ <% end %> +
+
diff --git a/views/layout.erb b/views/layout.erb new file mode 100644 index 0000000..4c1cd1a --- /dev/null +++ b/views/layout.erb @@ -0,0 +1,41 @@ + + + + + + Todoizer - Get Things Done + + + + + + + + +
+ <% if @error %> + + <% end %> + <%= yield %> +
+ + + + diff --git a/views/login.erb b/views/login.erb new file mode 100644 index 0000000..ba5b589 --- /dev/null +++ b/views/login.erb @@ -0,0 +1,25 @@ +
+
+
+
+

Welcome Back

+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
diff --git a/views/signup.erb b/views/signup.erb new file mode 100644 index 0000000..b49375f --- /dev/null +++ b/views/signup.erb @@ -0,0 +1,26 @@ +
+
+
+
+

Save Your Progress

+

Create an account to securely save your current tasks and access them from anywhere.

+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+