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);