diff --git a/package.json b/package.json index a4a44a3..dd0a5d8 100644 --- a/package.json +++ b/package.json @@ -31,15 +31,16 @@ "autoprefixer": "^10", "classnames": "^2.3.2", "daisyui": "^3.9.3", + "dayjs": "^1.11.10", "eslint": "^8", "eslint-config-next": "13.5.6", "fetch-mock": "npm:@gr2m/fetch-mock@9.11.0-pull-request-644.1", "jsdom": "^22.1.0", "next-auth": "^4.24.3", - "nock": "^13.3.6", "node-mocks-http": "^1.13.0", "postcss": "^8", "react-icons": "^4.11.0", + "sharp": "^0.32.6", "tailwindcss": "^3", "typescript": "^5", "vitest": "^0.34.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2e40c4..3e242e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ devDependencies: daisyui: specifier: ^3.9.3 version: 3.9.3 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 eslint: specifier: ^8 version: 8.51.0 @@ -76,9 +79,6 @@ devDependencies: next-auth: specifier: ^4.24.3 version: 4.24.3(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) - nock: - specifier: ^13.3.6 - version: 13.3.6 node-mocks-http: specifier: ^1.13.0 version: 1.13.0 @@ -88,6 +88,9 @@ devDependencies: react-icons: specifier: ^4.11.0 version: 4.11.0(react@18.2.0) + sharp: + specifier: ^0.32.6 + version: 0.32.6 tailwindcss: specifier: ^3 version: 3.3.3 @@ -1423,10 +1426,18 @@ packages: dequal: 2.0.3 dev: true + /b4a@1.6.4: + resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + /before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} dev: true @@ -1436,6 +1447,14 @@ packages: engines: {node: '>=8'} dev: true + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1460,6 +1479,13 @@ packages: node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1550,6 +1576,10 @@ packages: fsevents: 2.3.3 dev: true + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true + /classnames@2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: true @@ -1576,6 +1606,21 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: true + + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: true + /colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} dev: true @@ -1679,6 +1724,10 @@ packages: whatwg-url: 12.0.1 dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: true + /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1705,6 +1754,13 @@ packages: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: true + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -1735,6 +1791,11 @@ packages: which-typed-array: 1.1.13 dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -1776,6 +1837,11 @@ packages: engines: {node: '>=6'} dev: true + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: true + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -1828,6 +1894,12 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -2260,10 +2332,19 @@ packages: engines: {node: '>=0.10.0'} dev: true + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: true + /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -2352,6 +2433,10 @@ packages: engines: {node: '>= 0.6'} dev: true + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -2413,6 +2498,10 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2586,6 +2675,10 @@ packages: safer-buffer: 2.1.2 dev: true + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -2620,6 +2713,10 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + /internal-slot@1.0.5: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} @@ -2645,6 +2742,10 @@ packages: is-typed-array: 1.1.12 dev: true + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: true + /is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} @@ -2936,10 +3037,6 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true - /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3115,6 +3212,11 @@ packages: hasBin: true dev: true + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -3130,6 +3232,10 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: true + /mlly@1.4.2: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: @@ -3159,6 +3265,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: true + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -3231,15 +3341,15 @@ packages: - '@babel/core' - babel-plugin-macros - /nock@13.3.6: - resolution: {integrity: sha512-lT6YuktKroUFM+27mubf2uqQZVy2Jf+pfGzuh9N6VwdHlFoZqvi4zyxFTVR1w/ChPqGY6yxGehHp6C3wqCASCw==} - engines: {node: '>= 10.13'} + /node-abi@3.51.0: + resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} + engines: {node: '>=10'} dependencies: - debug: 4.3.4 - json-stringify-safe: 5.0.1 - propagate: 2.0.1 - transitivePeerDependencies: - - supports-color + semver: 7.5.4 + dev: true + + /node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} dev: true /node-mocks-http@1.13.0: @@ -3579,6 +3689,25 @@ packages: resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==} dev: true + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.51.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3614,15 +3743,17 @@ packages: react-is: 16.13.1 dev: true - /propagate@2.0.1: - resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} - engines: {node: '>= 8'} - dev: true - /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -3642,11 +3773,25 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: true + /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} dev: true + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -3693,6 +3838,15 @@ packages: pify: 2.3.0 dev: true + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3863,6 +4017,21 @@ packages: has-property-descriptors: 1.0.0 dev: true + /sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.1 + semver: 7.5.4 + simple-get: 4.0.1 + tar-fs: 3.0.4 + tunnel-agent: 0.6.0 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3887,6 +4056,24 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: true + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: true + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3920,6 +4107,13 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + /streamx@2.15.1: + resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: true + /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} dependencies: @@ -3959,6 +4153,12 @@ packages: es-abstract: 1.22.2 dev: true + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3978,6 +4178,11 @@ packages: min-indent: 1.0.1 dev: true + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4078,6 +4283,42 @@ packages: engines: {node: '>=6'} dev: true + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: true + + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.6 + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /tar-stream@3.1.6: + resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + dependencies: + b4a: 1.6.4 + fast-fifo: 1.3.2 + streamx: 2.15.1 + dev: true + /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -4177,6 +4418,12 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} diff --git a/src/app/api/github/route.ts b/src/app/api/github/route.ts new file mode 100644 index 0000000..e55a18d --- /dev/null +++ b/src/app/api/github/route.ts @@ -0,0 +1,6 @@ +import { deleteRepo } from '@/pkg/github/api/delete'; +import { patch } from '@/pkg/github/api/patch'; +export { + deleteRepo as DELETE, + patch as PATCH +} diff --git a/src/app/github/[name]/page.tsx b/src/app/github/[name]/page.tsx deleted file mode 100644 index e73c2f0..0000000 --- a/src/app/github/[name]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import RepoPage from '@/pkg/github/pages/RepoPage'; - - -export default RepoPage diff --git a/src/app/github/layout.tsx b/src/app/github/layout.tsx deleted file mode 100644 index 133dcdf..0000000 --- a/src/app/github/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import GitHubProvider from '@/pkg/github/contexts/GitHubContext'; -import { PropsWithChildren } from 'react'; - -export default function Layout({children}: PropsWithChildren) { - return ( - - {children} - - ) -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3bd3fd7..f134be2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,8 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' -import Layout from '@/pkg/ui/components/Layout' -import AuthProvider from '@/pkg/auth/context/AuthContext' import classNames from 'classnames' -import ReactQueryProvider from '@/pkg/ui/contexts/ReactQueryContext' +import ThemeProvider from '@/pkg/ui/contexts/ThemeContext' const inter = Inter({ subsets: ['latin'] }) @@ -25,13 +23,9 @@ export default function RootLayout({ 'bg-gray-200 min-h-screen': true })} > - - - - {children} - - - + + {children} + ) diff --git a/src/pkg/auth/types.ts b/src/pkg/auth/types.ts index dae3f88..aa6e10d 100644 --- a/src/pkg/auth/types.ts +++ b/src/pkg/auth/types.ts @@ -1,5 +1,5 @@ -import NextAuth, { DefaultSession, Profile } from 'next-auth' -import { JWT } from 'next-auth/jwt' +import { DefaultSession, Profile } from 'next-auth' +import { DefaultJWT } from 'next-auth/jwt' declare module 'next-auth' { interface Session { @@ -10,8 +10,8 @@ declare module 'next-auth' { } declare module 'next-auth/jwt'{ - interface JWT { + interface JWT extends DefaultJWT { accessToken?: string profile: Profile } -} \ No newline at end of file +} diff --git a/src/pkg/github/api/delete.ts b/src/pkg/github/api/delete.ts new file mode 100644 index 0000000..9229a1a --- /dev/null +++ b/src/pkg/github/api/delete.ts @@ -0,0 +1,20 @@ +import { createOctokit } from '../octokit'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function deleteRepo(req: NextRequest){ + try{ + const octokit = await createOctokit(req) + const params = await req.json() + + const [owner,name] = params.full_name.split('/') + const response = await octokit.repos.delete({ + owner, + repo: name + }) + return NextResponse.json(response, {status: response.status}) + }catch(e: any){ + return NextResponse.json({error: e}, { + status: e.status, + }) + } +} diff --git a/src/pkg/github/api/patch.ts b/src/pkg/github/api/patch.ts new file mode 100644 index 0000000..2f4d518 --- /dev/null +++ b/src/pkg/github/api/patch.ts @@ -0,0 +1,16 @@ +import { createOctokit } from '../octokit'; +import { NextRequest, NextResponse } from 'next/server'; +import { GitHubPatchRepoParams } from '../types'; + +export async function patch(req: NextRequest){ + try{ + const octokit = await createOctokit(req) + const params: GitHubPatchRepoParams = await req.json() + const response = await octokit.repos.update(params) + return NextResponse.json(response, {status: response.status}) + }catch(e: any){ + return NextResponse.json({error: e}, { + status: e.status, + }) + } +} diff --git a/src/pkg/github/api/search.spec.ts b/src/pkg/github/api/search.spec.ts index b38c136..47e66a0 100644 --- a/src/pkg/github/api/search.spec.ts +++ b/src/pkg/github/api/search.spec.ts @@ -1,9 +1,7 @@ import { createMocks } from 'node-mocks-http' import { describe, it, expect, vi } from 'vitest' -import nock from 'nock' import search from './search' -import exp from 'constants' -import { GitHubSearchParams, GitHubSearchResponse } from '../types' +import { GitHubSearchParams } from '../types' import { NextResponse } from 'next/server' import fetchMock from 'fetch-mock' diff --git a/src/pkg/github/components/Repositories.tsx b/src/pkg/github/components/Repositories.tsx index e80aaac..9687252 100644 --- a/src/pkg/github/components/Repositories.tsx +++ b/src/pkg/github/components/Repositories.tsx @@ -1,28 +1,103 @@ 'use client' -import Loading from '@/pkg/ui/components/Loading' import { GitHubSearchItems } from '../types' -import { MdDelete, MdLock, MdPublic } from 'react-icons/md' +import { MdDelete, MdLock, MdPublic, MdSettings } from 'react-icons/md' import classNames from 'classnames' +import { useRouter } from 'next/navigation' +import { useEffect, useState } from 'react' +import deleteRepo from '../hooks/delete' +import { useQueryClient } from '@tanstack/react-query' +import { useToastContext } from '@/pkg/ui/contexts/ToastContext' +import dayjs from 'dayjs' +import { useThemeContext } from '@/pkg/ui/contexts/ThemeContext' interface Props { repositories: GitHubSearchItems, loading: boolean } -export default function Repositories({repositories, loading}: Props) { - if(loading){ - return () +const DeleteConfirm = ({ + repoToDelete, + handleConfirm +}:{ + repoToDelete: string, + handleConfirm: (confirmed: boolean) => void +}) => { + return ( + +
+
+

Delete {repoToDelete}

+
+
+

+ Are you sure want to delete {repoToDelete} repository? +

+
+
+
+ {/* if there is a button in form, it will close the modal */} + + +
+
+
+
+ ) +} + +export default function Repositories({repositories}: Props) { + const { setLoading: themeLoading } = useThemeContext() + const [repoToDelete, setRepoToDelete] = useState('') + const [loading, setLoading] = useState(false) + const qc = useQueryClient() + const { success } = useToastContext() + + useEffect(() => { + themeLoading(loading) + }, [loading, themeLoading]) + + const confirmDelete = (repo: string) => { + setRepoToDelete(repo) + const el: any = document.getElementById('delete_confirm') + el?.showModal() + } + + const handleDeleteConfirmed = async (confirmed: boolean) => { + if(confirmed){ + setLoading(true) + await deleteRepo(repoToDelete) + qc.invalidateQueries({ + queryKey: ['gitHubSearch'] + }) + success('Repository '+repoToDelete+' deleted!') + setLoading(false) + } + setRepoToDelete('') } return ( -
- {repositories.map((repo,index) => -
-
-
-
- {repo.name} + <> +
+ {repositories.map((repo,index) => +
+ {/* header */} +
+
+ + {/* description */} +
+

{repo.description}

+
+ + forks {repo.forks_count} + + + updated {dayjs(repo.pushed_at).format('DD-MM-YYYY')} + +
+
+ + {/* action bar */}
- +
+ + + +
+
+ +
-
- )} -
+ )} +
+ + ) } diff --git a/src/pkg/github/components/Search.tsx b/src/pkg/github/components/Search.tsx index cd6b5ef..c202dec 100644 --- a/src/pkg/github/components/Search.tsx +++ b/src/pkg/github/components/Search.tsx @@ -1,15 +1,16 @@ 'use client' import { useSearchContext } from '../contexts/SearchContext'; -import { GitHubSearchItems } from '../types'; import Repositories from './Repositories'; import Toolbar from './Toolbar'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useSearchRepos } from '../hooks/search'; +import { useThemeContext } from '@/pkg/ui/contexts/ThemeContext'; export default function Search() { + const { setLoading } = useThemeContext() const { setTotal, repositories, setRepositories } = useSearchContext() - const { response, isLoading } = useSearchRepos() + const { data: response, isLoading, isError, error, isFetching } = useSearchRepos() useEffect(() => { if(response){ @@ -18,6 +19,18 @@ export default function Search() { } }, [setRepositories, setTotal, response]) + useEffect(() => { + setLoading(isFetching) + }, [isFetching, setLoading]) + + if(isError){ + return ( +
+ {error.message} +
+ ) + } + return (
diff --git a/src/pkg/github/components/Toolbar.tsx b/src/pkg/github/components/Toolbar.tsx index 9590587..861518a 100644 --- a/src/pkg/github/components/Toolbar.tsx +++ b/src/pkg/github/components/Toolbar.tsx @@ -2,33 +2,23 @@ import { ChangeEvent, KeyboardEvent, useEffect, useState } from 'react' import { useSearchContext } from '../contexts/SearchContext' -import { useQueryClient } from '@tanstack/react-query' -import { MdNavigateBefore, MdFirstPage, MdNavigateNext, MdLastPage, MdSearch} from 'react-icons/md' +import { MdSearch} from 'react-icons/md' import { useGitHubContext } from '../contexts/GitHubContext' +import Pagination from '@/pkg/ui/components/Pagination' interface Props { loading: boolean } export default function Toolbar({loading}: Props) { - const { total, page, setPage, per_page, setQ } = useSearchContext() + const { total, page, setPage, per_page, setKeyword } = useSearchContext() const { profile } = useGitHubContext() const [numPage, setNumPage] = useState(1) const [keyword, setKeyWord] = useState('') - const client = useQueryClient() - useEffect(() => { setNumPage(Math.ceil(total/per_page)) - }, [total, setNumPage, per_page]) - - const moveNext = () => { - setPage(page + 1) - } - - const movePrevious = () => { - setPage(page-1) - } + }, [total, setNumPage, per_page, numPage, page]) const handleKeyDown = (e: KeyboardEvent) => { if(e.key === 'Enter'){ @@ -37,25 +27,21 @@ export default function Toolbar({loading}: Props) { } const search = () => { - const queries = [] - if(keyword.length > 0){ - queries.push(keyword) - } - queries.push('user:'+profile.login) - setQ(queries.join(' ')) + setKeyword(keyword) setPage(1) } const handleKeyWordChange = (e: ChangeEvent) => { const value = e.target.value setKeyWord(value) - if(value.length === 0){ - search() - } + } + + const onPageChanged = (page: number) => { + setPage(page) } return ( -
+
-
- - -
+
) } diff --git a/src/pkg/github/contexts/GitHubContext.tsx b/src/pkg/github/contexts/GitHubContext.tsx index 37f5120..342c76f 100644 --- a/src/pkg/github/contexts/GitHubContext.tsx +++ b/src/pkg/github/contexts/GitHubContext.tsx @@ -1,19 +1,25 @@ 'use client' -import { PropsWithChildren, createContext, useContext, useState } from 'react' -import { GitHubContextProps, GitHubSearchParams, GitHubUser } from '../types' +import { PropsWithChildren, createContext, useContext, useEffect } from 'react' +import { GitHubContextProps, GitHubUser } from '../types' import { useSession } from 'next-auth/react' +import { useThemeContext } from '@/pkg/ui/contexts/ThemeContext' import Loading from '@/pkg/ui/components/Loading' const GitHubContext = createContext(undefined) export default function GitHubProvider({children}:PropsWithChildren>) { + const { setLoading } = useThemeContext() const { data: session, status} = useSession() const profile = session?.profile as GitHubUser - if(status == 'loading'){ - return () - } + useEffect(() => { + if(status=='loading'){ + setLoading(true) + }else{ + setLoading(false) + } + }, [status, setLoading]) return ( (undefined) export default function SearchProvider({children}: PropsWithChildren) { + const test = typeof localStorage !== 'undefined' ? localStorage.getItem('gitHubSearchContext'):undefined + let existing = undefined + if(test){ + existing = JSON.parse(test) + } + const { profile } = useGitHubContext() - const [q, setQ] = useState('user:'+profile.login) - const [sort, setSort] = useState('name') - const [order, setOrder] = useState('asc') - const [per_page, setPerPage] = useState(10) - const [page, setPage] = useState(1) + + const [sort, setSort] = useState(existing? existing.sort : 'name') + const [order, setOrder] = useState(existing ? existing.order : 'asc') + const [per_page, setPerPage] = useState(existing ? existing.per_page : 6) + const [page, setPage] = useState(existing ? existing.page : 1) + const [keyword, setKeyword] = useState(existing ? existing.keyword : '') + const [owner, setOwner] = useState(existing ? existing.owner : 'user:'+profile?.login) const [total, setTotal] = useState(0) const [repositories, setRepositories] = useState([]) + const [initialized, setInitialized] = useState(false) + const getSearchParams = (): GitHubSearchParams => { + const keywords = [] + if(keyword != ''){ + keywords.push(keyword) + } + keywords.push(owner) const params = { - q, + q: keywords.join(' '), per_page, page } @@ -27,15 +42,27 @@ export default function SearchProvider({children}: PropsWithChildren) { return params } + + useEffect(() => { + if(typeof localStorage !== 'undefined'){ + localStorage.setItem('gitHubSearchContext', JSON.stringify({ + keyword, + owner, + sort, + order, + per_page, + page + })) + } + }, [keyword, owner,sort,order, per_page,page]) + return ( {children} diff --git a/src/pkg/github/hooks/delete.ts b/src/pkg/github/hooks/delete.ts new file mode 100644 index 0000000..ec06f45 --- /dev/null +++ b/src/pkg/github/hooks/delete.ts @@ -0,0 +1,12 @@ +import { useMutation } from '@tanstack/react-query' +import { GitHubDeleteRepoResponse } from '../types' + +export default async function deleteRepo(repoFullName: string): Promise{ + const response = await fetch('/api/github', { + method: 'DELETE', + body: JSON.stringify({ + full_name: repoFullName + }) + }) + return await response.json() +} diff --git a/src/pkg/github/hooks/search.ts b/src/pkg/github/hooks/search.ts index 12d5a3f..c93e4a2 100644 --- a/src/pkg/github/hooks/search.ts +++ b/src/pkg/github/hooks/search.ts @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query' export const useSearchRepos = () => { const { getSearchParams } = useSearchContext() const params = getSearchParams() - const { data: response, isLoading } = useQuery({ + const result = useQuery({ queryKey: ['gitHubSearch', { params }], queryFn: async({queryKey}: any) => { const [_key, { params }] = queryKey @@ -13,13 +13,7 @@ export const useSearchRepos = () => { body: JSON.stringify(params) }) return await response.json() - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, + } }) - return { - response, - isLoading, - } + return result } diff --git a/src/pkg/github/hooks/update.ts b/src/pkg/github/hooks/update.ts new file mode 100644 index 0000000..4ba1b44 --- /dev/null +++ b/src/pkg/github/hooks/update.ts @@ -0,0 +1,13 @@ +import { GitHubPatchRepoResponse } from '../types' + +export default async function update(owner: string, repo: string, payload: any): Promise{ + const response = await fetch('/api/github', { + method: 'PATCH', + body: JSON.stringify({ + owner, + repo, + ...payload + }) + }) + return await response.json() +} diff --git a/src/pkg/github/pages/RepoPage.tsx b/src/pkg/github/pages/RepoPage.tsx deleted file mode 100644 index 647bdcc..0000000 --- a/src/pkg/github/pages/RepoPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ - -export default function RepoPage() { - return ( -
- Repo Page -
- ) -} diff --git a/src/pkg/github/types.ts b/src/pkg/github/types.ts index cda24fe..4ea28e7 100644 --- a/src/pkg/github/types.ts +++ b/src/pkg/github/types.ts @@ -11,14 +11,15 @@ export type GitHubContextProps = { } export type GitHubSearchContextProps = { - q: string sort?: string order?: string per_page: number page: number total: number repositories: GitHubSearchItems - setQ: (q: string) => void + initialized: boolean + keyword: string + owner: string setSort: (sort: any) => void setOrder: (order: any) => void setPerPage: (perPage: number) => void @@ -26,6 +27,18 @@ export type GitHubSearchContextProps = { setTotal: (total: number) => void setRepositories: (items: GitHubSearchItems) => void getSearchParams: () => GitHubSearchParams + setInitialized: (initialized: boolean) => void + setKeyword: (keyword: string) => void + setOwner: (owner:string) => void } -export type GitHubRepo = GitHubSearchItems['at'] +export type GitHubGetRepoParams = Endpoints['GET /repos/{owner}/{repo}']['parameters'] +export type GitHubGetRepoResponse = Endpoints['GET /repos/{owner}/{repo}']['response'] +export type GitHubRepo = GitHubGetRepoResponse['data'] + +export type GitHubDeleteRepoResponse = Endpoints['DELETE /repos/{owner}/{repo}']['response'] + +export type GitHubPatchRepoEndpoints = Endpoints['PATCH /repos/{owner}/{repo}'] +export type GitHubPatchRepoParams = GitHubPatchRepoEndpoints['parameters'] +export type GitHubPatchRepoResponse = GitHubPatchRepoEndpoints['response'] + diff --git a/src/pkg/ui/components/Layout.tsx b/src/pkg/ui/components/Layout.tsx index 8d82e9f..b20a512 100644 --- a/src/pkg/ui/components/Layout.tsx +++ b/src/pkg/ui/components/Layout.tsx @@ -18,4 +18,4 @@ export default function Layout({children}: PropsWithChildren) { {children} ) -} \ No newline at end of file +} diff --git a/src/pkg/ui/components/LoadingOverlay.tsx b/src/pkg/ui/components/LoadingOverlay.tsx new file mode 100644 index 0000000..edd0fb5 --- /dev/null +++ b/src/pkg/ui/components/LoadingOverlay.tsx @@ -0,0 +1,26 @@ +import classNames from 'classnames' + +export default function LoadingOverlay({ + loading +}:{ + loading: boolean +}) { + return ( +
+
+
+
+
+
+
+
+ ) +} diff --git a/src/pkg/ui/components/Navbar.tsx b/src/pkg/ui/components/Navbar.tsx index 3b8a233..f30781f 100644 --- a/src/pkg/ui/components/Navbar.tsx +++ b/src/pkg/ui/components/Navbar.tsx @@ -4,16 +4,16 @@ import Link from 'next/link'; export default function Navbar() { return ( -
- logo + logo reflow
) -} \ No newline at end of file +} diff --git a/src/pkg/ui/components/Pagination.tsx b/src/pkg/ui/components/Pagination.tsx new file mode 100644 index 0000000..c3dc8e9 --- /dev/null +++ b/src/pkg/ui/components/Pagination.tsx @@ -0,0 +1,91 @@ +import classNames from 'classnames' +import { useEffect, useState } from 'react' +import { MdFirstPage, MdNavigateBefore, MdNavigateNext, MdLastPage } from 'react-icons/md' + +interface Props { + total: number + perPage: number + loading?: boolean + currentPage?: number + onPageChanged: (page: number) => void +} + +export default function Pagination({total, onPageChanged, perPage, loading, currentPage}: Props) { + const [page, setPage] = useState(1) + const [numPage, setNumPage] = useState(0) + const [pageNumbers, setPageNumbers] = useState([]) + + const buttonStyle = 'join-item btn btn-sm btn-square' + if(!loading){ + loading = false + } + + const goToPage = (page: number) => { + setPage(page) + onPageChanged(page) + } + + useEffect(() => { + setNumPage(Math.ceil(total/perPage)) + const pNums = [] + const start = page < 5 ? 1 : (Math.floor(page/5) * 5) + const end = start + 4 + for(let i=start; i<=end; i++){ + if(i <= numPage){ + pNums.push(i) + } + } + setPageNumbers(pNums as any) + }, [total, perPage, numPage, page, setPageNumbers]) + + useEffect(() => { + if(currentPage){ + setPage(currentPage) + } + }, [currentPage]) + + return ( +
+ + + {/* paged ranges */} + {pageNumbers.map((num, index) => + + )} + + +
+ ) +} diff --git a/src/pkg/ui/components/ToastContainer.tsx b/src/pkg/ui/components/ToastContainer.tsx new file mode 100644 index 0000000..d94a90c --- /dev/null +++ b/src/pkg/ui/components/ToastContainer.tsx @@ -0,0 +1,59 @@ +import classNames from 'classnames' +import { ToastType, Toast } from '../types' +import { useEffect, useRef, useState } from 'react' +import { useToastContext } from '../contexts/ToastContext' +import { MdClose } from 'react-icons/md' + +function ToastElement({toast}: {toast: Toast}) { + const { remove } = useToastContext() + const [timerId, setTimerId] = useState() + + useEffect(() => { + if(!timerId){ + const id = setTimeout(() => { + remove(toast) + }, 5000, toast) + setTimerId(id) + } + return () => clearTimeout(timerId) + }, [timerId, toast, remove]) + + return ( +
+ +
+

+

+
+ ) +} + +export default function ToastContainer({ + toasts +}:{ + toasts: Toast[] +}) { + return ( +
+ {toasts.map((toast) => )} +
+ ) +} diff --git a/src/pkg/ui/contexts/ReactQueryContext.tsx b/src/pkg/ui/contexts/ReactQueryContext.tsx index b2751ca..61c563e 100644 --- a/src/pkg/ui/contexts/ReactQueryContext.tsx +++ b/src/pkg/ui/contexts/ReactQueryContext.tsx @@ -5,7 +5,15 @@ import { QueryClientProvider, QueryClient } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' export default function ReactQueryProvider({children}: PropsWithChildren) { - const [client] = useState(() => new QueryClient()) + const [client] = useState(() => new QueryClient({ + defaultOptions: { + queries: { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + } + } + })) return ( diff --git a/src/pkg/ui/contexts/ThemeContext.tsx b/src/pkg/ui/contexts/ThemeContext.tsx new file mode 100644 index 0000000..c80d628 --- /dev/null +++ b/src/pkg/ui/contexts/ThemeContext.tsx @@ -0,0 +1,59 @@ +'use client' + +import { createContext, useContext, useReducer, useState } from 'react' +import { ThemeAction, ThemeActionType, ThemeContextProps, ThemeState, ToastState } from '../types' +import { PropsWithChildren } from 'react' +import ToastProvider from './ToastContext' +import Layout from '../components/Layout' +import AuthProvider from '@/pkg/auth/context/AuthContext' +import ReactQueryProvider from './ReactQueryContext' +import LoadingOverlay from '../components/LoadingOverlay' + +function themeReducer(state: ThemeState, action: ThemeAction){ + const {type, payload} = action + + switch(type){ + case ThemeActionType.Loading: + return { + ...state, + ...payload + } + default: + throw new Error('Unknown theme action type: '+type) + } +} + +const ThemeContext = createContext(undefined) +export default function ThemeProvider({children}: PropsWithChildren) { + const [loading, setLoading] = useState(false) + return ( + + + + + + + {children} + + + + + + ) +} + +export function useThemeContext(): ThemeContextProps { + const context = useContext(ThemeContext) as ThemeContextProps | undefined + + if(!context){ + throw Error( + 'useThemeContext should be used within ThemeProvider' + ) + } + + return context +} + diff --git a/src/pkg/ui/contexts/ToastContext.tsx b/src/pkg/ui/contexts/ToastContext.tsx new file mode 100644 index 0000000..946957d --- /dev/null +++ b/src/pkg/ui/contexts/ToastContext.tsx @@ -0,0 +1,80 @@ +'use client' + +import { PropsWithChildren, createContext, useContext, useReducer } from 'react'; +import { Toast, ToastAction, ToastActionType, ToastContextProps, ToastState, ToastType } from '../types'; +import ToastContainer from '../components/ToastContainer'; + +function toastReducer(state: ToastState, action: ToastAction){ + const { type, payload } = action + + switch(type){ + case ToastActionType.ADD: + return { + ...state, + toasts: [...state.toasts, payload] + } + case ToastActionType.REMOVE: + const updated = state.toasts.filter( + (toast) => { + return toast.id !== payload.id + } + ) + return { + ...state, + toasts: updated + } + default: + throw new Error('Unhandled action type: ' + type) + } +} + +const ToastContext = createContext(undefined) + +export default function ToastProvider({children}: PropsWithChildren) { + const [state, dispatch] = useReducer(toastReducer, { toasts: []}) + + const addToast = (type: ToastType, message: string) => { + const id = Math.floor(Math.random() * 10000000) + dispatch({type: ToastActionType.ADD, payload: {id, type, message}}) + } + + const remove = (toast: Toast) => { + dispatch({type: ToastActionType.REMOVE, payload: toast }) + } + + const success = (message: string) => { + addToast(ToastType.success, message) + } + + const info = (message: string) => { + addToast(ToastType.info, message) + } + + const error = (message: string) => { + addToast(ToastType.error, message) + } + + const warning = (message: string) => { + addToast(ToastType.warning, message) + } + + const value = { success, info, error, warning, remove} + return ( + + + {children} + + ) +} + +export function useToastContext(): ToastContextProps { + const context = useContext(ToastContext) as ToastContextProps | undefined + + if(!context){ + throw Error( + 'useSearchContext should be used within SearchProvider' + ) + } + + return context +} diff --git a/src/pkg/ui/reducers/toast.ts b/src/pkg/ui/reducers/toast.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pkg/ui/types.ts b/src/pkg/ui/types.ts index dda1471..9aa07f3 100644 --- a/src/pkg/ui/types.ts +++ b/src/pkg/ui/types.ts @@ -7,3 +7,56 @@ export enum ThemeLayout { export type UI = { layout: ThemeLayout } + +export enum ToastType { + success = 'success', + info = 'info', + error = 'error', + warning = 'warning' +} + +export type Toast = { + id: number + message: string + type: ToastType +} + +export enum ToastActionType { + ADD = 'ADD_TOAST', + REMOVE = 'DELETE_TOAST' +} + +export type ToastAction = { + type: ToastActionType, + payload: Toast +} + +export type ToastState = { + toasts: Toast[] +} + +export type ToastContextProps = { + success: (message: string) => void + info: (message: string) => void + error: (message: string) => void + warning: (message: string) => void + remove: (toast: Toast) => void +} + +export type ThemeContextProps = { + loading: boolean + setLoading: (loading: boolean) => void +} + +export enum ThemeActionType { + Loading = 'loading' +} + +export type ThemeAction = { + type: ThemeActionType + payload: object +} + +export type ThemeState = { + loading: boolean +}