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