diff --git a/.editorConfig b/.editorConfig
new file mode 100644
index 0000000..afbd9e3
--- /dev/null
+++ b/.editorConfig
@@ -0,0 +1,12 @@
+[*]
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+[{*.css,*.scss,*.less,*.overrides,*.variables}]
+indent_size = 4
+
+[{*.js,*.jsx,*.json,*.ts,*.tsx}]
+indent_size = 2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3c3629e..6eb5a28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
node_modules
+data/*.sqlite
diff --git a/data/.gitkeep b/data/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/html/homepage.ts b/src/html/homepage.ts
new file mode 100644
index 0000000..36190f4
--- /dev/null
+++ b/src/html/homepage.ts
@@ -0,0 +1,46 @@
+import { html } from "hono/html";
+import layout from "./layout";
+
+const homepage = layout(html`
+
+ Link Shortner
+
+
+
+
+
+
+`);
+
+export default homepage;
diff --git a/src/html/layout.ts b/src/html/layout.ts
new file mode 100644
index 0000000..3d7bae9
--- /dev/null
+++ b/src/html/layout.ts
@@ -0,0 +1,40 @@
+import { html } from "hono/html";
+
+const layout = (children: any) => html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Link Shortner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${children}
+
+
+`;
+
+export default layout;
diff --git a/src/html/linkDetails.ts b/src/html/linkDetails.ts
new file mode 100644
index 0000000..8405f76
--- /dev/null
+++ b/src/html/linkDetails.ts
@@ -0,0 +1,75 @@
+import { html } from "hono/html";
+import layout from "./layout";
+
+const linkDetails = (link: {
+ id: string;
+ url: string;
+ expires_at?: string;
+ created_at: string;
+}) =>
+ layout(html`
+
+
+ Link Shortner
+
+
+
+
+ Link Details
+
+ Target URL:
+
+ ${link.url}
+
+
+ Expires:
+
+
+
+
+ Short URL:
+
+
+ /${link.id}
+
+
+
+
+
+
+ `);
+
+export default linkDetails;
diff --git a/src/html/notFound.ts b/src/html/notFound.ts
new file mode 100644
index 0000000..27771d8
--- /dev/null
+++ b/src/html/notFound.ts
@@ -0,0 +1,24 @@
+import { html } from "hono/html";
+import layout from "./layout";
+
+const notFound = layout(html`
+
+
+ Link Shortner
+
+
+
+
+ Not Found
+ The link you are trying to access does not exist or has expired.
+
+
+`);
+
+export default notFound;
diff --git a/src/index.ts b/src/index.ts
index 3191383..d37272e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,153 @@
-import { Hono } from 'hono'
+import { Hono } from "hono";
+import { cors } from "hono/cors";
+import { etag } from "hono/etag";
+import { logger } from "hono/logger";
+import { prettyJSON } from "hono/pretty-json";
+import { serveStatic } from "hono/bun";
-const app = new Hono()
+import notFound from "./html/notFound";
+import homepage from "./html/homepage";
+import linkDetails from "./html/linkDetails";
-app.get('/', (c) => {
- return c.text('Hello Hono!')
-})
+import { Database } from "bun:sqlite";
-export default app
+const db = new Database("./data/links.sqlite", { create: true });
+
+const prepareDb = () => {
+ db.prepare(
+ `CREATE TABLE IF NOT EXISTS links (
+ id TEXT PRIMARY KEY,
+ url TEXT NOT NULL,
+ expires_at TEXT,
+ created_at TEXT NOT NULL DEFAULT current_timestamp
+ )`
+ ).run();
+};
+
+prepareDb();
+
+const deleteExpiredLinks = () => {
+ db.prepare(
+ "DELETE FROM links WHERE datetime(expires_at) < datetime('now')"
+ ).run();
+
+ setTimeout(deleteExpiredLinks, 1000 * 60 * 60);
+};
+
+deleteExpiredLinks();
+
+const app = new Hono();
+app.use(prettyJSON());
+app.notFound((c) => c.html(notFound, 404));
+app.use(etag(), logger());
+
+app.use("/favicon.ico", serveStatic({ path: "./static/favicon.ico" }));
+app.use("/logo.svg", serveStatic({ path: "./static/logo.svg" }));
+app.use("/logo.png", serveStatic({ path: "./static/logo.png" }));
+app.use(
+ "/apple-touch-icon.png",
+ serveStatic({ path: "./static/apple-touch-icon.png" })
+);
+app.use(
+ "/favicon-32x32.png",
+ serveStatic({ path: "./static/favicon-32x32.png" })
+);
+app.use(
+ "/favicon-16x16.png",
+ serveStatic({ path: "./static/favicon-16x16.png" })
+);
+
+app.use("/api/*", cors());
+
+app.get("/", (c) => {
+ return c.html(homepage);
+});
+
+app.get("/:id", (c) => {
+ const { id } = c.req.param();
+ const link = db.prepare("SELECT * FROM links WHERE id = ?").get(id) as
+ | {
+ id: string;
+ url: string;
+ expires_at: string;
+ created_at: string;
+ }
+ | undefined;
+ if (link) {
+ if (link.expires_at?.length) {
+ const expiresAt = new Date(link.expires_at);
+ if (expiresAt < new Date()) {
+ return c.html(notFound, 404);
+ }
+ }
+
+ return c.redirect(link.url);
+ }
+
+ return c.html(notFound, 404);
+});
+
+app.get("/link/:id", (c) => {
+ const { id } = c.req.param();
+ const link = db.prepare("SELECT * FROM links WHERE id = ?").get(id) as
+ | {
+ id: string;
+ url: string;
+ expires_at?: string;
+ created_at: string;
+ }
+ | undefined;
+ if (link) {
+ return c.html(linkDetails(link));
+ }
+
+ return c.html(notFound, 404);
+});
+
+app.post("/api/shorten", async (c) => {
+ const formData = await c.req.formData();
+ const url = formData.get("url") as string;
+ const expiration = formData.get("expiration") as string | null | undefined;
+
+ let expires_at = "";
+ if (expiration?.length && expiration !== "never") {
+ const date = new Date();
+ switch (expiration) {
+ case "hour":
+ date.setHours(date.getHours() + 1);
+ break;
+
+ case "day":
+ date.setDate(date.getDate() + 1);
+ break;
+
+ case "week":
+ date.setDate(date.getDate() + 7);
+ break;
+
+ case "month":
+ date.setMonth(date.getMonth() + 1);
+ break;
+
+ default:
+ break;
+ }
+
+ expires_at = date.toISOString();
+ }
+
+ const id = Math.random().toString(36).slice(2, 9);
+
+ const query = expires_at?.length
+ ? `INSERT INTO links (id, url, expires_at) VALUES ('${id}', '${url}', '${expires_at}')`
+ : `INSERT INTO links (id, url) VALUES ('${id}', '${url}')`;
+ console.log(query);
+ db.prepare(query).run();
+
+ return c.redirect(`/link/${id}`);
+});
+
+export default {
+ port: Bun.env.PORT || 8787,
+ fetch: app.fetch,
+};
diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png
new file mode 100644
index 0000000..ddf5134
Binary files /dev/null and b/static/android-chrome-192x192.png differ
diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png
new file mode 100644
index 0000000..50151e0
Binary files /dev/null and b/static/android-chrome-512x512.png differ
diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png
new file mode 100644
index 0000000..2177b8c
Binary files /dev/null and b/static/apple-touch-icon.png differ
diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png
new file mode 100644
index 0000000..7335583
Binary files /dev/null and b/static/favicon-16x16.png differ
diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png
new file mode 100644
index 0000000..c050cde
Binary files /dev/null and b/static/favicon-32x32.png differ
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..d962d93
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/logo.png b/static/logo.png
new file mode 100644
index 0000000..92b2705
Binary files /dev/null and b/static/logo.png differ
diff --git a/static/logo.svg b/static/logo.svg
new file mode 100644
index 0000000..ae6e160
--- /dev/null
+++ b/static/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file