From 5636edd5b6af3486284b430f660b1b9e309a3671 Mon Sep 17 00:00:00 2001 From: Carson Fleming Date: Fri, 20 Oct 2023 23:37:28 -0400 Subject: Bring it all together and nitpick the appearance --- api-routes.js | 32 -------------------------------- index.js | 2 -- routes.js | 40 ++++++++++++++++++++++++++++++++++++++-- static/css/standard.css | 26 +++++++++++++++++++++++--- static/index.html | 23 ++++++++++++++++++++++- static/js/ajax.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ static/js/slug.js | 10 ++++++++++ utils.js | 7 +++++-- 8 files changed, 145 insertions(+), 42 deletions(-) delete mode 100644 api-routes.js create mode 100644 static/js/ajax.js create mode 100644 static/js/slug.js 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 @@ fle.ms + +
-

A URL shortener is a thing that does blah blah blah.

+

What is this?

+

fle.ms is an REST API driven URL shortener—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. +

+

Why this service over another?

+

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. +

+

fle.ms places an emphasis on readability and communicability, so our + service ignores capitalization entirely. An 'a' is an + 'A' and a 'z' is a 'Z'. No need to sweat + over what kind. Our technology is optimized for usability without + sacrificing speed. +

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) { -- cgit v1.2.3