Initial commit of Todoizer web application
Build and Validate / build-and-test (push) Failing after 1m23s

This commit is contained in:
2026-04-20 08:56:12 -04:00
commit 1f637c6bde
12 changed files with 468 additions and 0 deletions
+38
View File
@@ -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
+21
View File
@@ -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"]
+9
View File
@@ -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'
+131
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
require './app'
run Sinatra::Application
+7
View File
@@ -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
+15
View File
@@ -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
+102
View File
@@ -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;
}
+51
View File
@@ -0,0 +1,51 @@
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card glass-card shadow-lg border-0 mb-4">
<div class="card-body p-4">
<h2 class="mb-4 fw-bold text-center">What needs to be done?</h2>
<form action="/todos" method="post" class="mb-4">
<div class="input-group input-group-lg shadow-sm rounded-pill overflow-hidden">
<input type="text" name="content" class="form-control border-0 bg-dark-subtle px-4" placeholder="Add a new task..." required autofocus>
<button class="btn btn-primary px-4 fw-bold add-btn" type="submit">Add</button>
</div>
</form>
<div class="todo-list">
<% if @todos.empty? %>
<div class="text-center text-muted py-5">
<p class="fs-5 mb-0">You're all caught up!</p>
<small>Add a task above to get started.</small>
</div>
<% else %>
<% @todos.each do |todo| %>
<div class="todo-item d-flex justify-content-between align-items-center p-3 mb-2 rounded-3 <%= todo.is_completed ? 'completed bg-dark-subtle opacity-75' : 'bg-dark text-light border border-secondary border-opacity-25' %>">
<div class="d-flex align-items-center flex-grow-1">
<form action="/todos/<%= todo.id %>/toggle" method="post" class="me-3 m-0">
<button type="submit" class="toggle-btn p-0 border-0 bg-transparent">
<div class="checkbox-custom <%= todo.is_completed ? 'checked' : '' %>"></div>
</button>
</form>
<span class="todo-text fs-5 <%= 'text-decoration-line-through text-muted' if todo.is_completed %>"><%= Rack::Utils.escape_html(todo.content) %></span>
</div>
<form action="/todos/<%= todo.id %>/delete" method="post" class="m-0 ms-2">
<button type="submit" class="btn btn-sm btn-outline-danger delete-btn rounded-circle" title="Delete">✕</button>
</form>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<% if current_user&.is_temporary %>
<div class="alert alert-warning shadow-sm border-0 rounded-4 d-flex align-items-center justify-content-between p-4 temp-warning">
<div>
<h5 class="alert-heading fw-bold mb-1">Temporary Session</h5>
<p class="mb-0 text-dark opacity-75">Your tasks will be lost if you clear your cookies. Create a free account to save them!</p>
</div>
<a href="/signup" class="btn btn-dark fw-bold px-4 rounded-pill">Sign Up Now</a>
</div>
<% end %>
</div>
</div>
+41
View File
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todoizer - Get Things Done</title>
<meta name="description" content="A simple and premium todo application.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent mb-4">
<div class="container">
<a class="navbar-brand fw-bold fs-3 text-gradient" href="/">✓ Todoizer</a>
<div class="d-flex align-items-center">
<% if current_user %>
<% if current_user.is_temporary %>
<a href="/signup" class="btn btn-outline-warning btn-sm me-2 fw-semibold glow-effect">Sign up to save permanently</a>
<a href="/login" class="btn btn-link text-light text-decoration-none btn-sm">Log In</a>
<% else %>
<span class="text-light me-3">Hello, <strong><%= current_user.username %></strong></span>
<form action="/logout" method="post" class="d-inline">
<button type="submit" class="btn btn-outline-light btn-sm">Log Out</button>
</form>
<% end %>
<% end %>
</div>
</div>
</nav>
<div class="container main-container">
<% if @error %>
<div class="alert alert-danger" role="alert"><%= @error %></div>
<% end %>
<%= yield %>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card glass-card shadow-lg border-0 mt-5">
<div class="card-body p-5">
<h2 class="fw-bold mb-4 text-center">Welcome Back</h2>
<form action="/login" method="post">
<div class="mb-3">
<label for="username" class="form-label text-light">Username</label>
<input type="text" class="form-control form-control-lg bg-dark text-light border-secondary" id="username" name="username" required autofocus>
</div>
<div class="mb-4">
<label for="password" class="form-label text-light">Password</label>
<input type="password" class="form-control form-control-lg bg-dark text-light border-secondary" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 rounded-pill fw-bold glow-effect">Log In</button>
</form>
<div class="mt-4 text-center">
<a href="/" class="text-decoration-none text-muted">← Back to home</a>
</div>
</div>
</div>
</div>
</div>
+26
View File
@@ -0,0 +1,26 @@
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card glass-card shadow-lg border-0 mt-5">
<div class="card-body p-5">
<h2 class="fw-bold mb-4 text-center">Save Your Progress</h2>
<p class="text-muted text-center mb-4">Create an account to securely save your current tasks and access them from anywhere.</p>
<form action="/signup" method="post">
<div class="mb-3">
<label for="username" class="form-label text-light">Username</label>
<input type="text" class="form-control form-control-lg bg-dark text-light border-secondary" id="username" name="username" required autofocus>
</div>
<div class="mb-4">
<label for="password" class="form-label text-light">Password</label>
<input type="password" class="form-control form-control-lg bg-dark text-light border-secondary" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 rounded-pill fw-bold glow-effect">Create Account</button>
</form>
<div class="mt-4 text-center">
<a href="/" class="text-decoration-none text-muted">← Back to my tasks</a>
</div>
</div>
</div>
</div>
</div>