jimmu 1.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +279 -0
- data/examples/bbs/app.rb +21 -0
- data/examples/bbs/public/css/style.css +15 -0
- data/examples/bbs/views/404.erb +1 -0
- data/examples/bbs/views/500.erb +1 -0
- data/examples/bbs/views/index.erb +29 -0
- data/examples/bbs/views/layout.erb +15 -0
- data/examples/blog/app.rb +28 -0
- data/examples/blog/public/css/style.css +18 -0
- data/examples/blog/views/404.erb +1 -0
- data/examples/blog/views/500.erb +1 -0
- data/examples/blog/views/index.erb +9 -0
- data/examples/blog/views/layout.erb +15 -0
- data/examples/blog/views/new.erb +18 -0
- data/examples/blog/views/post/[id].erb +17 -0
- data/examples/chatroom/app.rb +44 -0
- data/examples/chatroom/public/css/style.css +15 -0
- data/examples/chatroom/views/404.erb +1 -0
- data/examples/chatroom/views/500.erb +1 -0
- data/examples/chatroom/views/index.erb +18 -0
- data/examples/chatroom/views/layout.erb +12 -0
- data/examples/chatroom/views/room/[name].erb +47 -0
- data/examples/counter/app.rb +15 -0
- data/examples/counter/public/css/style.css +8 -0
- data/examples/counter/public/js/app.js +11 -0
- data/examples/counter/views/404.erb +1 -0
- data/examples/counter/views/500.erb +1 -0
- data/examples/counter/views/api/increment.erb +5 -0
- data/examples/counter/views/api/reset.erb +4 -0
- data/examples/counter/views/index.erb +8 -0
- data/examples/counter/views/layout.erb +13 -0
- data/examples/hello-world/app.rb +6 -0
- data/examples/hello-world/public/css/style.css +3 -0
- data/examples/hello-world/views/404.erb +2 -0
- data/examples/hello-world/views/500.erb +1 -0
- data/examples/hello-world/views/greet/[name].erb +2 -0
- data/examples/hello-world/views/index.erb +3 -0
- data/examples/hello-world/views/layout.erb +12 -0
- data/examples/image-upload/app.rb +22 -0
- data/examples/image-upload/public/css/style.css +11 -0
- data/examples/image-upload/views/404.erb +1 -0
- data/examples/image-upload/views/500.erb +1 -0
- data/examples/image-upload/views/index.erb +31 -0
- data/examples/image-upload/views/layout.erb +15 -0
- data/examples/json-api/app.rb +27 -0
- data/examples/json-api/views/404.erb +2 -0
- data/examples/json-api/views/500.erb +2 -0
- data/examples/json-api/views/api/notes/[id].erb +33 -0
- data/examples/json-api/views/api/notes.erb +20 -0
- data/examples/json-api/views/index.erb +13 -0
- data/examples/login-demo/app.rb +31 -0
- data/examples/login-demo/public/css/style.css +12 -0
- data/examples/login-demo/views/404.erb +1 -0
- data/examples/login-demo/views/500.erb +1 -0
- data/examples/login-demo/views/index.erb +6 -0
- data/examples/login-demo/views/layout.erb +24 -0
- data/examples/login-demo/views/login.erb +22 -0
- data/examples/login-demo/views/logout.erb +4 -0
- data/examples/login-demo/views/mypage/index.erb +3 -0
- data/examples/login-demo/views/signup.erb +24 -0
- data/examples/poll/app.rb +55 -0
- data/examples/poll/public/css/style.css +10 -0
- data/examples/poll/public/js/app.js +24 -0
- data/examples/poll/views/404.erb +1 -0
- data/examples/poll/views/500.erb +1 -0
- data/examples/poll/views/index.erb +6 -0
- data/examples/poll/views/layout.erb +13 -0
- data/examples/todo/app.rb +21 -0
- data/examples/todo/public/css/style.css +17 -0
- data/examples/todo/views/404.erb +1 -0
- data/examples/todo/views/500.erb +1 -0
- data/examples/todo/views/delete/[id].erb +4 -0
- data/examples/todo/views/index.erb +27 -0
- data/examples/todo/views/layout.erb +15 -0
- data/examples/todo/views/toggle/[id].erb +5 -0
- data/examples/url-shortener/app.rb +39 -0
- data/examples/url-shortener/public/css/style.css +13 -0
- data/examples/url-shortener/views/404.erb +1 -0
- data/examples/url-shortener/views/500.erb +1 -0
- data/examples/url-shortener/views/[code].erb +10 -0
- data/examples/url-shortener/views/api/shorten.erb +13 -0
- data/examples/url-shortener/views/index.erb +35 -0
- data/examples/url-shortener/views/layout.erb +15 -0
- data/examples/visitor-counter/app.rb +22 -0
- data/examples/visitor-counter/public/css/style.css +4 -0
- data/examples/visitor-counter/views/404.erb +1 -0
- data/examples/visitor-counter/views/500.erb +1 -0
- data/examples/visitor-counter/views/index.erb +11 -0
- data/examples/visitor-counter/views/layout.erb +12 -0
- data/exe/jimmu +5 -0
- data/lib/jimmu.rb +1916 -0
- metadata +143 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Todo app.
|
|
2
|
+
# Demonstrates: SQLite CRUD, forms, dynamic routes for actions,
|
|
3
|
+
# redirect.
|
|
4
|
+
|
|
5
|
+
port 3000
|
|
6
|
+
|
|
7
|
+
db = sqlite "db/todo.db"
|
|
8
|
+
db.exec <<~SQL
|
|
9
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
text TEXT NOT NULL,
|
|
12
|
+
done INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
created_at TEXT NOT NULL
|
|
14
|
+
)
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
before do
|
|
18
|
+
log "#{request.method} #{request.path}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
run
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
:root{color-scheme:light dark}
|
|
2
|
+
*{box-sizing:border-box}
|
|
3
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#fafafa;color:#1a1a1a;margin:0}
|
|
4
|
+
.wrap{max-width:480px;margin:0 auto;padding:48px 20px}
|
|
5
|
+
h1{font-size:22px}
|
|
6
|
+
.add-form{display:flex;gap:8px;margin-bottom:20px}
|
|
7
|
+
.add-form input{flex:1;padding:12px;border-radius:8px;border:1px solid #ddd;font-size:15px}
|
|
8
|
+
.add-form button{padding:12px 18px;border-radius:8px;border:0;background:#222;color:#fff;font-weight:600;cursor:pointer}
|
|
9
|
+
.todos{list-style:none;padding:0;display:flex;flex-direction:column;gap:6px}
|
|
10
|
+
.todos li{display:flex;align-items:center;gap:10px;background:#fff;padding:10px 12px;border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
|
11
|
+
.todos li.done .text{text-decoration:line-through;color:#999}
|
|
12
|
+
.inline{display:inline;margin:0}
|
|
13
|
+
.check{width:26px;height:26px;border-radius:50%;border:1px solid #ccc;background:#fff;cursor:pointer;color:#2a2}
|
|
14
|
+
.todos li.done .check{background:#2a2;color:#fff;border-color:#2a2}
|
|
15
|
+
.text{flex:1}
|
|
16
|
+
.del{border:0;background:none;color:#c33;font-size:18px;cursor:pointer;padding:0 6px}
|
|
17
|
+
.empty{color:#999;text-align:center;padding:20px 0}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>404</h1><p><a href="/">Home</a></p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>500</h1>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%
|
|
2
|
+
if request.method == "POST"
|
|
3
|
+
text = params[:text].to_s.strip
|
|
4
|
+
db.exec("INSERT INTO todos(text, done, created_at) VALUES (?, 0, ?)", text, Time.now.to_s) unless text.empty?
|
|
5
|
+
redirect "/"
|
|
6
|
+
end
|
|
7
|
+
todos = db.query("SELECT * FROM todos ORDER BY done ASC, id DESC")
|
|
8
|
+
%>
|
|
9
|
+
<form method="post" action="/" class="add-form">
|
|
10
|
+
<input type="text" name="text" placeholder="What needs doing?" required autofocus>
|
|
11
|
+
<button type="submit">Add</button>
|
|
12
|
+
</form>
|
|
13
|
+
|
|
14
|
+
<ul class="todos">
|
|
15
|
+
<% todos.each do |todo| %>
|
|
16
|
+
<li class="<%= todo[:done] == 1 ? 'done' : '' %>">
|
|
17
|
+
<form method="post" action="/toggle/<%= todo[:id] %>" class="inline">
|
|
18
|
+
<button type="submit" class="check" aria-label="toggle"><%= todo[:done] == 1 ? "✓" : "" %></button>
|
|
19
|
+
</form>
|
|
20
|
+
<span class="text"><%= CGI.escapeHTML(todo[:text]) %></span>
|
|
21
|
+
<form method="post" action="/delete/<%= todo[:id] %>" class="inline">
|
|
22
|
+
<button type="submit" class="del" aria-label="delete">×</button>
|
|
23
|
+
</form>
|
|
24
|
+
</li>
|
|
25
|
+
<% end %>
|
|
26
|
+
</ul>
|
|
27
|
+
<% if todos.empty? %><p class="empty">Nothing to do. Add a task above.</p><% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Jimmu Todo</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="wrap">
|
|
11
|
+
<h1>Todo</h1>
|
|
12
|
+
<%= yield %>
|
|
13
|
+
</div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# URL shortener.
|
|
2
|
+
# Demonstrates: SQLite, a dynamic catch-all-ish route ("/[code].erb")
|
|
3
|
+
# that redirects, and a small JSON API.
|
|
4
|
+
#
|
|
5
|
+
# Note on helpers: a `def` written directly at the top level of
|
|
6
|
+
# app.rb only becomes a method on the application-configuration
|
|
7
|
+
# object itself, so it would NOT be callable from inside views (they
|
|
8
|
+
# run against a separate per-request object). Wrapping shared helper
|
|
9
|
+
# logic in a plain Ruby module with `module_function`, as below, makes
|
|
10
|
+
# it callable as Helpers.random_code from anywhere -- app.rb, views,
|
|
11
|
+
# before/after hooks, or websocket blocks.
|
|
12
|
+
|
|
13
|
+
port 3000
|
|
14
|
+
|
|
15
|
+
db = sqlite "db/links.db"
|
|
16
|
+
db.exec <<~SQL
|
|
17
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
18
|
+
code TEXT PRIMARY KEY,
|
|
19
|
+
url TEXT NOT NULL,
|
|
20
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
created_at TEXT NOT NULL
|
|
22
|
+
)
|
|
23
|
+
SQL
|
|
24
|
+
|
|
25
|
+
module Helpers
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
CODE_CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
|
29
|
+
|
|
30
|
+
def random_code
|
|
31
|
+
Array.new(6) { CODE_CHARS.sample }.join
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
log "#{request.method} #{request.path}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
run
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
:root{color-scheme:light dark}
|
|
2
|
+
*{box-sizing:border-box}
|
|
3
|
+
body{font-family:-apple-system,sans-serif;background:#f6f7fb;color:#1a1a1a;margin:0}
|
|
4
|
+
.wrap{max-width:640px;margin:0 auto;padding:48px 20px}
|
|
5
|
+
h1{font-size:22px}
|
|
6
|
+
.shorten-form{display:flex;gap:8px;margin:20px 0}
|
|
7
|
+
.shorten-form input{flex:1;padding:12px;border-radius:8px;border:1px solid #ddd;font-size:15px}
|
|
8
|
+
.shorten-form button{padding:12px 20px;border-radius:8px;border:0;background:#3b6fe0;color:#fff;font-weight:600;cursor:pointer}
|
|
9
|
+
.result{background:#e7f3e8;border:1px solid #bfe3c2;padding:12px 16px;border-radius:8px;margin-bottom:20px}
|
|
10
|
+
table{width:100%;border-collapse:collapse;font-size:14px}
|
|
11
|
+
th,td{text-align:left;padding:8px 6px;border-bottom:1px solid #e5e5e5}
|
|
12
|
+
.url{max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
13
|
+
a{color:#3b6fe0}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>404</h1><p>Unknown short link.</p><p><a href="/">← home</a></p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>500</h1>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%
|
|
2
|
+
data = params
|
|
3
|
+
target = data[:url].to_s.strip
|
|
4
|
+
if target !~ /\Ahttps?:\/\//
|
|
5
|
+
status 422
|
|
6
|
+
json(error: "url must start with http:// or https://")
|
|
7
|
+
else
|
|
8
|
+
code = Helpers.random_code
|
|
9
|
+
code = Helpers.random_code while db.query("SELECT 1 AS x FROM links WHERE code = ?", code).any?
|
|
10
|
+
db.exec("INSERT INTO links(code, url, clicks, created_at) VALUES (?, ?, 0, ?)", code, target, Time.now.to_s)
|
|
11
|
+
json(code: code, short_url: "#{request.header('host')}/#{code}", url: target)
|
|
12
|
+
end
|
|
13
|
+
%>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<%
|
|
2
|
+
short_url = nil
|
|
3
|
+
if request.method == "POST"
|
|
4
|
+
target = params[:url].to_s.strip
|
|
5
|
+
if target =~ /\Ahttps?:\/\//
|
|
6
|
+
code = Helpers.random_code
|
|
7
|
+
code = Helpers.random_code while db.query("SELECT 1 AS x FROM links WHERE code = ?", code).any?
|
|
8
|
+
db.exec("INSERT INTO links(code, url, clicks, created_at) VALUES (?, ?, 0, ?)", code, target, Time.now.to_s)
|
|
9
|
+
short_url = "#{request.header('host')}/#{code}"
|
|
10
|
+
else
|
|
11
|
+
status 422
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
recent = db.query("SELECT * FROM links ORDER BY rowid DESC LIMIT 10")
|
|
15
|
+
%>
|
|
16
|
+
<form method="post" action="/" class="shorten-form">
|
|
17
|
+
<input type="url" name="url" placeholder="https://example.com/a/very/long/url" required value="<%= CGI.escapeHTML(params[:url].to_s) %>">
|
|
18
|
+
<button type="submit">Shorten</button>
|
|
19
|
+
</form>
|
|
20
|
+
|
|
21
|
+
<% if short_url %>
|
|
22
|
+
<div class="result">Your short link: <a href="http://<%= short_url %>"><%= short_url %></a></div>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<h2>Recent links</h2>
|
|
26
|
+
<table>
|
|
27
|
+
<tr><th>Code</th><th>URL</th><th>Clicks</th></tr>
|
|
28
|
+
<% recent.each do |link| %>
|
|
29
|
+
<tr>
|
|
30
|
+
<td><a href="/<%= link[:code] %>">/<%= link[:code] %></a></td>
|
|
31
|
+
<td class="url"><%= CGI.escapeHTML(link[:url]) %></td>
|
|
32
|
+
<td><%= link[:clicks] %></td>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
</table>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Jimmu URL Shortener</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="wrap">
|
|
11
|
+
<h1>URL Shortener</h1>
|
|
12
|
+
<%= yield %>
|
|
13
|
+
</div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Page-view counter.
|
|
2
|
+
# Demonstrates: the `before` hook running on every request, SQLite
|
|
3
|
+
# for durable counters, and a cookie used to recognize a repeat
|
|
4
|
+
# visitor (as opposed to a brand new one) without requiring login.
|
|
5
|
+
|
|
6
|
+
port 3000
|
|
7
|
+
|
|
8
|
+
db = sqlite "db/visits.db"
|
|
9
|
+
db.exec "CREATE TABLE IF NOT EXISTS stats (name TEXT PRIMARY KEY, value INTEGER NOT NULL)"
|
|
10
|
+
db.exec "INSERT OR IGNORE INTO stats(name, value) VALUES ('total_views', 0)"
|
|
11
|
+
db.exec "INSERT OR IGNORE INTO stats(name, value) VALUES ('unique_visitors', 0)"
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
next unless request.path == "/"
|
|
15
|
+
db.exec("UPDATE stats SET value = value + 1 WHERE name = 'total_views'")
|
|
16
|
+
if cookie[:visitor_id].nil?
|
|
17
|
+
cookie[:visitor_id] = SecureRandom.hex(8)
|
|
18
|
+
db.exec("UPDATE stats SET value = value + 1 WHERE name = 'unique_visitors'")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
run
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
:root{color-scheme:light dark}
|
|
2
|
+
body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fafafa;color:#1a1a1a;text-align:center}
|
|
3
|
+
.big{font-size:80px;font-weight:800;color:#3b6fe0;font-variant-numeric:tabular-nums}
|
|
4
|
+
.you{color:#888;font-size:14px;margin-top:24px}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="wrap"><h1>404</h1></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="wrap"><h1>500</h1></div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%
|
|
2
|
+
total = db.query("SELECT value FROM stats WHERE name = 'total_views'").first[:value]
|
|
3
|
+
unique = db.query("SELECT value FROM stats WHERE name = 'unique_visitors'").first[:value]
|
|
4
|
+
is_returning = !cookie[:visitor_id].nil?
|
|
5
|
+
%>
|
|
6
|
+
<div class="wrap">
|
|
7
|
+
<h1>This page has been viewed</h1>
|
|
8
|
+
<div class="big"><%= total %></div>
|
|
9
|
+
<p>time<%= total == 1 ? "" : "s" %>, by <strong><%= unique %></strong> unique visitor<%= unique == 1 ? "" : "s" %>.</p>
|
|
10
|
+
<p class="you"><%= is_returning ? "Welcome back -- you've been counted before." : "This is your first visit -- welcome!" %></p>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Jimmu Visitor Counter</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<%= yield %>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|