Initial commit of Todoizer web application
Build and Validate / build-and-test (push) Failing after 1m23s
Build and Validate / build-and-test (push) Failing after 1m23s
This commit is contained in:
@@ -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
@@ -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"]
|
||||||
@@ -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'
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user