From 34853c0dbdad490c2be37a150e605190a19d3a6a Mon Sep 17 00:00:00 2001
From: Gustavo Maronato
Date: Sat, 19 Aug 2023 02:03:42 -0300
Subject: [PATCH] going along
---
Makefile | 26 +++
frontend/package-lock.json | 277 +++++++++++++++++++++++++-
frontend/package.json | 4 +-
frontend/src/App.css | 5 -
frontend/src/App.tsx | 48 ++---
frontend/src/components/Container.tsx | 11 +
frontend/src/components/Header.tsx | 11 +
frontend/src/components/ShortItem.tsx | 143 +++++++++++++
frontend/src/hooks/useAuth.tsx | 6 +-
frontend/src/index.css | 1 +
frontend/src/main.tsx | 2 +-
frontend/src/pages/Index.tsx | 10 +-
frontend/src/pages/Login.tsx | 7 +-
frontend/src/pages/Sessions.tsx | 7 +-
frontend/src/pages/ShortDetails.tsx | 30 +++
frontend/src/pages/Shorts.tsx | 97 +++++++--
frontend/src/pages/Signup.tsx | 7 +-
frontend/src/pages/Tokens.tsx | 7 +-
frontend/src/router.tsx | 31 ++-
frontend/src/util/fetchAPI.ts | 20 +-
internal/server/api/router.go | 6 +-
internal/server/middleware/session.go | 2 +-
22 files changed, 663 insertions(+), 95 deletions(-)
delete mode 100644 frontend/src/App.css
create mode 100644 frontend/src/components/Container.tsx
create mode 100644 frontend/src/components/Header.tsx
create mode 100644 frontend/src/components/ShortItem.tsx
create mode 100644 frontend/src/pages/ShortDetails.tsx
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 && }
-
-
+
-
+
+
+
+
+
+ {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 (
+
+ )
+}
+
+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 (
+
+
+
+
+
+
+
+ 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 (
+
+ )
}
-
-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}
-
+ >
)
}
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}
-
+ >
)
}
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 {