Skip to content

Commit

Permalink
Add link list page
Browse files Browse the repository at this point in the history
  • Loading branch information
rikkix committed Nov 15, 2024
1 parent 75b5241 commit 4eba56e
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 14 deletions.
162 changes: 162 additions & 0 deletions public/links.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<!doctype html>
<html>

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link href="https://unpkg.com/[email protected]/dist/css/tabulator.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/6.3.0/css/tabulator_semanticui.min.css"
integrity="sha512-V5osaNojM1vXQvKtRIjCawiV39/632N1HTlfUnlBu35bY7CTbUbeubWOiQcWPuWxGrj+DDXh/Kl1qAILGA/8Og=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>

<body class="vh-100">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a class="navbar-brand" href="/">PasteBin</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/">Upload</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/shorten">Shorten</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/files">Files</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/links">Links</a>
</li>
</div>
</div>
</div>
</nav>

<div class="container mt-3">
<div id="list-table"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/js/tabulator.min.js"></script>
<script>
function dtFormatter(cell, formatterParams, onRendered) {
if (!cell.getValue()) {
return "N/A";
}
const dt = new Date(cell.getValue());
// return formatted date "YYYY-MM-DD HH:MM:SS"
const year = dt.getFullYear();
const month = String(dt.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(dt.getDate()).padStart(2, '0');
const hours = String(dt.getHours()).padStart(2, '0');
const minutes = String(dt.getMinutes()).padStart(2, '0');
const seconds = String(dt.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function sizeFormatter(cell, formatterParams, onRendered) {
const size = cell.getValue();
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + " KB";
} else if (size < 1024 * 1024 * 1024) {
return (size / 1024 / 1024).toFixed(2) + " MB";
} else {
return (size / 1024 / 1024 / 1024).toFixed(2) + " GB";
}
}
function printDeleteIcon(cell, formatterParams, onRendered) {
return "<i class='fa fa-trash text-danger'></i>";
}
var table = new Tabulator("#list-table", {
ajaxURL: window.location.origin + "/api/links/list?before=" + new Date().getTime().toString(), //ajax URL
ajaxURLGenerator: function (url, config, params) {
//url - the url from the ajaxURL property or setData function
//config - the request config object from the ajaxConfig property
//params - the params object from the ajaxParams property, this will also include any pagination, filter and sorting properties based on table setup
const res = new URL(url);
console.log(params);
if (params.page) {
res.searchParams.append("page", params.page);
}
if (params.size) {
res.searchParams.append("num", params.size);
}
if (params.sort && params.sort.length > 0) {
const sort = params.sort[0];
res.searchParams.append("order_by", sort.field);
res.searchParams.append("order", sort.dir);
}
if (params.filter && params.filter.length > 0) {
const filters = params.filter;
for (const filter of filters) {
res.searchParams.append(filter.field, filter.value);
}
}
return res.toString();
},
filterMode: "remote", //send filter data to the server instead of processing locally
ajaxResponse: function (url, params, response) {
response.last_page = response.total_pages;
return response;
},
layout: "fitColumns",
sortMode: "remote", //send sort data to the server instead of processing locally

pagination: true, //enable pagination
paginationMode: "remote", //enable remote pagination
paginationSize: 50, //optional parameter to request a certain number of rows per page=
columns: [
{ title: "Key", field: "key", headerSort: false, width: 120, headerFilter: true,
cellClick: function (e, cell) {
// copy access link to clipboard
const key = cell.getRow().getData().key;
const url = window.location.origin + "/l/" + key;

}
},
{ title: "Destination", field: "destination", headerSort: false, headerFilter: true },
{
title: "Created At", field: "created_at", width: 170, formatter: dtFormatter
},
{ title: "Expire At", field: "expire_at", width: 170, formatter: dtFormatter },
{ title: "Access", width: 100, field: "access_count" },
{
formatter: printDeleteIcon,
headerSort: false, width: 20, frozen: true,
cellClick: function (e, cell) {
const key = cell.getRow().getData().key;
const url = window.location.origin + "/api/links/delete/" + key;
// send alert
if (confirm("Are you sure you want to delete this link?")) {
fetch(url, { method: "DELETE" })
.then(response => {
if (response.ok) {
alert("Link deleted successfully!");
table.setData();
} else {
alert("Failed to delete link!");
}
});
}
}
}
]
});
</script>
</body>

</html>
54 changes: 40 additions & 14 deletions src/handlers/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ export async function handleCreateLink(c: HonoContext): Promise<Response> {
await c.env.DB.prepare('INSERT INTO links (key, destination, created_at, expire_at) VALUES (?, ?, ?, ?)')
.bind(key, destination, created_at, expire_at)
.run()
return c.json({
return c.json({
key: key,
access_url: accessURL
}, { status: 201 })
}, { status: 201 })
} catch (e) {
return c.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

export async function deleteLink(db: D1Database, key: string): Promise<void> {
await db.prepare('DELETE FROM links WHERE key = ?')
.bind(key)
.run()
.bind(key)
.run()

// await db.prepare('DELETE FROM link_access WHERE key = ?')
// .bind(key)
// .run()
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function handleAccessLink(c: HonoContext): Promise<Response> {
await c.env.DB.prepare('INSERT INTO link_access (key, ip, country, city, ua, referer, access_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
.bind(key, ip, country, city, ua, referer, access_at)
.run()

await c.env.DB.prepare('UPDATE links SET access_count = access_count + 1 WHERE key = ?')
.bind(key)
.run()
Expand Down Expand Up @@ -169,37 +169,63 @@ export async function handleDeleteLink(c: HonoContext): Promise<Response> {
* - `order` (optional): A string to specify the sort order. Can be 'ASC' or 'DESC'. Defaults to 'DESC'.
*/
export async function handleListLinks(c: HonoContext): Promise<Response> {
const dest = c.req.query('dest') || null
const before_raw = parseInt(c.req.query('before') || '')
const before = (!isNaN(before_raw) && before_raw > 0) ? before_raw : new Date().getTime()

const page_raw = parseInt(c.req.query('page') || '')
const page = (!isNaN(page_raw) && page_raw > 0) ? page_raw : 1

const num_raw = parseInt(c.req.query('num') || '')
const num = (!isNaN(num_raw) && num_raw > 0 && num_raw < 500) ? num_raw : 50

const key = c.req.query('key') || null

const dest = c.req.query('destination') || null

const order_by_raw = c.req.query('order_by') || 'created_at'
const order_by = ['created_at', 'access_count'].includes(order_by_raw) ? order_by_raw : 'created_at'
const order_by = ['created_at', 'expire_at', 'access_count'].includes(order_by_raw) ? order_by_raw : 'created_at'

const order_raw = c.req.query('order')?.toUpperCase() || 'DESC'
const order = ['ASC', 'DESC'].includes(order_raw) ? order_raw : 'DESC'


let query = 'SELECT * FROM links'
let condition_query = ''
let params = []
condition_query += ' WHERE created_at < ?'
params.push(before)
if (key) {
condition_query += ' AND key LIKE ?'
params.push(`%${key}%`)
}
if (dest) {
query += ' WHERE destination LIKE ?'
condition_query += ' AND destination LIKE ?'
params.push(`%${dest}%`)
}
query += ` ORDER BY ${order_by} ${order}`
query += ` LIMIT ? OFFSET ?`

const query_count = `SELECT COUNT(*) as count FROM links ${condition_query}`
const count = await c.env.DB.prepare(query_count)
.bind(...params)
.first()
if (!count) {
return c.json({ error: 'Internal Server Error' }, { status: 500 })
}
const total = count.count as number

const query = `SELECT * FROM links ${condition_query} ORDER BY ${order_by} ${order} LIMIT ? OFFSET ?`
params.push(num)
params.push((page - 1) * num)

const links = await c.env.DB.prepare(query)
.bind(...params)
.all()

const total_pages = Math.ceil(total / num)
const remaining = total > (page * num) ? total - (page * num) : 0
const remaining_pages = Math.ceil(remaining / num)

return c.json({
total: total,
total_pages: total_pages,
remaining: remaining,
remaining_pages: remaining_pages,
success: links.success,
data: links.results
}, { status: 200 })
Expand Down

0 comments on commit 4eba56e

Please sign in to comment.