diff --git a/Makefile b/Makefile index e69de29..d0a5f14 100644 --- a/Makefile +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: install frontend backend all dev + +install: + npm ci --prefix frontend + +frontend: + VITE_API_URL=/api npm run --prefix frontend build + +backend: + CGO_ENABLED=0 go build -o goshort goshort.go + +all: + make frontend + make backend + +serve: + go run goshort.go serve + +dev: + go run goshort.go dev + +lint: + golangci-lint run + +lint-fix: + golangci-lint run --fix diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e41cb7e..c838d12 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,10 +8,12 @@ "name": "goshort", "version": "0.0.0", "dependencies": { + "@heroicons/react": "^2.0.18", "classnames": "^2.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.15.0" + "react-router-dom": "^6.15.0", + "react-use": "^17.4.0" }, "devDependencies": { "@marolint/eslint-config-react": "^1.0.2", @@ -52,7 +54,6 @@ "version": "7.22.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -468,6 +469,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@heroicons/react": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz", + "integrity": "sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -831,6 +840,11 @@ "node": ">=10" } }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -1103,6 +1117,11 @@ "vite": "^4" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1628,6 +1647,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1642,6 +1669,26 @@ "node": ">= 8" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1657,8 +1704,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1762,6 +1808,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -2359,8 +2413,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -2408,6 +2461,21 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-loops": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", + "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -2763,6 +2831,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2813,6 +2886,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/inline-style-prefixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", + "integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==", + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -3187,6 +3269,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3330,6 +3417,11 @@ "node": ">=10" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3390,6 +3482,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-css": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz", + "integrity": "sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==", + "dependencies": { + "css-tree": "^1.1.2", + "csstype": "^3.0.6", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^6.0.0", + "rtl-css-js": "^1.14.0", + "sourcemap-codec": "^1.4.8", + "stacktrace-js": "^2.0.2", + "stylis": "^4.0.6" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -3983,6 +4094,45 @@ "react-dom": ">=16.8" } }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.4.0.tgz", + "integrity": "sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.3.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-use/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4027,8 +4177,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", @@ -4047,6 +4196,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", @@ -4114,6 +4268,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4177,6 +4339,17 @@ "loose-envify": "^1.1.0" } }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4192,6 +4365,14 @@ "node": ">=10" } }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4236,6 +4417,14 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -4245,6 +4434,52 @@ "node": ">=0.10.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" + }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -4342,6 +4577,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -4472,6 +4712,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4484,6 +4732,16 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -4505,8 +4763,7 @@ "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/frontend/package.json b/frontend/package.json index 9567ad9..066f8ad 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@heroicons/react": "^2.0.18", "classnames": "^2.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.15.0" + "react-router-dom": "^6.15.0", + "react-use": "^17.4.0" }, "devDependencies": { "@marolint/eslint-config-react": "^1.0.2", diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index 592d97d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,5 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f43da81..2358255 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,37 +2,39 @@ import { FunctionComponent } from "react" import { Outlet } from "react-router-dom" -import "./App.css" import Button from "./components/Button" +import Container from "./components/Container" import { useIsAuthenticated } from "./hooks/useAuth" const App: FunctionComponent = () => { const isAuthenticated = useIsAuthenticated() return ( -
-
-
-
-
- {isAuthenticated && ( - <> -
-
- {isAuthenticated ||
-
+
- +
+
+
+
+ {isAuthenticated && ( + <> +
+
+ {isAuthenticated ||
+
+
+ +
-
+ ) } diff --git a/frontend/src/components/Container.tsx b/frontend/src/components/Container.tsx new file mode 100644 index 0000000..be49b9f --- /dev/null +++ b/frontend/src/components/Container.tsx @@ -0,0 +1,11 @@ +import { FunctionComponent, PropsWithChildren } from "react" + +const Container: FunctionComponent = ({ children }) => { + return ( +
+ {children} +
+ ) +} + +export default Container diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..c8fc0fe --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,11 @@ +import { FunctionComponent } from "react" + +const Header: FunctionComponent<{ title: string }> = ({ title }) => { + return ( +
+

{title}

+
+ ) +} + +export default Header diff --git a/frontend/src/components/ShortItem.tsx b/frontend/src/components/ShortItem.tsx new file mode 100644 index 0000000..b904770 --- /dev/null +++ b/frontend/src/components/ShortItem.tsx @@ -0,0 +1,143 @@ +import { FunctionComponent, useCallback, useEffect, useState } from "react" + +import { + ArrowRightIcon, + ArrowTopRightOnSquareIcon, + ChevronRightIcon, + ClipboardIcon, + TrashIcon, +} from "@heroicons/react/24/outline" +import { Link } from "react-router-dom" + +import { Short } from "../types" + +const ShortItem: FunctionComponent void }> = ({ + name, + url, + doDelete, +}) => { + const origin = location.origin + const host = "marona.to" + + const maxSize = 190 + url = `${url}/fdfbdsjhbfsjhbfsjdhbfdsjhfbsjdhbfjshdbfjshdbfjshbfhsjbfdjhfbshjfbsdjhfbsjhfbsjdhfbsjhdfbsjdfhbdnfjkdsnfsjkfnsdkjfndskjfnskjfnskdjfnskjdfdfbdsjhbfsjhbfsjdhbfdsjhfbsjdhbfjshdbfjshdbfjshbfhsjbfdjhfbshjfbsdjhfbsjhfbsjdhfbsjhdfbsjdfhbdnfjkdsnfsjkfnsdkjfndskjfnskjfnskdjfnskjd` + const displayURL = + url.length >= maxSize - 3 ? `${url.slice(0, maxSize)}...` : url + + const shortNameURL = `${origin}/${name}` + + const [copied, copy] = useClipboardTimeout(shortNameURL) + + const [deleting, triggerDelete] = useDoubleclickDelete(doDelete) + + return ( +
  • +
    +
    + + + {host}/ + {name} + + + + + +
    + + + {copied ? "Copied!" : "Copy"} + + + + {deleting ? "Are you sure?" : "Delete"} + +
    +
    +
    + +
    +
    + + + {displayURL} + + + + + +
    +
    +

    + 1000 views +

    +

    + Last viewed +

    + +
    + details + + +
    + +
    +
    +
  • + ) +} + +export default ShortItem + +const useClipboardTimeout = (text: string): [boolean, () => void] => { + const [copied, setCopied] = useState(false) + useEffect(() => { + if (copied) { + const timeout = setTimeout(() => { + setCopied(false) + }, 2000) + return () => clearTimeout(timeout) + } + }, [copied]) + + const copy = useCallback( + () => navigator.clipboard.writeText(text).then(() => setCopied(true)), + [text] + ) + + return [copied, copy] +} + +const useDoubleclickDelete = (doDelete: () => void): [boolean, () => void] => { + const [deleting, setDeleting] = useState(false) + useEffect(() => { + if (deleting) { + const timeout = setTimeout(() => { + setDeleting(false) + }, 5000) + return () => clearTimeout(timeout) + } + }, [deleting]) + + const trigger = useCallback(() => { + if (deleting) { + doDelete() + } else { + setDeleting(true) + } + }, [doDelete, deleting]) + + return [deleting, trigger] +} diff --git a/frontend/src/hooks/useAuth.tsx b/frontend/src/hooks/useAuth.tsx index 48a2df4..ec47dee 100644 --- a/frontend/src/hooks/useAuth.tsx +++ b/frontend/src/hooks/useAuth.tsx @@ -131,7 +131,9 @@ export const signupLoader = loginLoader export const logoutLoader: LoaderFunction = async () => { await AuthProvider.logout() - + setTimeout(() => { + location.reload() + }, 100) return redirect("/") } @@ -142,7 +144,7 @@ export const protectedLoader: LoaderFunction = async ({ request }) => { if (!AuthProvider.isAuthenticated) { const params = new URLSearchParams() params.set("from", new URL(request.url).pathname) - return redirect("/login?" + params.toString()) + return redirect("/lgn?" + params.toString()) } return null } diff --git a/frontend/src/index.css b/frontend/src/index.css index 5465d19..62d2254 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,5 +3,6 @@ @tailwind utilities; :root { + @apply text-slate-700; font-family: -apple-system, SF Pro Text, SF UI Text, system-ui, Helvetica Neue, Helvetica, Arial, sans-serif; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c077ddc..13e2508 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -12,6 +12,6 @@ if (!rootEl) throw new Error("Root element not found") createRoot(rootEl).render( - Loading...

    } /> +
    ) diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx index a9e8551..4a98e1b 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/Index.tsx @@ -1,7 +1,9 @@ import { FunctionComponent } from "react" -const IndexPage: FunctionComponent = () => { - return
    Index
    +export const Component: FunctionComponent = () => { + return ( +
    +

    hello

    +
    + ) } - -export default IndexPage diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 709f9ff..47fc849 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -5,6 +5,8 @@ import { useNavigation, } from "react-router-dom" +import Header from "../components/Header" + export function Component() { const location = useLocation() const params = new URLSearchParams(location.search) @@ -16,7 +18,8 @@ export function Component() { const actionData = useActionData() as { error: string } | undefined return ( -
    + <> +

    You must log in to view the page at {from}

    @@ -34,7 +37,7 @@ export function Component() {

    {actionData.error}

    ) : null}
    -
    + ) } diff --git a/frontend/src/pages/Sessions.tsx b/frontend/src/pages/Sessions.tsx index 2127517..9e9006e 100644 --- a/frontend/src/pages/Sessions.tsx +++ b/frontend/src/pages/Sessions.tsx @@ -1,5 +1,6 @@ import { LoaderFunction, json, useLoaderData } from "react-router-dom" +import Header from "../components/Header" import { protectedLoader } from "../hooks/useAuth" type Session = { @@ -35,14 +36,14 @@ export const loader: LoaderFunction = async (args) => { export function Component() { const data = useLoaderData() as Session[] return ( -
    -
    Sessions
    + <> +
      {data.map((s) => (
    • {s.title}
    • ))}
    -
    + ) } diff --git a/frontend/src/pages/ShortDetails.tsx b/frontend/src/pages/ShortDetails.tsx new file mode 100644 index 0000000..f245833 --- /dev/null +++ b/frontend/src/pages/ShortDetails.tsx @@ -0,0 +1,30 @@ +import { LoaderFunction, redirect, useLoaderData } from "react-router-dom" + +import Header from "../components/Header" +import { protectedLoader } from "../hooks/useAuth" +import { Short } from "../types" +import fetchAPI from "../util/fetchAPI" + +export function Component() { + const short = useLoaderData() as Short + + return ( + <> +
    + {short.url} + + ) +} + +export const loader: LoaderFunction = async (args) => { + const resp = await protectedLoader(args) + if (resp) return resp + + const data = await fetchAPI("/shorts") + if (data.ok) { + return data.data?.find((short) => short.name === args.params.name) + } + return redirect("/lgo") +} + +Component.displayName = "ShortDetailsPage" diff --git a/frontend/src/pages/Shorts.tsx b/frontend/src/pages/Shorts.tsx index e54b71c..5e6e620 100644 --- a/frontend/src/pages/Shorts.tsx +++ b/frontend/src/pages/Shorts.tsx @@ -1,37 +1,92 @@ -import { LoaderFunction, redirect, useLoaderData } from "react-router-dom" +import { + FunctionComponent, + useCallback, + useEffect, + useMemo, + useState, +} from "react" +import { + LoaderFunction, + redirect, + useLoaderData, + useRevalidator, +} from "react-router-dom" + +import Header from "../components/Header" +import ShortItem from "../components/ShortItem" import { protectedLoader } from "../hooks/useAuth" +import { Short } from "../types" import fetchAPI from "../util/fetchAPI" -type Short = { - name: string - url: string +export function Component() { + const sourceDataDefault = useMemo(() => [], []) + const sourceData = (useLoaderData() ?? sourceDataDefault) as Short[] + + const [data, setData] = useState( + sourceData + .map((short) => short) + .sort((a, b) => a.name.localeCompare(b.name)) + ) + useEffect(() => { + setData( + sourceData + .map((short) => short) + .sort((a, b) => a.name.localeCompare(b.name)) + ) + }, [sourceData]) + + const { revalidate } = useRevalidator() + const deleteShort = useCallback( + (name: string) => async () => { + // optimistic update + setData((data) => data.filter((short) => short.name !== name)) + // do the actual delete + await fetchAPI(`/shorts/${name}`, { + method: "DELETE", + }) + + revalidate() + }, + [revalidate] + ) + + const Shorts: FunctionComponent = () => { + return ( +
      + {data.map((short) => ( + + ))} +
    + ) + } + const NoShorts = () => { + return
    No Shorts
    + } + + return ( + <> +
    + {data.length > 0 ? : } + + ) } export const loader: LoaderFunction = async (args) => { const resp = await protectedLoader(args) if (resp) return resp - const data = await fetchAPI("/short") + const data = await fetchAPI("/shorts") if (data.ok) { return data.data } - return redirect("/logout") -} - -export function Component() { - const data = (useLoaderData() ?? []) as Short[] - console.log(data) - return ( -
    -
    Shorts
    -
      - {data.map((s) => ( -
    • {s.name}
    • - ))} -
    -
    - ) + return redirect("/lgo") } Component.displayName = "ShortsPage" diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index 709f9ff..d972600 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -5,6 +5,8 @@ import { useNavigation, } from "react-router-dom" +import Header from "../components/Header" + export function Component() { const location = useLocation() const params = new URLSearchParams(location.search) @@ -16,7 +18,8 @@ export function Component() { const actionData = useActionData() as { error: string } | undefined return ( -
    + <> +

    You must log in to view the page at {from}

    @@ -34,7 +37,7 @@ export function Component() {

    {actionData.error}

    ) : null}
    -
    + ) } diff --git a/frontend/src/pages/Tokens.tsx b/frontend/src/pages/Tokens.tsx index c4aadf5..2ee69a0 100644 --- a/frontend/src/pages/Tokens.tsx +++ b/frontend/src/pages/Tokens.tsx @@ -1,5 +1,6 @@ import { LoaderFunction, json, useLoaderData } from "react-router-dom" +import Header from "../components/Header" import { protectedLoader } from "../hooks/useAuth" type Token = { @@ -22,14 +23,14 @@ export const loader: LoaderFunction = async (args) => { export function Component() { const data = useLoaderData() as Token[] return ( -
    -
    Tokens
    + <> +
      {data.map((s) => (
    • {s.name}
    • ))}
    -
    + ) } diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index f52aa68..b249d89 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -6,10 +6,10 @@ import { loginAction, loginLoader, logoutLoader, + protectedLoader, signupLoader, singupAction, } from "./hooks/useAuth" -import IndexPage from "./pages/Index" import NotFound from "./pages/NotFound" export default createBrowserRouter([ @@ -21,33 +21,48 @@ export default createBrowserRouter([ // via our `useUser` hook. loader: indexLoader, children: [ - { index: true, element: }, { - path: "login", + index: true, + lazy: () => import("./pages/Index"), + loader: protectedLoader, + }, + { + id: "login", + path: "lgn", lazy: () => import("./pages/Login"), loader: loginLoader, action: loginAction, }, { - path: "signup", + id: "signup", + path: "sgn", lazy: () => import("./pages/Signup"), loader: signupLoader, action: singupAction, }, { - path: "logout", + id: "logout", + path: "lgo", loader: logoutLoader, }, { - path: "shorts", + id: "shorts", + path: "sht", lazy: () => import("./pages/Shorts"), }, { - path: "tokens", + id: "shortDetails", + path: "/sht/:name", + lazy: () => import("./pages/ShortDetails"), + }, + { + id: "tokens", + path: "tkn", lazy: () => import("./pages/Tokens"), }, { - path: "sessions", + id: "sessions", + path: "ses", lazy: () => import("./pages/Sessions"), }, { diff --git a/frontend/src/util/fetchAPI.ts b/frontend/src/util/fetchAPI.ts index 48104b8..74bef73 100644 --- a/frontend/src/util/fetchAPI.ts +++ b/frontend/src/util/fetchAPI.ts @@ -2,22 +2,30 @@ export default async function ( path: string, args: Parameters[1] = {} -): Promise<{ data: T; ok: true } | { data: null; ok: false }> { - args.credentials = "include" +): Promise<{ data: T | null; ok: boolean }> { + if (import.meta.env.DEV) { + args.credentials = "include" + } const response = await fetch( `${import.meta.env.VITE_API_URL || ""}${path}`, args ) + let responseData: T | null = null + try { + responseData = await response.json() + } catch (e) { + responseData = null + } + // if the response was not ok if (!response.ok) { console.error(response.statusText) - return { data: null, ok: false } + return { data: responseData, ok: false } } - // on a successfull response, return the response - const data: T = await response.json() - return { data, ok: true } + // on a successfull response, return the data + return { data: responseData, ok: true } } diff --git a/internal/server/api/router.go b/internal/server/api/router.go index 0e0fd50..11f7001 100644 --- a/internal/server/api/router.go +++ b/internal/server/api/router.go @@ -24,9 +24,9 @@ func NewAPIRouter(h *APIHandler) http.Handler { r.Delete("/me", h.DeleteMe) // Shorts routes - r.Get("/short", h.ListShorts) - r.Post("/short", h.CreateShort) - r.Delete("/short/{short}", h.DeleteShort) + r.Get("/shorts", h.ListShorts) + r.Post("/shorts", h.CreateShort) + r.Delete("/shorts/{short}", h.DeleteShort) }) return mux diff --git a/internal/server/middleware/session.go b/internal/server/middleware/session.go index 8fb7515..f76f9b5 100644 --- a/internal/server/middleware/session.go +++ b/internal/server/middleware/session.go @@ -20,7 +20,7 @@ func Session(cfg *config.Config) func(http.Handler) http.Handler { Path: "/", SameSite: http.SameSiteLaxMode, Persist: true, - Secure: cfg.Prod, + // Secure: cfg.Prod, } return func(next http.Handler) http.Handler {