mirror of
https://github.com/lxsang/antd-web-apps
synced 2024-12-25 17:08:22 +01:00
use quicktalk as comment API
This commit is contained in:
parent
e955d8a244
commit
5dd6da0b29
2
Makefile
2
Makefile
@ -1,5 +1,5 @@
|
|||||||
BUILDDIR?=./build
|
BUILDDIR?=./build
|
||||||
PROJS?=grs info blog os doc ci
|
PROJS?=grs info blog os doc ci talk
|
||||||
copyfiles = index.ls mimes.json
|
copyfiles = index.ls mimes.json
|
||||||
main: copy
|
main: copy
|
||||||
for f in $(PROJS); do BUILDDIR=$(BUILDDIR)/"$${f}" make -C "$${f}" ; done
|
for f in $(PROJS); do BUILDDIR=$(BUILDDIR)/"$${f}" make -C "$${f}" ; done
|
||||||
|
@ -21,6 +21,10 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/font-awesome.css" />
|
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/font-awesome.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/afx.css" />
|
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/afx.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/assets/style.css" />
|
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/assets/style.css" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://chat.iohub.dev/assets/quicktalk.css" />
|
||||||
|
<script src="https://chat.iohub.dev/assets/quicktalk.js"> </script>
|
||||||
|
|
||||||
<script src="<?=HTTP_ROOT?>/rst/afx.js"> </script>
|
<script src="<?=HTTP_ROOT?>/rst/afx.js"> </script>
|
||||||
<script src="<?=HTTP_ROOT?>/rst/gscripts/jquery-3.2.1.min.js"> </script>
|
<script src="<?=HTTP_ROOT?>/rst/gscripts/jquery-3.2.1.min.js"> </script>
|
||||||
<script src="<?=HTTP_ROOT?>/assets/main.js"></script>
|
<script src="<?=HTTP_ROOT?>/assets/main.js"></script>
|
||||||
@ -54,6 +58,15 @@
|
|||||||
hljs.highlightBlock(block);
|
hljs.highlightBlock(block);
|
||||||
hljs.lineNumbersBlock(block);
|
hljs.lineNumbersBlock(block);
|
||||||
});
|
});
|
||||||
|
// comment
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
target: "quick_talk_comment_thread",
|
||||||
|
api_uri: "https://chat.iohub.dev/comment",
|
||||||
|
uri: "<?=url?>",
|
||||||
|
page: $("#desktop")[0]
|
||||||
|
};
|
||||||
|
new QuickTalk(options);
|
||||||
});
|
});
|
||||||
<?lua end ?>
|
<?lua end ?>
|
||||||
window.twttr = (function(d, s, id) {
|
window.twttr = (function(d, s, id) {
|
||||||
|
@ -52,44 +52,10 @@
|
|||||||
end
|
end
|
||||||
echo("</ul>")
|
echo("</ul>")
|
||||||
end?>
|
end?>
|
||||||
<h1 class = "commentsec"></h1>
|
<h1 class = "commentsec">Comments</h1>
|
||||||
<div id="disqus_thread"></div>
|
<div>
|
||||||
<script>
|
The comment editor supports <b>Markdown</b> document format. Your email is necessary to notify you of further updates on the discussion. It will be hidden from the public.
|
||||||
|
</div>
|
||||||
/**
|
<div id="quick_talk_comment_thread"></div>
|
||||||
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
|
|
||||||
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/
|
|
||||||
|
|
||||||
var disqus_config = function () {
|
|
||||||
this.page.url = "<?=url?>"; // Replace PAGE_URL with your page's canonical URL variable
|
|
||||||
this.page.identifier = "<?=std.md5(url)?>"; // Replace PAGE_IDENTIFIER with your
|
|
||||||
};
|
|
||||||
|
|
||||||
(function() { // DON'T EDIT BELOW THIS LINE
|
|
||||||
var d = document, s = d.createElement('script');
|
|
||||||
s.src = 'https://https-blog-lxsang-me.disqus.com/embed.js';
|
|
||||||
s.setAttribute('data-timestamp', +new Date());
|
|
||||||
(d.head || d.body).appendChild(s);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
|
|
||||||
|
|
||||||
<!--div class = "commentform">
|
|
||||||
<div class = "inputbox">
|
|
||||||
<div class = "label">Name:</div>
|
|
||||||
<input data-class = "data" type = "text" name = "name" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class = "inputbox">
|
|
||||||
<div class = "label">Email:</div>
|
|
||||||
<input data-class = "data" type = "text" name = "email" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea data-class = "data" name = "content"></textarea>
|
|
||||||
<div class = "inputboxbt">
|
|
||||||
<div data-id="status"></div>
|
|
||||||
<button data-id = "send" >Comment</button>
|
|
||||||
</div>
|
|
||||||
</div-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,52 +1,51 @@
|
|||||||
|
|
||||||
-- create class
|
-- create class
|
||||||
BaseObject:subclass("BaseModel", {registry = {}})
|
BaseObject:subclass("BaseModel", {registry = {}})
|
||||||
|
|
||||||
function BaseModel:initialize()
|
function BaseModel:initialize()
|
||||||
self.db = self.registry.db
|
self.db = self.registry.db
|
||||||
if self.db and self.name and self.name ~= "" and self.fields and not self.db:available(self.name) then
|
if self.db and self.name and self.name ~= "" and self.fields and
|
||||||
|
not self.db:available(self.name) then
|
||||||
self.db:createTable(self.name, self.fields)
|
self.db:createTable(self.name, self.fields)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function BaseModel:create(m)
|
function BaseModel:create(m)
|
||||||
if self.db and m then
|
if self.db and m then return self.db:insert(self.name, m) end
|
||||||
return self.db:insert(self.name,m)
|
|
||||||
end
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function BaseModel:update(m)
|
function BaseModel:update(m)
|
||||||
if self.db and m then
|
if self.db and m then return self.db:update(self.name, m) end
|
||||||
return self.db:update(self.name,m)
|
|
||||||
end
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function BaseModel:delete(cond)
|
function BaseModel:delete(cond)
|
||||||
if self.db and cond then
|
if self.db and cond then return self.db:delete(self.name, cond) end
|
||||||
return self.db:delete(self.name,cond)
|
|
||||||
end
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function BaseModel:find(cond)
|
function BaseModel:find(cond)
|
||||||
if self.db and cond then
|
if self.db and cond then return self.db:find(self.name, cond) end
|
||||||
return self.db:find(self.name, cond)
|
|
||||||
end
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function BaseModel:get(id)
|
function BaseModel:get(id)
|
||||||
local data, order = self:find({exp = {["="] = { id = id}} })
|
local data, order = self:find({exp = {["="] = {id = id}}})
|
||||||
if not data or #order == 0 then return false end
|
if not data or #order == 0 then return false end
|
||||||
return data[1]
|
return data[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
function BaseModel:findAll()
|
function BaseModel:findAll()
|
||||||
if self.db then
|
if self.db then return self.db:getAll(self.name) end
|
||||||
return self.db:getAll(self.name)
|
|
||||||
end
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function BaseModel:query(sql)
|
||||||
|
if self.db then return self.db:query(sql) end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseModel:select(sel, sql_cnd)
|
||||||
|
if self.db then return self.db:select(self.name, sel, sql_cnd) end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
@ -2,148 +2,143 @@ sqlite = modules.sqlite()
|
|||||||
|
|
||||||
if sqlite == nil then return 0 end
|
if sqlite == nil then return 0 end
|
||||||
-- create class
|
-- create class
|
||||||
BaseObject:subclass("DBHelper",{db={}})
|
BaseObject:subclass("DBHelper", {db = {}})
|
||||||
|
|
||||||
function DBHelper:createTable(tbl, m)
|
function DBHelper:createTable(tbl, m)
|
||||||
if self:available(tbl) then return true end
|
if self:available(tbl) then return true end
|
||||||
local sql = "CREATE TABLE "..tbl.."(id INTEGER PRIMARY KEY"
|
local sql = "CREATE TABLE " .. tbl .. "(id INTEGER PRIMARY KEY"
|
||||||
for k, v in pairs(m) do
|
for k, v in pairs(m) do
|
||||||
if k ~= "id" then
|
if k ~= "id" then sql = sql .. "," .. k .. " " .. v end
|
||||||
sql = sql..","..k.." "..v
|
end
|
||||||
end
|
sql = sql .. ");"
|
||||||
end
|
return sqlite.query(self.db, sql) == 1
|
||||||
sql = sql..");"
|
|
||||||
return sqlite.query(self.db,sql) == 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:insert(tbl, m)
|
function DBHelper:insert(tbl, m)
|
||||||
local keys = {}
|
local keys = {}
|
||||||
local values = {}
|
local values = {}
|
||||||
for k,v in pairs(m) do
|
for k, v in pairs(m) do
|
||||||
if k ~= "id" then
|
if k ~= "id" then
|
||||||
table.insert(keys,k)
|
table.insert(keys, k)
|
||||||
if type(v) == "number" then
|
if type(v) == "number" then
|
||||||
table.insert(values, v)
|
table.insert(values, v)
|
||||||
else
|
else
|
||||||
local t = "\""..v:gsub('"', '""').."\""
|
local t = "\"" .. v:gsub('"', '""') .. "\""
|
||||||
table.insert(values,t)
|
table.insert(values, t)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local sql = "INSERT INTO "..tbl.." ("..table.concat(keys,',')..') VALUES ('
|
local sql = "INSERT INTO " .. tbl .. " (" .. table.concat(keys, ',') ..
|
||||||
sql = sql..table.concat(values,',')..');'
|
') VALUES ('
|
||||||
return sqlite.query(self.db, sql) == 1
|
sql = sql .. table.concat(values, ',') .. ');'
|
||||||
|
return sqlite.query(self.db, sql) == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:get(tbl, id)
|
function DBHelper:get(tbl, id)
|
||||||
return sqlite.select(self.db, tbl, "*","id="..id)[1]
|
return sqlite.select(self.db, tbl, "*", "id=" .. id)[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:getAll(tbl)
|
function DBHelper:getAll(tbl)
|
||||||
local data = sqlite.select(self.db, tbl, "*", "1=1")
|
local data = sqlite.select(self.db, tbl, "*", "1=1")
|
||||||
if data == nil then return nil end
|
if data == nil then return nil end
|
||||||
local a = {}
|
local a = {}
|
||||||
for n in pairs(data) do table.insert(a, n) end
|
for n in pairs(data) do table.insert(a, n) end
|
||||||
table.sort(a)
|
table.sort(a)
|
||||||
return data, a
|
return data, a
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:find(tbl, cond)
|
function DBHelper:find(tbl, cond)
|
||||||
local cnd = "1=1"
|
local cnd = "1=1"
|
||||||
local sel = "*"
|
local sel = "*"
|
||||||
if cond.exp then
|
if cond.exp then cnd = self:gencond(cond.exp) end
|
||||||
cnd = self:gencond(cond.exp)
|
if cond.order then
|
||||||
end
|
cnd = cnd .. " ORDER BY "
|
||||||
if cond.order then
|
local l = {}
|
||||||
cnd = cnd.." ORDER BY "
|
local i = 1
|
||||||
local l = {}
|
for k, v in pairs(cond.order) do
|
||||||
local i = 1
|
l[i] = k .. " " .. v
|
||||||
for k,v in pairs(cond.order) do
|
i = i + 1
|
||||||
l[i] = k.." "..v
|
end
|
||||||
i = i+1
|
cnd = cnd .. table.concat(l, ",")
|
||||||
end
|
end
|
||||||
cnd = cnd..table.concat(l, ",")
|
if cond.limit then cnd = cnd .. " LIMIT " .. cond.limit end
|
||||||
end
|
if cond.fields then
|
||||||
if cond.limit then
|
sel = table.concat(cond.fields, ",")
|
||||||
cnd = cnd.." LIMIT "..cond.limit
|
-- print(sel)
|
||||||
end
|
end
|
||||||
if cond.fields then
|
local data = sqlite.select(self.db, tbl, sel, cnd)
|
||||||
sel = table.concat(cond.fields, ",")
|
if data == nil then return nil end
|
||||||
--print(sel)
|
local a = {}
|
||||||
end
|
for n in pairs(data) do table.insert(a, n) end
|
||||||
local data = sqlite.select(self.db, tbl, sel, cnd)
|
table.sort(a)
|
||||||
if data == nil then return nil end
|
return data, a
|
||||||
local a = {}
|
|
||||||
for n in pairs(data) do table.insert(a, n) end
|
|
||||||
table.sort(a)
|
|
||||||
return data, a
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:query(sql)
|
function DBHelper:select(tbl, sel, cnd)
|
||||||
return sqlite.query(self.db, sql) == 1
|
local data = sqlite.select(self.db, tbl, sel, cnd)
|
||||||
|
if data == nil then return nil end
|
||||||
|
local a = {}
|
||||||
|
for n in pairs(data) do table.insert(a, n) end
|
||||||
|
table.sort(a)
|
||||||
|
return data, a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function DBHelper:query(sql) return sqlite.query(self.db, sql) == 1 end
|
||||||
|
|
||||||
function DBHelper:update(tbl, m)
|
function DBHelper:update(tbl, m)
|
||||||
local id = m['id']
|
local id = m['id']
|
||||||
if id ~= nil then
|
if id ~= nil then
|
||||||
local lst = {}
|
local lst = {}
|
||||||
for k,v in pairs(m) do
|
for k, v in pairs(m) do
|
||||||
if(type(v)== "number") then
|
if (type(v) == "number") then
|
||||||
table.insert(lst,k.."="..v)
|
table.insert(lst, k .. "=" .. v)
|
||||||
else
|
else
|
||||||
table.insert(lst,k.."=\""..v:gsub('"', '""').."\"")
|
table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local sql = "UPDATE "..tbl.." SET "..table.concat(lst,",").." WHERE id="..id..";"
|
local sql = "UPDATE " .. tbl .. " SET " .. table.concat(lst, ",") ..
|
||||||
return sqlite.query(self.db, sql) == 1
|
" WHERE id=" .. id .. ";"
|
||||||
end
|
return sqlite.query(self.db, sql) == 1
|
||||||
return false
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:available(tbl)
|
function DBHelper:available(tbl) return sqlite.hasTable(self.db, tbl) == 1 end
|
||||||
return sqlite.hasTable(self.db, tbl) == 1
|
function DBHelper:deleteByID(tbl, id)
|
||||||
end
|
local sql = "DELETE FROM " .. tbl .. " WHERE id=" .. id .. ";"
|
||||||
function DBHelper:deleteByID(tbl,id)
|
return sqlite.query(self.db, sql) == 1
|
||||||
local sql = "DELETE FROM "..tbl.." WHERE id="..id..";"
|
|
||||||
return sqlite.query(self.db, sql) == 1
|
|
||||||
end
|
end
|
||||||
function DBHelper:gencond(o)
|
function DBHelper:gencond(o)
|
||||||
for k,v in pairs(o) do
|
for k, v in pairs(o) do
|
||||||
if k == "and" or k == "or" then
|
if k == "and" or k == "or" then
|
||||||
local cnd = {}
|
local cnd = {}
|
||||||
local i = 1
|
local i = 1
|
||||||
for k1,v1 in pairs(v) do
|
for k1, v1 in pairs(v) do
|
||||||
cnd[i] = self:gencond(v1)
|
cnd[i] = self:gencond(v1)
|
||||||
i = i + 1
|
i = i + 1
|
||||||
end
|
end
|
||||||
return " ("..table.concat(cnd, " "..k.." ")..") "
|
return " (" .. table.concat(cnd, " " .. k .. " ") .. ") "
|
||||||
else
|
else
|
||||||
for k1,v1 in pairs(v) do
|
for k1, v1 in pairs(v) do
|
||||||
local t = type(v1)
|
local t = type(v1)
|
||||||
if(t == "string") then
|
if (t == "string") then
|
||||||
return " ("..k1.." "..k..' "'..v1:gsub('"','""')..'") '
|
return
|
||||||
end
|
" (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') ..
|
||||||
return " ("..k1.." "..k.." "..v1..") "
|
'") '
|
||||||
end
|
end
|
||||||
end
|
return " (" .. k1 .. " " .. k .. " " .. v1 .. ") "
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
function DBHelper:delete(tbl, cond)
|
function DBHelper:delete(tbl, cond)
|
||||||
local sql = "DELETE FROM "..tbl.." WHERE "..self:gencond(cond)..";"
|
local sql = "DELETE FROM " .. tbl .. " WHERE " .. self:gencond(cond) .. ";"
|
||||||
return sqlite.query(self.db, sql) == 1
|
return sqlite.query(self.db, sql) == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
function DBHelper:lastInsertID()
|
function DBHelper:lastInsertID() return sqlite.lastInsertID(self.db) end
|
||||||
return sqlite.lastInsertID(self.db)
|
|
||||||
end
|
|
||||||
|
|
||||||
function DBHelper:close()
|
function DBHelper:close() if self.db then sqlite.dbclose(self.db) end end
|
||||||
if self.db then
|
|
||||||
sqlite.dbclose(self.db)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
function DBHelper:open()
|
function DBHelper:open()
|
||||||
if self.db ~= nil then
|
if self.db ~= nil then self.db = sqlite.getdb(self.db) end
|
||||||
self.db = sqlite.getdb(self.db)
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
6
talk/Makefile
Normal file
6
talk/Makefile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
copyfiles = router.lua models controllers assets
|
||||||
|
|
||||||
|
main:
|
||||||
|
- mkdir -p $(BUILDDIR)
|
||||||
|
cp -rvf $(copyfiles) $(BUILDDIR)
|
||||||
|
- mkdir -p $(BUILDDIR)/log
|
122
talk/assets/quicktalk.css
Normal file
122
talk/assets/quicktalk.css
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
.quick-talk-compose {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-compose input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
font-style: italic;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #878887;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-compose textarea {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-compose .quick-talk-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.quick-talk-compose .quick-talk-compose-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-compose .quick-talk-compose-footer div {
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
color: orangered;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-compose .quick-talk-compose-footer button,
|
||||||
|
.quick-talk-compose-button {
|
||||||
|
color: white;
|
||||||
|
background-color: steelblue;
|
||||||
|
padding: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-compose-button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-comment-thread .quick-talk-sub-comment {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-header {
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-footer {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-footer span {
|
||||||
|
display: block;
|
||||||
|
color: steelblue;
|
||||||
|
cursor: pointer;
|
||||||
|
/* text-align: right; */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-footer span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-user {
|
||||||
|
font-weight: bold;
|
||||||
|
color: steelblue;
|
||||||
|
padding-left: 3px;
|
||||||
|
padding-right: 3px;
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #e1ecf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-time {
|
||||||
|
font-style: italic;
|
||||||
|
color: steelblue;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding-top: 1px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.quick-talk-comment-thread .quick-talk-comment-content {
|
||||||
|
display: block;
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
248
talk/assets/quicktalk.js
Normal file
248
talk/assets/quicktalk.js
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
class QuickTalk {
|
||||||
|
constructor(opt) {
|
||||||
|
this.options = opt;
|
||||||
|
if (typeof this.options.target === "string") {
|
||||||
|
this.options.target = document.getElementById(this.options.target);
|
||||||
|
}
|
||||||
|
this.preview_on = false;
|
||||||
|
this.instant_compose = undefined;
|
||||||
|
let editor = document.createElement("div");
|
||||||
|
let compose_button = document.createElement("button");
|
||||||
|
compose_button.setAttribute("class", "quick-talk-compose-button");
|
||||||
|
compose_button.textContent = "Write a comment";
|
||||||
|
this.options.target.appendChild(compose_button);
|
||||||
|
this.options.target.appendChild(editor);
|
||||||
|
let container = this.load_thread(this.options.target);
|
||||||
|
compose_button.addEventListener("click", (event) => {
|
||||||
|
if (this.instant_compose) {
|
||||||
|
this.instant_compose.parentNode.removeChild(this.instant_compose);
|
||||||
|
}
|
||||||
|
this.instant_compose = this.compose(editor, 0, true, (data) => {
|
||||||
|
this.show_comment(container, data, true).scrollIntoView();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
request(uri, data, callback) {
|
||||||
|
let xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.open("POST", uri, true);
|
||||||
|
xhttp.setRequestHeader("Content-type", "application/json");
|
||||||
|
xhttp.onreadystatechange = () => {
|
||||||
|
if (xhttp.readyState == 4) {
|
||||||
|
if (xhttp.status == 200) {
|
||||||
|
if (callback) {
|
||||||
|
callback(JSON.parse(xhttp.responseText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.error(xhttp.statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (data) {
|
||||||
|
xhttp.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
xhttp.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_status(type, msg) {
|
||||||
|
if (this.status_el) {
|
||||||
|
this.status_el.innerHTML = type + ": " + msg;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(type + ": " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clear_status() {
|
||||||
|
this.status_el.innerHTML = "";
|
||||||
|
}
|
||||||
|
info(msg) {
|
||||||
|
this.set_status("INFO", msg);
|
||||||
|
}
|
||||||
|
error(obj) {
|
||||||
|
console.log(obj);
|
||||||
|
this.set_status("ERROR", obj.toString());
|
||||||
|
}
|
||||||
|
compose(at, cid, auto_hide, callback) {
|
||||||
|
let preview = document.createElement("div");
|
||||||
|
preview.setAttribute("class", "quick-talk-preview");
|
||||||
|
preview.style.display = "none";
|
||||||
|
let container = document.createElement("div");
|
||||||
|
container.setAttribute("class", "quick-talk-compose");
|
||||||
|
let name = document.createElement("input");
|
||||||
|
name.value = "Name";
|
||||||
|
name.addEventListener("focus", (event) => {
|
||||||
|
if (name.value == "Name") {
|
||||||
|
name.value = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
name.addEventListener("blur", (event) => {
|
||||||
|
if (name.value.trim() == "") {
|
||||||
|
name.value = "Name";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let email = document.createElement("input");
|
||||||
|
email.value = "Email";
|
||||||
|
email.addEventListener("focus", (event) => {
|
||||||
|
if (email.value == "Email") {
|
||||||
|
email.value = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
email.addEventListener("blur", (event) => {
|
||||||
|
if (email.value.trim() == "") {
|
||||||
|
email.value = "Email";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let ta = document.createElement("textarea");
|
||||||
|
let footer = document.createElement("div");
|
||||||
|
footer.setAttribute("class", "quick-talk-compose-footer");
|
||||||
|
this.status_el = document.createElement("div");
|
||||||
|
let bt_preview = document.createElement("button");
|
||||||
|
bt_preview.textContent = "Preview";
|
||||||
|
bt_preview.addEventListener("click", (event) => {
|
||||||
|
let md = { data: ta.value };
|
||||||
|
if (!this.preview_on && ta.value.trim() != "") {
|
||||||
|
this.request(this.options.api_uri + "/preview", md, (ret) => {
|
||||||
|
if (ret.result) {
|
||||||
|
ta.style.display = "none";
|
||||||
|
preview.innerHTML = ret.result;
|
||||||
|
preview.style.display = "block";
|
||||||
|
bt_preview.textContent = "Edit";
|
||||||
|
this.preview_on = !this.preview_on;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.error(ret.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ta.style.display = "block";
|
||||||
|
preview.style.display = "none";
|
||||||
|
bt_preview.textContent = "Preview";
|
||||||
|
this.preview_on = !this.preview_on;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let bt_submit = document.createElement("button");
|
||||||
|
bt_submit.textContent = "Send";
|
||||||
|
bt_submit.addEventListener("click", (event) => {
|
||||||
|
this.clear_status();
|
||||||
|
if (name.value.trim() == "" ||
|
||||||
|
name.value.trim() == "Name" ||
|
||||||
|
email.value.trim() == "" ||
|
||||||
|
email.value.trim() == "Email" ||
|
||||||
|
ta.value.trim() == "") {
|
||||||
|
this.info("Please enter all the fields");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check for email
|
||||||
|
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
if (!re.test(String(email.value).toLowerCase()))
|
||||||
|
return this.info("Email is not correct");
|
||||||
|
// send the post request
|
||||||
|
let data = {
|
||||||
|
page: {
|
||||||
|
uri: this.options.uri,
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
name: name.value,
|
||||||
|
email: email.value,
|
||||||
|
rid: cid,
|
||||||
|
content: ta.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.request(this.options.api_uri + "/post", data, (ret) => {
|
||||||
|
if (ret.result) {
|
||||||
|
// TODO: more check goes here
|
||||||
|
name.value = "Name";
|
||||||
|
email.value = "Email";
|
||||||
|
ta.value = "";
|
||||||
|
if (auto_hide) {
|
||||||
|
at.removeChild(container);
|
||||||
|
this.instant_compose = undefined;
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(ret.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.error(ret.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
footer.appendChild(this.status_el);
|
||||||
|
footer.appendChild(bt_preview);
|
||||||
|
footer.appendChild(bt_submit);
|
||||||
|
container.appendChild(name);
|
||||||
|
container.appendChild(email);
|
||||||
|
container.appendChild(ta);
|
||||||
|
container.appendChild(preview);
|
||||||
|
container.appendChild(footer);
|
||||||
|
at.appendChild(container);
|
||||||
|
container.scrollIntoView();
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
load_thread(at) {
|
||||||
|
let container = document.createElement("div");
|
||||||
|
container.setAttribute("class", "quick-talk-comment-thread");
|
||||||
|
at.appendChild(container);
|
||||||
|
this.request(this.options.api_uri, { page: this.options.uri }, (ret) => {
|
||||||
|
if (ret.result) {
|
||||||
|
ret.result.forEach((comment) => {
|
||||||
|
this.show_comment(container, comment, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.error(ret.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
show_comment(at, comment, show_footer) {
|
||||||
|
let container = document.createElement("div");
|
||||||
|
container.setAttribute("class", "quick-talk-comment");
|
||||||
|
let header = document.createElement("div");
|
||||||
|
header.setAttribute("class", "quick-talk-comment-header");
|
||||||
|
let username = document.createElement("span");
|
||||||
|
username.setAttribute("class", "quick-talk-comment-user");
|
||||||
|
let time = document.createElement("span");
|
||||||
|
time.setAttribute("class", "quick-talk-comment-time");
|
||||||
|
let content = document.createElement("div");
|
||||||
|
content.setAttribute("class", "quick-talk-comment-content");
|
||||||
|
username.innerHTML = comment.name;
|
||||||
|
let date = new Date(parseInt(comment.time) * 1000);
|
||||||
|
time.innerHTML = `on ${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}, wrote:`;
|
||||||
|
content.innerHTML = comment.content;
|
||||||
|
header.appendChild(username);
|
||||||
|
header.appendChild(time);
|
||||||
|
container.appendChild(header);
|
||||||
|
container.appendChild(content);
|
||||||
|
let sub_comments = document.createElement("div");
|
||||||
|
sub_comments.setAttribute("class", "quick-talk-sub-comment");
|
||||||
|
if (comment.children && comment.children.length > 0) {
|
||||||
|
comment.children.forEach((cmt) => {
|
||||||
|
this.show_comment(sub_comments, cmt, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
container.appendChild(sub_comments);
|
||||||
|
if (show_footer) {
|
||||||
|
let footer = document.createElement("div");
|
||||||
|
footer.setAttribute("class", "quick-talk-comment-footer");
|
||||||
|
let span = document.createElement("span");
|
||||||
|
span.innerText = "Reply";
|
||||||
|
footer.appendChild(span);
|
||||||
|
let editor = document.createElement("div");
|
||||||
|
footer.appendChild(editor);
|
||||||
|
span.addEventListener("click", (event) => {
|
||||||
|
if (this.instant_compose) {
|
||||||
|
this.instant_compose.parentNode.removeChild(this.instant_compose);
|
||||||
|
}
|
||||||
|
this.instant_compose = this.compose(editor, parseInt(comment.id), true, (data) => {
|
||||||
|
this.show_comment(sub_comments, data, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
container.appendChild(footer);
|
||||||
|
}
|
||||||
|
at.appendChild(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
146
talk/controllers/CommentController.lua
Normal file
146
talk/controllers/CommentController.lua
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
BaseController:subclass("CommentController",
|
||||||
|
{registry = {}, models = {"comment", "pages"}})
|
||||||
|
|
||||||
|
local function process_md(input)
|
||||||
|
local md = require("md")
|
||||||
|
local content = ""
|
||||||
|
local callback = function(s) content = content .. s end
|
||||||
|
md.to_html(input, callback)
|
||||||
|
return content
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sendmail(to, subject, content)
|
||||||
|
local from = "From: contact@iohub.dev\n"
|
||||||
|
local suject = "Subject: " .. subject .. "\n"
|
||||||
|
|
||||||
|
local cmd = 'echo "' .. utils.escape(from .. suject .. content) ..
|
||||||
|
'"| sendmail ' .. to
|
||||||
|
local r = os.execute(cmd)
|
||||||
|
|
||||||
|
if r then return true end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
function CommentController:index(...)
|
||||||
|
if (REQUEST.method == "OPTIONS") then
|
||||||
|
result("")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local rq = (JSON.decodeString(REQUEST.json))
|
||||||
|
if (rq) then
|
||||||
|
local pages, order = self.pages:find({exp = {["="] = {uri = rq.page}}})
|
||||||
|
if not pages or #order == 0 then
|
||||||
|
fail("Be the first to comment")
|
||||||
|
else
|
||||||
|
local pid = pages[1].id
|
||||||
|
local comments, order = self.comment:find(
|
||||||
|
{
|
||||||
|
exp = {
|
||||||
|
["and"] = {{["="] = {pid = pid}}, {[" = "] = {rid = 0}}}
|
||||||
|
},
|
||||||
|
order = {time = "ASC"},
|
||||||
|
fields = {"id", "time", "name", "rid", "pid", "content"}
|
||||||
|
})
|
||||||
|
if not comments or #order == 0 then
|
||||||
|
fail("Be the first to comment")
|
||||||
|
else
|
||||||
|
for idx, v in pairs(order) do
|
||||||
|
local data = comments[v]
|
||||||
|
data.content = process_md(data.content)
|
||||||
|
data.children = {}
|
||||||
|
-- find all the replies to this thread
|
||||||
|
local sub_comments, suborder =
|
||||||
|
self.comment:find(
|
||||||
|
{
|
||||||
|
exp = {
|
||||||
|
["and"] = {
|
||||||
|
{["="] = {pid = pid}},
|
||||||
|
{[" = "] = {rid = data.id}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
order = {time = "ASC"}
|
||||||
|
|
||||||
|
})
|
||||||
|
if sub_comments and #suborder ~= 0 then
|
||||||
|
for i, subc in pairs(suborder) do
|
||||||
|
sub_comments[subc].content =
|
||||||
|
process_md(sub_comments[subc].content)
|
||||||
|
end
|
||||||
|
data.children = sub_comments
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result(comments)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
fail("Invalid request")
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function CommentController:post(...)
|
||||||
|
if (REQUEST.method == "OPTIONS") then
|
||||||
|
result("")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local rq = (JSON.decodeString(REQUEST.json))
|
||||||
|
if rq then
|
||||||
|
local pages, order = self.pages:find({exp = {["="] = rq.page}})
|
||||||
|
if not pages or #order == 0 then
|
||||||
|
-- insert data
|
||||||
|
if self.pages:create(rq.page) then
|
||||||
|
rq.comment.pid = self.pages.db:lastInsertID()
|
||||||
|
else
|
||||||
|
fail("Unable to initialize comment thread for page: " ..
|
||||||
|
rq.page.uri)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rq.comment.pid = pages[1].id
|
||||||
|
end
|
||||||
|
-- now insert the comment
|
||||||
|
rq.comment.time = os.time(os.date("!*t"))
|
||||||
|
if (self.comment:create(rq.comment)) then
|
||||||
|
rq.comment.id = self.comment.db:lastInsertID()
|
||||||
|
|
||||||
|
rq.comment.content = process_md(rq.comment.content)
|
||||||
|
|
||||||
|
-- send mail to all users of current page
|
||||||
|
local cmts, cmti = self.comment:select("MIN(id) as id,email",
|
||||||
|
"pid=" .. rq.comment.pid ..
|
||||||
|
" AND email != '" ..
|
||||||
|
rq.comment.email ..
|
||||||
|
"' GROUP BY email")
|
||||||
|
if cmts and #cmti > 0 then
|
||||||
|
for idx, v in pairs(cmti) do
|
||||||
|
sendmail(cmts[v].email, rq.comment.name ..
|
||||||
|
" has written something on a page that you've commented on",
|
||||||
|
rq.comment.name ..
|
||||||
|
" has written something on a page that you've commented. \nPlease visit this page: " ..
|
||||||
|
rq.page.uri ..
|
||||||
|
" for updates on the discussion.\nBest regard,\nEmail automatically sent by QuickTalk API")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rq.comment.email = ""
|
||||||
|
result(rq.comment)
|
||||||
|
else
|
||||||
|
fail("Unable to save comment")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
fail("Invalid request")
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function CommentController:preview(...)
|
||||||
|
if (REQUEST.method == "OPTIONS") then
|
||||||
|
result("")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local rq = (JSON.decodeString(REQUEST.json))
|
||||||
|
if (rq and rq.data) then
|
||||||
|
result(process_md(rq.data))
|
||||||
|
else
|
||||||
|
fail("Invalid request")
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
6
talk/controllers/IndexController.lua
Normal file
6
talk/controllers/IndexController.lua
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
BaseController:subclass("IndexController", {registry = {}})
|
||||||
|
|
||||||
|
function IndexController:index(...)
|
||||||
|
result("Quicktalk API")
|
||||||
|
return false
|
||||||
|
end
|
12
talk/models/CommentModel.lua
Normal file
12
talk/models/CommentModel.lua
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
BaseModel:subclass("CommentModel", {
|
||||||
|
registry = {},
|
||||||
|
name = "comments",
|
||||||
|
fields = {
|
||||||
|
pid = "INTEGER",
|
||||||
|
name = "TEXT",
|
||||||
|
email = "TEXT",
|
||||||
|
content = "TEXT",
|
||||||
|
time = "NUMERIC",
|
||||||
|
rid = "INTEGER DEFAULT 0"
|
||||||
|
}
|
||||||
|
})
|
2
talk/models/PagesModel.lua
Normal file
2
talk/models/PagesModel.lua
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
BaseModel:subclass("PagesModel",
|
||||||
|
{registry = {}, name = "pages", fields = {uri = "TEXT"}})
|
56
talk/router.lua
Normal file
56
talk/router.lua
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
-- the rewrite rule for the framework
|
||||||
|
-- should be something like this
|
||||||
|
-- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&<query>
|
||||||
|
-- some global variables
|
||||||
|
function fail(msg)
|
||||||
|
std.json()
|
||||||
|
std.t(JSON.encode({error = msg}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function result(obj)
|
||||||
|
std.json()
|
||||||
|
std.t(JSON.encode({result = obj, error = false}))
|
||||||
|
end
|
||||||
|
DIR_SEP = "/"
|
||||||
|
WWW_ROOT = __ROOT__ .. "/talk"
|
||||||
|
if HEADER.Host then
|
||||||
|
HTTP_ROOT = "https://" .. HEADER.Host
|
||||||
|
else
|
||||||
|
HTTP_ROOT = "https://talk.iohub.dev"
|
||||||
|
end
|
||||||
|
-- class path: path.to.class
|
||||||
|
BASE_FRW = ""
|
||||||
|
-- class path: path.to.class
|
||||||
|
CONTROLLER_ROOT = BASE_FRW .. "talk.controllers"
|
||||||
|
MODEL_ROOT = BASE_FRW .. "talk.models"
|
||||||
|
-- file path: path/to/file
|
||||||
|
VIEW_ROOT = WWW_ROOT .. DIR_SEP .. "views"
|
||||||
|
LOG_ROOT = WWW_ROOT .. DIR_SEP .. "logs"
|
||||||
|
|
||||||
|
-- require needed library
|
||||||
|
require(BASE_FRW .. "silk.api")
|
||||||
|
|
||||||
|
-- registry object store global variables
|
||||||
|
local REGISTRY = {}
|
||||||
|
-- set logging level
|
||||||
|
REGISTRY.logger = Logger:new{
|
||||||
|
levels = {INFO = false, ERROR = false, DEBUG = false}
|
||||||
|
}
|
||||||
|
|
||||||
|
REGISTRY.layout = 'default'
|
||||||
|
REGISTRY.fileaccess = true
|
||||||
|
REGISTRY.db = DBHelper:new{db = "quicktalk"}
|
||||||
|
REGISTRY.db:open()
|
||||||
|
|
||||||
|
local router = Router:new{registry = REGISTRY}
|
||||||
|
REGISTRY.router = router
|
||||||
|
router:setPath(CONTROLLER_ROOT)
|
||||||
|
-- router:route('edit', 'post/edit', "ALL" )
|
||||||
|
|
||||||
|
std.header("Access-Control-Allow-Origin", "*")
|
||||||
|
std.header("Access-Control-Allow-Methods", "POST")
|
||||||
|
std.header("Access-Control-Allow-Headers", "content-type")
|
||||||
|
|
||||||
|
-- router:route('default', nil)
|
||||||
|
router:delegate()
|
||||||
|
if REGISTRY.db then REGISTRY.db:close() end
|
Loading…
Reference in New Issue
Block a user