diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..d336448 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy +on: + push: + branches: [main] + pull_request: + branches: main + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + permissions: + id-token: write # Needed for auth with Deno Deploy + contents: read # Needed to clone the repository + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Install Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Build step + run: "deno task build" # 📝 Update the build command(s) if necessary + + - name: Upload to Deno Deploy + uses: denoland/deployctl@v1 + with: + project: "example-project" # 📝 Update the deploy project name if necessary + entrypoint: "./main.ts" # 📝 Update the entrypoint if necessary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b4bdef --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# Fresh build directory +_fresh/ +# npm dependencies +node_modules/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..455f9fc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "denoland.vscode-deno", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2716b41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "deno.enable": true, + "deno.lint": true, + "editor.defaultFormatter": "denoland.vscode-deno", + "[typescriptreact]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "[javascript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "css.customData": [ + ".vscode/tailwind.json" + ], + "deno.unstable": true, +} diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json new file mode 100644 index 0000000..96a1f57 --- /dev/null +++ b/.vscode/tailwind.json @@ -0,0 +1,55 @@ +{ + "version": 1.1, + "atDirectives": [ + { + "name": "@tailwind", + "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" + } + ] + }, + { + "name": "@apply", + "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#apply" + } + ] + }, + { + "name": "@responsive", + "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" + } + ] + }, + { + "name": "@screen", + "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#screen" + } + ] + }, + { + "name": "@variants", + "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#variants" + } + ] + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..19510f4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# tdotin + +> t.in/ diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..ba49a42 --- /dev/null +++ b/deno.json @@ -0,0 +1,39 @@ +{ + "lock": false, + "tasks": { + "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", + "cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", + "manifest": "deno task cli manifest $(pwd)", + "start": "deno run -A --unstable-kv --watch=static/,routes/ dev.ts", + "build": "deno run -A dev.ts build", + "preview": "deno run -A main.ts", + "update": "deno run -A -r https://fresh.deno.dev/update ." + }, + "lint": { + "rules": { + "tags": [ + "fresh", + "recommended" + ] + } + }, + "exclude": [ + "**/_fresh/*" + ], + "imports": { + "$fresh/": "https://deno.land/x/fresh@1.6.5/", + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/", + "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", + "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", + "tailwindcss": "npm:tailwindcss@3.4.1", + "tailwindcss/": "npm:/tailwindcss@3.4.1/", + "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js", + "$std/": "https://deno.land/std@0.211.0/" + }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "nodeModulesDir": true +} diff --git a/dev.ts b/dev.ts new file mode 100644 index 0000000..ae73946 --- /dev/null +++ b/dev.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env -S deno run -A --watch=static/,routes/ + +import dev from "$fresh/dev.ts"; +import config from "./fresh.config.ts"; + +import "$std/dotenv/load.ts"; + +await dev(import.meta.url, "./main.ts", config); diff --git a/fresh.config.ts b/fresh.config.ts new file mode 100644 index 0000000..f50b17a --- /dev/null +++ b/fresh.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "$fresh/server.ts"; +import tailwind from "$fresh/plugins/tailwind.ts"; + +export default defineConfig({ + plugins: [tailwind()], +}); diff --git a/fresh.gen.ts b/fresh.gen.ts new file mode 100644 index 0000000..e7f33f8 --- /dev/null +++ b/fresh.gen.ts @@ -0,0 +1,27 @@ +// DO NOT EDIT. This file is generated by Fresh. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $_alias_ from "./routes/[alias].tsx"; +import * as $_404 from "./routes/_404.tsx"; +import * as $_app from "./routes/_app.tsx"; +import * as $api_url from "./routes/api/url.ts"; +import * as $index from "./routes/index.tsx"; +import * as $Form from "./islands/Form.tsx"; +import { type Manifest } from "$fresh/server.ts"; + +const manifest = { + routes: { + "./routes/[alias].tsx": $_alias_, + "./routes/_404.tsx": $_404, + "./routes/_app.tsx": $_app, + "./routes/api/url.ts": $api_url, + "./routes/index.tsx": $index, + }, + islands: { + "./islands/Form.tsx": $Form, + }, + baseUrl: import.meta.url, +} satisfies Manifest; + +export default manifest; diff --git a/islands/Form.tsx b/islands/Form.tsx new file mode 100644 index 0000000..b8bd595 --- /dev/null +++ b/islands/Form.tsx @@ -0,0 +1,39 @@ +import { useState } from "preact/hooks"; + +export default function Form() { + const [url, setUrl] = useState(""); + const [shortUrl, setShortUrl] = useState(""); + + const store = async () => { + const res = await fetch(location.href + "api/url", { + method: "POST", + body: JSON.stringify({ url }), + }); + const data = await res.json() as { alias: string; url: string }; + setShortUrl(`${location.href}${data.alias}`); + }; + + return ( +
+
+ setUrl((e.target as HTMLInputElement).value)} + class="rounded-md px-2 hover:outline-none focus:outline-none shadow-lg py-2 border-dotted border-black border-2 w-full" + /> + +
+ {shortUrl} +
+ ); +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..02ea409 --- /dev/null +++ b/main.ts @@ -0,0 +1,14 @@ +/// +/// +/// +/// +/// +/// + +import "$std/dotenv/load.ts"; + +import { start } from "$fresh/server.ts"; +import manifest from "./fresh.gen.ts"; +import config from "./fresh.config.ts"; + +await start(manifest, config); diff --git a/routes/[alias].tsx b/routes/[alias].tsx new file mode 100644 index 0000000..c05b8bf --- /dev/null +++ b/routes/[alias].tsx @@ -0,0 +1,12 @@ +import { defineRoute, PageProps } from "$fresh/server.ts"; +import { kv } from "../util/database.ts"; + +export default defineRoute(async (req, ctx) => { + const url = (await kv.get([ctx.params.alias])).value as string; + const headers = new Headers(); + headers.set("location", url); + return new Response(null, { + status: 303, + headers, + }); +}); diff --git a/routes/_404.tsx b/routes/_404.tsx new file mode 100644 index 0000000..c63ae2e --- /dev/null +++ b/routes/_404.tsx @@ -0,0 +1,27 @@ +import { Head } from "$fresh/runtime.ts"; + +export default function Error404() { + return ( + <> + + 404 - Page not found + +
+
+ the Fresh logo: a sliced lemon dripping with juice +

404 - Page not found

+

+ The page you were looking for doesn't exist. +

+ Go back home +
+
+ + ); +} diff --git a/routes/_app.tsx b/routes/_app.tsx new file mode 100644 index 0000000..902ca15 --- /dev/null +++ b/routes/_app.tsx @@ -0,0 +1,16 @@ +import { type PageProps } from "$fresh/server.ts"; +export default function App({ Component }: PageProps) { + return ( + + + + + tdotin + + + + + + + ); +} diff --git a/routes/api/url.ts b/routes/api/url.ts new file mode 100644 index 0000000..b53e911 --- /dev/null +++ b/routes/api/url.ts @@ -0,0 +1,20 @@ +import { FreshContext } from "$fresh/server.ts"; +import { kv } from "../../util/database.ts"; +import { createAlias } from "../../util/index.ts"; + +export const handler = async ( + req: Request, + _ctx: FreshContext, +): Promise => { + const alias = createAlias(5); + const { url } = await req.json() as NewUrl; + await kv.set([alias], [url]); + return new Response(JSON.stringify({ + alias, + url, + })); +}; + +interface NewUrl { + url: string; +} diff --git a/routes/index.tsx b/routes/index.tsx new file mode 100644 index 0000000..201b0d8 --- /dev/null +++ b/routes/index.tsx @@ -0,0 +1,9 @@ +import Form from "../islands/Form.tsx"; + +export default function Home() { + return ( +
+
+
+ ); +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..1cfaaa2 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/logo.svg b/static/logo.svg new file mode 100644 index 0000000..ef2fbe4 --- /dev/null +++ b/static/logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/static/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..99dba8f --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,7 @@ +import { type Config } from "tailwindcss"; + +export default { + content: [ + "{routes,islands,components}/**/*.{ts,tsx}", + ], +} satisfies Config; diff --git a/util/database.ts b/util/database.ts new file mode 100644 index 0000000..28ffb7f --- /dev/null +++ b/util/database.ts @@ -0,0 +1 @@ +export const kv = await Deno.openKv(); diff --git a/util/index.ts b/util/index.ts new file mode 100644 index 0000000..8a29ca0 --- /dev/null +++ b/util/index.ts @@ -0,0 +1,10 @@ +const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"; + +export function createAlias(len: number) { + let alias = ""; + for (let i = 0; i < len; i++) { + const randomIndex = Math.floor(Math.random() * 36); + alias += CHARS.at(randomIndex); + } + return alias; +}