diff --git a/web/package-lock.json b/web/package-lock.json index 6ef7222..5552126 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@phosphor-icons/react": "^2.0.8", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -21,6 +22,9 @@ "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "tailwindcss": "^3.3.1" } }, "node_modules/@adobe/css-tools": { @@ -3051,6 +3055,18 @@ "node": ">= 8" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.0.8.tgz", + "integrity": "sha512-VWACI+MkRGpol4htOcVtWKaDCosrcuCg8toJfPS0osgVjxM8i/KoSZSPxQvG5XYPCI8iyJoHKRpSfzOISAXFyg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -4468,27 +4484,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-node/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", @@ -4631,6 +4626,11 @@ "node": ">=4" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -6330,14 +6330,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6405,22 +6397,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -11321,6 +11297,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -11909,6 +11893,16 @@ "multicast-dns": "cli.js" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -15339,6 +15333,67 @@ "postcss": "^8.2.15" } }, + "node_modules/sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -15483,19 +15538,19 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "node_modules/tailwindcss": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", - "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", + "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", "dependencies": { "arg": "^5.0.2", "chokidar": "^3.5.3", "color-name": "^1.1.4", - "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", + "jiti": "^1.17.2", "lilconfig": "^2.0.6", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -15509,7 +15564,8 @@ "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "quick-lru": "^5.1.1", - "resolve": "^1.22.1" + "resolve": "^1.22.1", + "sucrase": "^3.29.0" }, "bin": { "tailwind": "lib/cli.js", @@ -15659,6 +15715,25 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -15739,6 +15814,11 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -16954,14 +17034,6 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/web/package.json b/web/package.json index 6610bb3..6f369dd 100644 --- a/web/package.json +++ b/web/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@phosphor-icons/react": "^2.0.8", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -40,5 +41,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "tailwindcss": "^3.3.1" } } diff --git a/web/public/index.html b/web/public/index.html index aa069f2..4a851df 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -2,12 +2,12 @@ - + - React App + URL Shortener - Gimly diff --git a/web/src/App.css b/web/src/App.css deleted file mode 100644 index feebe61..0000000 --- a/web/src/App.css +++ /dev/null @@ -1,131 +0,0 @@ -.index-page { - display: flex; - flex-direction: column; - align-items: center; - padding: 2rem; - font-family: Arial, sans-serif; -} - -h1 { - margin: 0 0 2rem; -} - -form { - display: flex; - flex-direction: column; - margin-bottom: 2rem; - max-width: 500px; - /* Set a maximum width for the form */ - width: 100%; - /* Set the form to take up the full width of its container */ -} - -.form-row { - display: flex; - flex-direction: column; - margin-bottom: 1rem; -} - -.form-row label { - margin-bottom: 0.5rem; - font-weight: bold; -} - -.text-input { - padding: 0.5rem; - font-size: 1rem; - border: 1px solid #ccc; - border-radius: 0.25rem; - flex: 1; - /* Use flexbox to make the text input expand to fill its container */ -} - -.submit-button { - padding: 0.5rem 1rem; - font-size: 1rem; - background-color: #007bff; - color: #fff; - border: none; - border-radius: 0.25rem; - cursor: pointer; - width: 100%; - /* Set the button to take up the full width of its container */ - margin-top: 1rem; - /* Add a bit of spacing between the button and the text input */ -} - -.shortened-url { - margin-top: 2rem; - text-align: center; -} - -.shortened-url p { - margin-bottom: 0.5rem; - font-weight: bold; -} - -.shortened-url a { - color: #007bff; - text-decoration: none; -} - -.shortened-url a:hover { - text-decoration: underline; -} - -h2 { - margin-top: 2rem; - margin-bottom: 1rem; - font-weight: bold; -} - -.url-list { - list-style: none; - margin: 0; - padding: 0; - max-width: 500px; - /* Set a maximum width for the URL list */ - width: 100%; - /* Set the URL list to take up the full width of its container */ -} - -.url-list li { - background-color: #fff; - border: 1px solid #ddd; - border-radius: 3px; - margin-bottom: 10px; - padding: 10px; - display: flex; - justify-content: space-between; -} - -.url-list .url-container { - display: flex; - flex-direction: column; -} - -.url-list .url-title { - font-weight: bold; - margin-bottom: 5px; -} - -.url-list .url-url { - font-size: 0.6; - color: #999; - margin-bottom: 5px; -} - -.url-list .url-click-count { - font-size: 0.6rem; - color: #999; -} - -.url-list .copy-button { - padding: 5px; - border: none; - background-color: #ddd; - color: #555; - cursor: pointer; - font-size: 14px; - border-radius: 3px; -} \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index 02ea825..b179085 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,14 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import './App.css'; +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import "./index.css"; +import { + ArrowRight, + ArrowUpRight, + Copy, + FileText, + LinkSimple, + WarningCircle, +} from "@phosphor-icons/react"; interface Url { url: { @@ -9,7 +17,7 @@ interface Url { }; short_id: string; click_count: number; -}; +} interface ApiResponse { data: Url[]; @@ -20,28 +28,67 @@ interface ApiError { error: string; } +interface IUrlListItemProps { + title?: string; + url: string; +} + +const UrlListItem = (props: IUrlListItemProps) => { + const { title = "Untitled link", url } = props; + + return ( +
  • +
    +

    {title}

    +
    +
    + + {url} + +
    +
    +
    + + +
    Click count: #0 click
    +
    +
  • + ); +}; + const IndexPage: React.FC = () => { - const [title, setTitle] = useState(''); - const [url, setUrl] = useState(''); + const [title, setTitle] = useState(""); + const [url, setUrl] = useState(""); const [urls, setUrls] = useState([]); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); try { - const response = await axios.post('/api/url', { data: { title, url } }); + const response = await axios.post("/api/url", { + data: { title, url }, + }); setUrls([...urls, response.data]); - setTitle(''); - setUrl(''); - setError(''); + setTitle(""); + setUrl(""); + setError(""); } catch (error: any) { const apiError = error.response.data as ApiError; setError(apiError.error); } }; - const handleCopyClick = (event: React.MouseEvent, url: string) => { + const handleCopyClick = ( + event: React.MouseEvent, + url: string + ) => { event.preventDefault(); navigator.clipboard.writeText(url); }; @@ -49,7 +96,7 @@ const IndexPage: React.FC = () => { useEffect(() => { const fetchUrls = async () => { try { - const response = await axios.get('/api/url'); + const response = await axios.get("/api/url"); setUrls(response.data.data); } catch (error) { console.error(error); @@ -59,55 +106,121 @@ const IndexPage: React.FC = () => { }, []); return ( -
    -

    URL Shortener

    -
    -
    - - setTitle(event.target.value)} - /> -
    -
    - - setUrl(event.target.value)} - /> - {error &&

    {error}

    } +
    +
    +
    +

    + A simple yet powerful URL shortener for everyone. +

    + +

    + Just paste your loooong 🧐 and boooring 😤 URL, and see the magic + happen! 🎉 +

    - - - {urls.length > 0 && ( -
      - {urls.map((url) => ( -
    • -
      -
      {url.url.title}
      - - {(url.click_count || 0) > 0 && -
      {url.click_count} clicks
      - } + {/* header */} +
      +
      +
      + +
      +
      + +
      + setUrl(event.target.value)} + />
      -
      + +
      + +
      +
      + +
      + setTitle(event.target.value)} + /> +
      +
      + +
      + -
    • - ))} -
    - ) - } -
    +
    + + +
    +
    +
    + Shortened links. +
    + + {/* NOTE: Can be deleted if unnecessary */} + {urls.length > 0 && ( +
    Total: #count
    + )} +
    + + {urls.length ? ( +
      + + + +
    + ) : ( +

    + There is no + shortened link yet. +

    + )} +
    +
    + {/* content */} +
    + Gimly © {new Date().getFullYear()} + + + GitHub + +
    + {/* footer */} +
    + {/* container */} + ); -} +}; -export default IndexPage; \ No newline at end of file +export default IndexPage; diff --git a/web/src/index.css b/web/src/index.css index ec2585e..7b21eb8 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,13 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + background-color: #f4f4f5; + background-image: radial-gradient( + rgba(136, 136, 137, 0.4) 1.05px, + #f4f4f5 1.05px + ); + background-size: 21px 21px; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; +button, +input { + @apply !outline-none; } diff --git a/web/src/index.tsx b/web/src/index.tsx index 032464f..ccbac81 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); root.render( diff --git a/web/tailwind.config.js b/web/tailwind.config.js new file mode 100644 index 0000000..37cc651 --- /dev/null +++ b/web/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +};