From e8ed08d49bfd6bc21729393dbbde5f3210c40c7e Mon Sep 17 00:00:00 2001 From: Khairul Hidayat <me@khairul.my.id> Date: Thu, 8 Aug 2024 14:46:28 +0700 Subject: [PATCH] feat: add view leaderboard item --- package.json | 1 + pnpm-lock.yaml | 434 ++++++++++++++++++ ...low_catseye.sql => 0000_medical_magma.sql} | 1 + server/db/migrations/meta/0000_snapshot.json | 9 +- server/db/migrations/meta/_journal.json | 4 +- server/models/repositories.ts | 2 +- src/components/containers/rank-board.tsx | 4 +- src/components/containers/rank-list-item.tsx | 3 + src/components/layouts/main-layout.tsx | 2 +- src/components/ui/bottom-sheet.tsx | 51 ++ src/hooks/useHashUrl.ts | 21 + src/pages/home/page.tsx | 7 + src/pages/home/view-sheet.tsx | 139 ++++++ 13 files changed, 672 insertions(+), 6 deletions(-) rename server/db/migrations/{0000_low_catseye.sql => 0000_medical_magma.sql} (98%) create mode 100644 src/components/ui/bottom-sheet.tsx create mode 100644 src/hooks/useHashUrl.ts create mode 100644 src/pages/home/view-sheet.tsx diff --git a/package.json b/package.json index 7c1bce4..2fe16af 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-icons": "^5.2.1", "react-lottie": "^1.2.4", "tailwind-merge": "^2.4.0", + "vaul": "^0.9.1", "zustand": "^4.5.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2465a29..78dd5a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: tailwind-merge: specifier: ^2.4.0 version: 2.4.0 + vaul: + specifier: ^0.9.1 + version: 0.9.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zustand: specifier: ^4.5.4 version: 4.5.4(@types/react@18.3.3)(react@18.3.1) @@ -651,6 +654,168 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.1': + resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.0': + resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.0': + resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-portal@1.1.1': + resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.0': + resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rollup/rollup-android-arm-eabi@4.20.0': resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} cpu: [arm] @@ -942,6 +1107,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -1151,6 +1320,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1450,6 +1622,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-tsconfig@4.7.6: resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} @@ -1526,6 +1702,9 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.4.1: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} @@ -1910,6 +2089,36 @@ packages: peerDependencies: react: '>=15.0.0' + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.7: + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -2169,6 +2378,26 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -2181,6 +2410,12 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + vaul@0.9.1: + resolution: {integrity: sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + vite@5.3.5: resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2587,6 +2822,141 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@rollup/rollup-android-arm-eabi@4.20.0': optional: true @@ -2844,6 +3214,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.6.3 + array-union@2.1.0: {} atomic-sleep@1.0.0: {} @@ -3079,6 +3453,8 @@ snapshots: detect-libc@2.0.3: {} + detect-node-es@1.1.0: {} + didyoumean@1.2.2: {} dir-glob@3.0.1: @@ -3376,6 +3752,8 @@ snapshots: get-caller-file@2.0.5: {} + get-nonce@1.0.1: {} + get-tsconfig@4.7.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -3460,6 +3838,10 @@ snapshots: ini@1.3.8: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ioredis@5.4.1: dependencies: '@ioredis/commands': 1.2.0 @@ -3839,6 +4221,34 @@ snapshots: lottie-web: 5.12.2 react: 18.3.1 + react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.3 + optionalDependencies: + '@types/react': 18.3.3 + + react-remove-scroll@2.5.7(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.3 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + + react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.6.3 + optionalDependencies: + '@types/react': 18.3.3 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -4120,6 +4530,21 @@ snapshots: dependencies: punycode: 2.3.1 + use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.6.3 + optionalDependencies: + '@types/react': 18.3.3 + + use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.6.3 + optionalDependencies: + '@types/react': 18.3.3 + use-sync-external-store@1.2.0(react@18.3.1): dependencies: react: 18.3.1 @@ -4128,6 +4553,15 @@ snapshots: uuid@9.0.1: {} + vaul@0.9.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vite@5.3.5(@types/node@20.12.14): dependencies: esbuild: 0.21.5 diff --git a/server/db/migrations/0000_low_catseye.sql b/server/db/migrations/0000_medical_magma.sql similarity index 98% rename from server/db/migrations/0000_low_catseye.sql rename to server/db/migrations/0000_medical_magma.sql index b8a255a..215bc6d 100644 --- a/server/db/migrations/0000_low_catseye.sql +++ b/server/db/migrations/0000_medical_magma.sql @@ -5,6 +5,7 @@ CREATE TABLE `repositories` ( `uri` text NOT NULL, `language` text NOT NULL, `stars` integer NOT NULL, + `forks` integer NOT NULL, `last_update` text NOT NULL, `languages` text, `contributors` text, diff --git a/server/db/migrations/meta/0000_snapshot.json b/server/db/migrations/meta/0000_snapshot.json index 37e4dba..d797d7a 100644 --- a/server/db/migrations/meta/0000_snapshot.json +++ b/server/db/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "ccf7929b-8198-4452-ad60-e02285ac8149", + "id": "891041fb-3c81-46b0-ac5a-cd85a640fe8c", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "repositories": { @@ -49,6 +49,13 @@ "notNull": true, "autoincrement": false }, + "forks": { + "name": "forks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "last_update": { "name": "last_update", "type": "text", diff --git a/server/db/migrations/meta/_journal.json b/server/db/migrations/meta/_journal.json index b0fee30..7da335e 100644 --- a/server/db/migrations/meta/_journal.json +++ b/server/db/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1723071912146, - "tag": "0000_low_catseye", + "when": 1723077872529, + "tag": "0000_medical_magma", "breakpoints": true } ] diff --git a/server/models/repositories.ts b/server/models/repositories.ts index 8a168df..a5fe568 100644 --- a/server/models/repositories.ts +++ b/server/models/repositories.ts @@ -15,7 +15,7 @@ export const repositories = sqliteTable( uri: text("uri").notNull(), language: text("language").notNull(), stars: integer("stars").notNull(), - forks: integer("stars").notNull(), + forks: integer("forks").notNull(), lastUpdate: text("last_update").notNull(), languages: text("languages", { mode: "json" }).$type<Language[]>(), contributors: text("contributors", { mode: "json" }).$type<Contributor[]>(), diff --git a/src/components/containers/rank-board.tsx b/src/components/containers/rank-board.tsx index a37ea42..259ebad 100644 --- a/src/components/containers/rank-board.tsx +++ b/src/components/containers/rank-board.tsx @@ -9,15 +9,17 @@ type RankBoardProps = { avatar: string; points: number; rank: number; + onClick?: () => void; }; -const RankBoard = ({ name, avatar, points, rank }: RankBoardProps) => { +const RankBoard = ({ name, avatar, points, rank, onClick }: RankBoardProps) => { return ( <button type="button" className={cn( "flex flex-col items-center rounded-lg py-4 hover:bg-neutral/70 active:scale-x-105 transition-all relative" )} + onClick={onClick} > {rank === 1 ? ( <> diff --git a/src/components/containers/rank-list-item.tsx b/src/components/containers/rank-list-item.tsx index 2ba6dd1..19a5be3 100644 --- a/src/components/containers/rank-list-item.tsx +++ b/src/components/containers/rank-list-item.tsx @@ -7,6 +7,7 @@ type RankListItemProps = { points: number; rank: number; className?: string; + onClick?: () => void; }; const RankListItem = ({ @@ -15,6 +16,7 @@ const RankListItem = ({ points, rank, className, + onClick, }: RankListItemProps) => { return ( <button @@ -24,6 +26,7 @@ const RankListItem = ({ rank % 2 === 0 && "bg-base-100/50", className )} + onClick={onClick} > <div className="w-10"> <Badge diff --git a/src/components/layouts/main-layout.tsx b/src/components/layouts/main-layout.tsx index 7f41368..e10285c 100644 --- a/src/components/layouts/main-layout.tsx +++ b/src/components/layouts/main-layout.tsx @@ -8,7 +8,7 @@ const MainLayout = ({ children }: PropsWithChildren) => { <footer className="py-8 bg-base-300 text-center rounded-t-xl rounded-b-0 p-4 md:rounded-b-lg md:rounded-t-lg mt-8 shadow-lg"> <a - href="/" + href="https://github.com/khairul169/github-leaderboard" target="_blank" className="inline-flex flex-row items-center justify-center gap-2 hover:underline" > diff --git a/src/components/ui/bottom-sheet.tsx b/src/components/ui/bottom-sheet.tsx new file mode 100644 index 0000000..72d42f3 --- /dev/null +++ b/src/components/ui/bottom-sheet.tsx @@ -0,0 +1,51 @@ +import { cn } from "@client/lib/utils"; +import { ComponentPropsWithoutRef } from "react"; +import { Drawer } from "vaul"; + +type BottomSheetProps = ComponentPropsWithoutRef<typeof Drawer.Root> & { + trigger?: React.ReactNode; + children?: React.ReactNode; + className?: string; +}; + +const BottomSheet = ({ + trigger, + children, + className, + ...props +}: BottomSheetProps) => { + return ( + <Drawer.Root {...props}> + {trigger ? <Drawer.Trigger asChild>{trigger}</Drawer.Trigger> : null} + + <Drawer.Portal> + <Drawer.Overlay className="fixed inset-0 bg-black/40 z-[9]" /> + <Drawer.Content + className={cn( + "flex flex-col bg-base-100 rounded-t-2xl mt-24 h-[60%] max-h-[96%] fixed z-10 bottom-0 left-0 right-0 md:mx-auto md:max-w-3xl focus:outline-none", + className + )} + > + <Drawer.Handle className="bg-neutral my-2" /> + {children} + </Drawer.Content> + </Drawer.Portal> + </Drawer.Root> + ); +}; + +export const BottomSheetTitle = ({ + className, + ...props +}: ComponentPropsWithoutRef<typeof Drawer.Title>) => { + return <Drawer.Title className={cn("text-2xl", className)} {...props} />; +}; + +export const BottomSheetDescription = ({ + className, + ...props +}: ComponentPropsWithoutRef<typeof Drawer.Description>) => { + return <Drawer.Description className={className} {...props} />; +}; + +export default BottomSheet; diff --git a/src/hooks/useHashUrl.ts b/src/hooks/useHashUrl.ts new file mode 100644 index 0000000..d140bdb --- /dev/null +++ b/src/hooks/useHashUrl.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; + +export const setHashUrl = (value: string) => { + history.replaceState(undefined, undefined as never, "#" + value); + window.dispatchEvent(new HashChangeEvent("hashchange")); +}; + +export const useHashUrl = () => { + const [value, setValue] = useState(location.hash.substring(1)); + + useEffect(() => { + const onHashChange = () => { + setValue(location.hash.substring(1)); + }; + + window.addEventListener("hashchange", onHashChange); + return () => window.removeEventListener("hashchange", onHashChange); + }, []); + + return value; +}; diff --git a/src/pages/home/page.tsx b/src/pages/home/page.tsx index 7f5d8a1..c2ef309 100644 --- a/src/pages/home/page.tsx +++ b/src/pages/home/page.tsx @@ -13,6 +13,8 @@ import { import { useMemo } from "react"; import { dummyAvatar } from "@client/lib/utils"; import { onLogin, useAuth } from "@client/hooks/useAuth"; +import ViewSheet from "./view-sheet"; +import { setHashUrl } from "@client/hooks/useHashUrl"; const HomePage = () => { const { user } = useAuth(); @@ -49,6 +51,7 @@ const HomePage = () => { name={item.name} avatar={item.avatar || dummyAvatar(item.rank)} points={item.points} + onClick={() => setHashUrl(item.username)} /> ); })} @@ -65,6 +68,7 @@ const HomePage = () => { name={item.name} avatar={item.avatar || dummyAvatar(item.rank)} points={item.points} + onClick={() => setHashUrl(item.username)} /> ))} @@ -77,6 +81,7 @@ const HomePage = () => { avatar={userRank.user.avatar || dummyAvatar(userRank.user.rank)} points={userRank.user.points} className="sticky z-[2] bottom-0 bg-base-100" + onClick={() => setHashUrl(userRank.user.username)} /> </> ) : ( @@ -96,6 +101,8 @@ const HomePage = () => { </div> )} </section> + + <ViewSheet /> </div> ); }; diff --git a/src/pages/home/view-sheet.tsx b/src/pages/home/view-sheet.tsx new file mode 100644 index 0000000..c3f996c --- /dev/null +++ b/src/pages/home/view-sheet.tsx @@ -0,0 +1,139 @@ +import BottomSheet, { + BottomSheetDescription, + BottomSheetTitle, +} from "@client/components/ui/bottom-sheet"; +import { setHashUrl, useHashUrl } from "@client/hooks/useHashUrl"; +import { memo, useMemo } from "react"; +import { Avatar, Badge } from "react-daisyui"; +import { useGetUserLeaderboard } from "./hooks"; +import { dummyAvatar } from "@client/lib/utils"; +import { FiType, FiUsers } from "react-icons/fi"; +import { FaCode, FaRegStar, FaTrophy } from "react-icons/fa"; +import { LuFolderGit } from "react-icons/lu"; +import { IoMdGitBranch, IoMdGitCommit } from "react-icons/io"; + +const ViewSheet = () => { + const username = useHashUrl(); + const { data } = useGetUserLeaderboard(username); + + const summary = useMemo(() => { + if (!data) { + return []; + } + + return [ + { + icon: LuFolderGit, + name: "Personal repo", + value: data.repositories.length, + }, + { + icon: FaRegStar, + name: "Stars", + value: data.repositories.reduce((acc, repo) => acc + repo.stars, 0), + }, + { + icon: IoMdGitBranch, + name: "Forks", + value: data.repositories.reduce((acc, repo) => acc + repo.forks, 0), + }, + { + icon: IoMdGitCommit, + name: "Commits", + value: data.user.commits, + }, + { + icon: FiType, + name: "Line of codes", + value: data.user.lineOfCodes, + }, + { + icon: FaCode, + name: "Languages", + value: data.languages.length, + }, + ]; + }, [data]); + + return ( + <BottomSheet + open={!!username} + onOpenChange={(open) => { + if (!open) setHashUrl(""); + }} + className="h-[90%]" + > + <div className="p-4 md:p-8 flex-1 overflow-y-auto"> + <div className="text-center flex flex-col md:flex-row md:text-left gap-x-8 gap-y-4 items-center"> + <Avatar + shape="circle" + size="md" + src={data?.user.avatar || dummyAvatar(data?.user.id)} + /> + + <div className="md:flex-1"> + <BottomSheetTitle>{data?.user.name}</BottomSheetTitle> + <BottomSheetDescription> + {"@" + (data?.user.username || "")} + </BottomSheetDescription> + + <p className="text-sm mt-4"> + <FiUsers className="inline" />{" "} + {data?.user?.followers + " followers"} + {" • "} + {data?.user?.following + " following"} + </p> + </div> + + <div className="bg-neutral text-neutral-content px-6 py-3 w-full md:w-auto rounded-lg"> + <div className="flex flex-row items-center justify-center font-mono gap-2 text-4xl md:text-3xl text-primary"> + <FaTrophy size={24} /> + <p>{data?.user.rank}</p> + </div> + <p className="text-xs">{data?.user.points + " pts"}</p> + </div> + </div> + + {data?.languages && data.languages.length > 0 && ( + <section id="languages" className="mt-4 md:mt-8"> + <div className="flex flex-row sm:flex-wrap overflow-x-auto gap-2 mt-2"> + {data?.languages.slice(0, 10).map((lang) => ( + <div + key={lang.name} + className="bg-base-300 shrink-0 rounded-xl px-3 py-2 text-xs inline-flex gap-2" + > + <p>{lang.name}</p> + <Badge color="primary" size="sm" className="px-1"> + {lang.percent.toFixed(1) + "%"} + </Badge> + </div> + ))} + </div> + </section> + )} + + <section className="mt-8 grid grid-cols-2 md:grid-cols-3 gap-3"> + {summary.map((item, idx) => ( + <div + key={idx} + className="flex flex-row items-center gap-x-4 bg-base-300 rounded-xl px-4 py-3" + > + <item.icon size={24} className="shrink-0 md:ml-2" /> + <div className="flex-1 truncate"> + <p + className="text-2xl font-mono truncate" + title={String(item.value)} + > + {item.value} + </p> + <p className="text-xs truncate">{item.name}</p> + </div> + </div> + ))} + </section> + </div> + </BottomSheet> + ); +}; + +export default memo(ViewSheet);