summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api-routes.js32
-rw-r--r--index.js2
-rw-r--r--routes.js40
-rw-r--r--static/css/standard.css26
-rw-r--r--static/index.html23
-rw-r--r--static/js/ajax.js47
-rw-r--r--static/js/slug.js10
-rw-r--r--utils.js7
8 files changed, 145 insertions, 42 deletions
diff --git a/api-routes.js b/api-routes.js
deleted file mode 100644
index 5c0ed7c..0000000
--- a/api-routes.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const express = require('express');
-const Url = require('./db/url-schema.js');
-const utils = require('./utils.js');
-const router = express.Router();
-
-router.all('/', async (req, res) => {
- utils.sendBadRequestError(res, 'Nothing to be done at API root.');
-});
-
-router.put('/:slug', async (req, res) => {
- try {
- const {url} = req.body;
- const slug = req.params.slug.toLowerCase();
- if (!utils.isUrlFormatted(url))
- return utils.sendBadRequestError(res, 'URL "'+url+'" is malformatted. Must be a valid URL.');
- if (!utils.isAlphaNumeric(slug))
- return utils.sendBadRequestError(res, 'Slug "'+slug+'" is malformatted. Must be alphanumeric.');
- if (await Url.exists({slug: slug}))
- return utils.sendBadRequestError(res, 'Slug "'+slug+'" already exists. Updates are not supported at this time.');
-
- await new Url({url, slug}).save();
- utils.sendSuccessfulSlugCreation(res, slug);
- } catch (err) {
- utils.sendInternalServerError(res);
- }
-});
-
-router.all('/*', async (req, res) => {
- utils.sendNotFoundError(res);
-});
-
-module.exports = router;
diff --git a/index.js b/index.js
index e091e1a..fe578ec 100644
--- a/index.js
+++ b/index.js
@@ -2,7 +2,6 @@ const express = require('express');
const mongoose = require('mongoose');
const config = require('./config.js');
const routes = require('./routes.js');
-const apiRoutes = require('./api-routes.js');
async function serve() {
try {
@@ -21,7 +20,6 @@ async function serve() {
app.use(express.urlencoded({extended: true}));
app.use(express.json());
- app.use('/api', apiRoutes);
app.use('/css', express.static(__dirname+'/static/css'));
app.use('/js', express.static(__dirname+'/static/js'));
app.use('/', routes);
diff --git a/routes.js b/routes.js
index 012befe..207a022 100644
--- a/routes.js
+++ b/routes.js
@@ -4,10 +4,36 @@ const Url = require('./db/url-schema.js');
const utils = require('./utils.js');
const router = express.Router();
+async function createRoute(res, url, slug) {
+ try {
+ if (!utils.isUrlFormatted(url))
+ return utils.sendBadRequestError(res, 'URL "'+url+'" is malformatted. Must be a valid URL.');
+ if (!utils.isAlphaNumeric(slug))
+ return utils.sendBadRequestError(res, 'Slug "'+slug+'" is malformatted. Must be alphanumeric.');
+
+ slug = slug.toLowerCase();
+ if (await Url.exists({slug: slug}))
+ return utils.sendBadRequestError(res, 'Slug "'+slug+'" already exists. Updates are not supported at this time.');
+
+ await new Url({url, slug}).save();
+ utils.sendSuccessfulSlugCreation(res, slug);
+ } catch (err) {
+ utils.sendInternalServerError(res, err);
+ }
+}
+
router.get('/', async (req, res) => {
utils.sendFile(res, 'static/index.html');
});
+router.post('/', async (req, res) => {
+ let slug;
+ do {
+ slug = utils.generateSlug();
+ } while (await Url.exists({slug: slug}));
+ await createRoute(res, req.body.url, slug);
+});
+
router.get('/:slug', async (req, res) => {
try {
const url = await Url.findOne({slug: req.params.slug.toLowerCase()});
@@ -16,12 +42,22 @@ router.get('/:slug', async (req, res) => {
else
utils.sendNotFoundError(res);
} catch (err) {
- utils.sendInternalServerError(res);
+ utils.sendInternalServerError(res, err);
}
});
-router.all('/*', async (req, res) => {
+router.put('/:slug', async (req, res) => {
+ if (!req.params.slug)
+ return utils.sendBadRequestError(res, 'Cannot PUT to root path.');
+ await createRoute(res, req.body.url, req.params.slug.toLowerCase());
+});
+
+router.get('/*', async (req, res) => {
utils.sendNotFoundError(res);
});
+router.all('/*', async (req, res) => {
+ utils.sendBadRequestError(res, 'Nothing exists at this endpoint.');
+});
+
module.exports = router;
diff --git a/static/css/standard.css b/static/css/standard.css
index c5fa6b9..d7acfdc 100644
--- a/static/css/standard.css
+++ b/static/css/standard.css
@@ -70,6 +70,9 @@ body {
font-family: var(--cflems-ff-text-primary);
font-size: 14pt;
}
+#content #rhs p {
+ margin-bottom: 0.5rem;
+}
.form-element {
margin: 1rem 0;
}
@@ -101,6 +104,15 @@ body {
#shortenButton:active {
background-color: #5f80be;
}
+#shortenButton.error {
+ background-color: #b2022f;
+}
+#shortenButton.error:hover {
+ background-color: #a0012a;
+}
+#shortenButton.error:active {
+ background-color: #900025;
+}
.form-element p .fullsize {
flex-grow: 1;
}
@@ -128,9 +140,9 @@ body {
@media (prefers-color-scheme: dark) {
body {
- color: #fdfbf9;
+ color: #e0e0e0;
background-image:
- linear-gradient(to top, #000000f0 20%, #00000090 50%, #00000050),
+ linear-gradient(to top, #000000f0 20%, #00000090 50%, #00000070),
url('https://cdn.cflems.net/fle.ms/earth.webp');
}
#content a {
@@ -160,8 +172,16 @@ body {
#shortenButton:active {
background-color: #68369b;
}
+ #shortenButton.error {
+ background-color: #74001d;
+ }
+ #shortenButton.error:hover {
+ background-color: #68001a;
+ }
+ #shortenButton.error:active {
+ background-color: #5d0017;
+ }
.form-element input[type=text], .form-element button {
border-color: none;
- /*box-shadow: 0rem 0rem 0.5rem #fdfbf9b0;*/
}
}
diff --git a/static/index.html b/static/index.html
index 25d0fd9..ccd7f42 100644
--- a/static/index.html
+++ b/static/index.html
@@ -9,6 +9,8 @@
<link rel="stylesheet" type="text/css" href="css/standard.css" />
<link rel="stylesheet" type="text/css" href="css/mobile.css" />
<title>fle.ms</title>
+ <script type="text/javascript" src="js/slug.js" async></script>
+ <script type="text/javascript" src="js/ajax.js" defer></script>
</head>
<body>
<div id="header">
@@ -32,7 +34,26 @@
</div>
</div>
<div id="rhs">
- <p>A URL shortener is a thing that does blah blah blah.</p>
+ <h3>What is this?</h3>
+ <p>fle.ms is an REST API driven URL shortener&mdash;a service which stores
+ long and complicated URLs such as Google Drive documents, GitHub commit
+ references, Spotify playlists, and social media permalinks. Our service
+ replaces these long links that are hard to remember and share,
+ with short alphanumeric slugs and keywords that you can say or type out
+ in one go.
+ </p>
+ <h3>Why this service over another?</h3>
+ <p>Popular URL shorteners, like most other real estate on the internet,
+ have become saturated over their long tenure. Relevant keywords, names,
+ and even short random slugs are all taken, leading to longer and less
+ meaningful links. This is a fresh start.
+ </p>
+ <p>fle.ms places an emphasis on readability and communicability, so our
+ service ignores capitalization entirely. An &apos;a&apos; is an
+ &apos;A&apos; and a &apos;z&apos; is a &apos;Z&apos;. No need to sweat
+ over what kind. Our technology is optimized for usability without
+ sacrificing speed.
+ </p>
</div>
</div>
</body>
diff --git a/static/js/ajax.js b/static/js/ajax.js
new file mode 100644
index 0000000..e31a009
--- /dev/null
+++ b/static/js/ajax.js
@@ -0,0 +1,47 @@
+(function () {
+ const btn = document.querySelector('#shortenButton');
+ const inpUrl = document.querySelector('#url');
+ const inpSlug = document.querySelector('#slug');
+
+ function shorten (e) {
+ if (!inpUrl.value) return;
+
+ const req = new XMLHttpRequest();
+ req.addEventListener('load', () => {
+ const reply = JSON.parse(req.responseText);
+ if (req.status != 201) {
+ btn.innerText = reply.uiString || 'API Error';
+ btn.classList.add('error');
+ console.error(req.status, reply.errorMessage);
+ return;
+ }
+
+ inpUrl.value = reply.data.shortUrl;
+ inpSlug.value = '';
+ btn.removeEventListener('click', shorten);
+ btn.classList.remove('error');
+ btn.innerText = 'Copy';
+ btn.addEventListener('click', copyUrl);
+ inpUrl.focus();
+ inpUrl.select();
+ });
+ req.addEventListener('error', () => {
+ btn.innerText = 'Connection Error';
+ btn.classList.add('error');
+ })
+
+ if (inpSlug.value)
+ req.open('PUT', '/'+encodeURIComponent(inpSlug.value));
+ else
+ req.open('POST', '/');
+ req.setRequestHeader('Content-Type', 'application/json');
+ req.send(JSON.stringify({url: encodeURI(inpUrl.value)}));
+ }
+
+ async function copyUrl(e) {
+ await navigator.clipboard.writeText(encodeURI(inpUrl.value));
+ btn.innerText = 'Copied!';
+ }
+
+ btn.addEventListener('click', shorten);
+})(); \ No newline at end of file
diff --git a/static/js/slug.js b/static/js/slug.js
new file mode 100644
index 0000000..f82635c
--- /dev/null
+++ b/static/js/slug.js
@@ -0,0 +1,10 @@
+function generateSlug (len = 5) {
+ const ALPHA_SIZE = 36;
+ const MIN = 36**(len-1);
+ const MAX = 36**len;
+ const slugNumeric = Math.floor(Math.random() * (MAX-MIN))+MIN;
+ return slugNumeric.toString(ALPHA_SIZE);
+}
+
+if (typeof module !== 'undefined')
+ module.exports = {generateSlug};
diff --git a/utils.js b/utils.js
index eefc84d..695063f 100644
--- a/utils.js
+++ b/utils.js
@@ -1,7 +1,9 @@
const path = require('path');
const config = require('./config.js');
+const slugUtils = require('./static/js/slug.js');
const helperFunctions = {
+ generateSlug : slugUtils.generateSlug,
sendFile : function (res, fileName) {
res.sendFile(fileName, {
root: path.join(__dirname)
@@ -23,9 +25,10 @@ const helperFunctions = {
});
},
sendBadRequestError : function (res, errorMessage) {
- res.status(400).json({status: "ERROR", statusCode: 400, errorType: "BAD REQUEST", errorMessage});
+ res.status(400).json({status: "ERROR", statusCode: 400, errorType: "BAD REQUEST", errorMessage, uiString: "Already Exists"});
},
- sendInternalServerError : function (res) {
+ sendInternalServerError : function (res, error) {
+ console.error('[express] Error:', error);
res.status(404).json({status: "ERROR", statusCode: 500, errorType: "INTERNAL", errorMessage: "Internal Server Error"});
},
sendSuccessfulSlugCreation : function (res, slug) {