chg: Moved the resources folder out of actix since it isn't Rust
This commit is contained in:
BIN
resources/assets/favicon-196.png
Normal file
BIN
resources/assets/favicon-196.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
resources/assets/favicon-32.png
Normal file
BIN
resources/assets/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
resources/assets/favicon.ico
Normal file
BIN
resources/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
37
resources/assets/favicon.svg
Normal file
37
resources/assets/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
82
resources/index.html
Normal file
82
resources/index.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
|
||||
<title>Chhoto URL</title>
|
||||
<meta name="description" content="A simple selfhosted URL shortener with no unnecessary features.">
|
||||
<meta name="keywords" content="url shortener, link shortener, self hosted, open source">
|
||||
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
||||
<link rel="icon" type="image/png" href="assets/favicon-32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="assets/favicon-196.png" sizes="196x196">
|
||||
|
||||
<script src="static/script.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/pure-min.css"
|
||||
integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" target="_blank" href="static/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container" id="container">
|
||||
<form class="pure-form pure-form-aligned" name="new-url-form">
|
||||
<fieldset>
|
||||
<legend id="logo"><img src="assets/favicon-32.png" width="26px" alt="logo"> Chhoto URL</legend>
|
||||
<div class="pure-control-group">
|
||||
<label for="longUrl">Long URL</label>
|
||||
<input type="url" name="longUrl" id="longUrl" placeholder="Please enter a valid URL"
|
||||
onblur="addProtocol(this)" required />
|
||||
</div>
|
||||
<div class=" pure-control-group">
|
||||
<label for="shortUrl">Short URL (optional)</label>
|
||||
<input type="text" name="shortUrl" id="shortUrl" placeholder="Only a-z, 0-9, - and _ are allowed"
|
||||
pattern="[A-Za-z0-9_-]+" />
|
||||
</div>
|
||||
<div class="pure-controls">
|
||||
<button class="pure-button pure-button-primary">Shorten!</button>
|
||||
<p id="alert-box"> </p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p name="loading-text">Loading links table...</p>
|
||||
<table class="pure-table">
|
||||
<caption>Active links</caption>
|
||||
<br>
|
||||
<thead>
|
||||
<tr>
|
||||
<td id="short-url-header">Short URL<br>(click to copy)</td>
|
||||
<td>Long URL</td>
|
||||
<td>Hits</td>
|
||||
<td name="deleteBtn">×</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="url-table">
|
||||
<!-- The links would be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div name="github-link">
|
||||
<a id="version-number" href="https://github.com/SinTan1729/chhoto-url" target="_blank" rel="noopener noreferrer"
|
||||
hidden>Source Code</a>
|
||||
<!-- The version number would be inserted here -->
|
||||
</div>
|
||||
|
||||
<dialog id="login-dialog">
|
||||
<form class="pure-form" name="login-form">
|
||||
<p>Please enter password to access this website</p>
|
||||
<input type="password" id="password" />
|
||||
<button class="pure-button pure-button-primary" value="default">Submit</button>
|
||||
<p id="wrong-pass"> </p>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
26
resources/static/404.html
Normal file
26
resources/static/404.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Error 404</title>
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
</head>
|
||||
|
||||
<style>
|
||||
#quote {
|
||||
text-indent: 4em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<h1>Error 404!</h1>
|
||||
<div style="display: inline-block; text-align:left;">
|
||||
<p>You step in the stream,</p>
|
||||
<p>But the water has moved on.</p>
|
||||
<p>The page is not here.</p>
|
||||
<p id="quote"> — Cass Whittington</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
220
resources/static/script.js
Normal file
220
resources/static/script.js
Normal file
@@ -0,0 +1,220 @@
|
||||
const getSiteUrl = async () => await fetch("/api/siteurl")
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
if (text == "unset") {
|
||||
return window.location.host;
|
||||
}
|
||||
else {
|
||||
return text;
|
||||
}
|
||||
});
|
||||
|
||||
const getVersion = async () => await fetch("/api/version")
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
return text;
|
||||
});
|
||||
|
||||
const refreshData = async () => {
|
||||
let reply = await fetch("/api/all").then(res => res.text());
|
||||
if (reply == "logged_out") {
|
||||
console.log("logged_out");
|
||||
document.getElementById("container").style.filter = "blur(2px)"
|
||||
document.getElementById("login-dialog").showModal();
|
||||
document.getElementById("password").focus();
|
||||
} else {
|
||||
data = reply
|
||||
.split("\n")
|
||||
.filter(line => line !== "")
|
||||
.map(line => line.split(","))
|
||||
.map(arr => ({
|
||||
short: arr[0],
|
||||
long: arr[1],
|
||||
hits: arr[2]
|
||||
}));
|
||||
|
||||
displayData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const displayData = async (data) => {
|
||||
let version = await getVersion();
|
||||
link = document.getElementById("version-number")
|
||||
link.innerText = "v" + version;
|
||||
link.href = "https://github.com/SinTan1729/chhoto-url/releases/tag/" + version;
|
||||
link.hidden = false;
|
||||
|
||||
let site = await getSiteUrl();
|
||||
|
||||
table_box = document.querySelector(".pure-table");
|
||||
loading_text = document.getElementsByName("loading-text")[0];
|
||||
|
||||
if (data.length == 0) {
|
||||
table_box.style.visibility = "hidden";
|
||||
loading_text.style.display = "block";
|
||||
loading_text.innerHTML = "No active links.";
|
||||
}
|
||||
else {
|
||||
loading_text.style.display = "none";
|
||||
const table = document.querySelector("#url-table");
|
||||
if (!window.isSecureContext) {
|
||||
const shortUrlHeader = document.getElementById("short-url-header");
|
||||
shortUrlHeader.innerHTML = "Short URL<br>(right click and copy)";
|
||||
}
|
||||
table_box.style.visibility = "visible";
|
||||
table.innerHTML = ''; // Clear
|
||||
data.forEach(tr => table.appendChild(TR(tr, site)));
|
||||
}
|
||||
};
|
||||
|
||||
const showAlert = async (text, col) => {
|
||||
document.getElementById("alert-box")?.remove();
|
||||
const controls = document.querySelector(".pure-controls");
|
||||
const alertBox = document.createElement("p");
|
||||
alertBox.id = "alert-box";
|
||||
alertBox.style.color = col;
|
||||
alertBox.innerHTML = text;
|
||||
controls.appendChild(alertBox);
|
||||
};
|
||||
|
||||
const TR = (row, site) => {
|
||||
const tr = document.createElement("tr");
|
||||
const longTD = TD(A_LONG(row.long), "Long URL");
|
||||
var shortTD = null;
|
||||
if (window.isSecureContext) {
|
||||
shortTD = TD(A_SHORT(row.short, site), "Short URL");
|
||||
}
|
||||
else {
|
||||
shortTD = TD(A_SHORT_INSECURE(row.short, site), "Short URL");
|
||||
}
|
||||
hitsTD = TD(row.hits);
|
||||
hitsTD.setAttribute("label", "Hits");
|
||||
const btn = deleteButton(row.short);
|
||||
|
||||
tr.appendChild(shortTD);
|
||||
tr.appendChild(longTD);
|
||||
tr.appendChild(hitsTD);
|
||||
tr.appendChild(btn);
|
||||
|
||||
return tr;
|
||||
};
|
||||
|
||||
const copyShortUrl = async (link) => {
|
||||
const site = await getSiteUrl();
|
||||
try {
|
||||
navigator.clipboard.writeText(`${site}/${link}`);
|
||||
showAlert(`Short URL ${link} was copied to clipboard!`, "green");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
showAlert("Could not copy short URL to clipboard, please do it manually.", "red");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const addProtocol = (input) => {
|
||||
var url = input.value.trim();
|
||||
if (url != "" && !~url.indexOf("://") && !~url.indexOf("magnet:")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
input.value = url;
|
||||
return input
|
||||
}
|
||||
|
||||
const A_LONG = (s) => `<a href='${s}'>${s}</a>`;
|
||||
const A_SHORT = (s, t) => `<a href="javascript:copyShortUrl('${s}');">${s}</a>`;
|
||||
const A_SHORT_INSECURE = (s, t) => `<a href="${t}/${s}">${s}</a>`;
|
||||
|
||||
const deleteButton = (shortUrl) => {
|
||||
const td = document.createElement("td");
|
||||
const btn = document.createElement("button");
|
||||
|
||||
btn.innerHTML = "×";
|
||||
|
||||
btn.onclick = e => {
|
||||
e.preventDefault();
|
||||
if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
|
||||
document.getElementById("alert-box")?.remove();
|
||||
showAlert(" ", "black");
|
||||
fetch(`/api/del/${shortUrl}`, {
|
||||
method: "DELETE"
|
||||
}).then(_ => refreshData());
|
||||
}
|
||||
};
|
||||
td.setAttribute("name", "deleteBtn");
|
||||
td.setAttribute("label", "Delete");
|
||||
td.appendChild(btn);
|
||||
return td;
|
||||
};
|
||||
|
||||
const TD = (s, u) => {
|
||||
const td = document.createElement("td");
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = s;
|
||||
td.appendChild(div);
|
||||
td.setAttribute("label", u);
|
||||
return td;
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
const form = document.forms.namedItem("new-url-form");
|
||||
const longUrl = form.elements["longUrl"];
|
||||
const shortUrl = form.elements["shortUrl"];
|
||||
|
||||
const url = `/api/new`;
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: `${longUrl.value};${shortUrl.value}`
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
showAlert("Short URL is not valid or it's already in use!", "red");
|
||||
return "error";
|
||||
}
|
||||
else {
|
||||
return res.text();
|
||||
}
|
||||
}).then(text => {
|
||||
if (text != "error") {
|
||||
copyShortUrl(text);
|
||||
longUrl.value = "";
|
||||
shortUrl.value = "";
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submitLogin = () => {
|
||||
const password = document.getElementById("password");
|
||||
fetch("/api/login", {
|
||||
method: "POST",
|
||||
body: password.value
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
document.getElementById("container").style.filter = "blur(0px)"
|
||||
document.getElementById("login-dialog").remove();
|
||||
refreshData();
|
||||
} else {
|
||||
const wrongPassBox = document.getElementById("wrong-pass");
|
||||
wrongPassBox.innerHTML = "Wrong password!";
|
||||
wrongPassBox.style.color = "red";
|
||||
password.focus();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await refreshData();
|
||||
|
||||
const form = document.forms.namedItem("new-url-form");
|
||||
form.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
submitForm();
|
||||
}
|
||||
|
||||
const login_form = document.forms.namedItem("login-form");
|
||||
login_form.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
submitLogin();
|
||||
}
|
||||
})();
|
||||
100
resources/static/styles.css
Normal file
100
resources/static/styles.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.container {
|
||||
max-width: 950px;
|
||||
margin: 20px auto auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table tr td div {
|
||||
max-height: 75px;
|
||||
line-height: 25px;
|
||||
word-wrap: break-word;
|
||||
max-width: 575px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] button {
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
form input[name="shortUrl"] {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
form input[name="shortUrl"]::placeholder {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
div[name="github-link"] {
|
||||
position: absolute;
|
||||
right: 0.5%;
|
||||
top: 0.5%;
|
||||
}
|
||||
|
||||
.pure-table {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pure-table caption {
|
||||
font-size: 22px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#logo {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#password {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
dialog form {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Settings for mobile devices */
|
||||
@media (pointer:none),
|
||||
(pointer:coarse) {
|
||||
table tr {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table td {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
table td::before {
|
||||
content: attr(label);
|
||||
font-weight: bold;
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table tr td div {
|
||||
width: 63vw
|
||||
}
|
||||
|
||||
.pure-table caption {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user