mirror of
https://github.com/AnmolSaini16/mapcn
synced 2026-04-25 16:14:54 +02:00
feat: add blocks showcase and restructure app
This commit is contained in:
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -23,7 +23,6 @@
|
||||
<img src="public/banner.png" alt="mapcn banner" />
|
||||
</p>
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- 🎨 **Theme-aware** — Automatically adapts to light/dark mode
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
|
||||
435
package-lock.json
generated
435
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"recharts": "^2.15.4",
|
||||
"shiki": "^3.20.0",
|
||||
"tailwind-merge": "^2.6.0"
|
||||
},
|
||||
@@ -29,6 +30,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"shadcn": "^3.6.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
@@ -455,6 +457,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -5566,6 +5577,69 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -7222,9 +7296,129 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@@ -7314,6 +7508,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
|
||||
@@ -7496,6 +7696,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
@@ -8268,6 +8478,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
@@ -8405,6 +8621,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
@@ -9248,6 +9473,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
@@ -9912,7 +10146,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -10368,6 +10601,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -10422,7 +10661,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@@ -11049,7 +11287,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -11618,6 +11855,102 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz",
|
||||
"integrity": "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-hermes": "*",
|
||||
"@prettier/plugin-oxc": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"@zackad/prettier-plugin-twig": "*",
|
||||
"prettier": "^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-multiline-arrays": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
"prettier-plugin-svelte": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-hermes": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-oxc": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@zackad/prettier-plugin-twig": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-multiline-arrays": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
@@ -11662,7 +11995,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
@@ -11945,7 +12277,6 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
@@ -11995,6 +12326,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-equals": "^5.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -12017,6 +12363,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recast": {
|
||||
"version": "0.23.11",
|
||||
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
|
||||
@@ -12034,6 +12396,44 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
||||
"integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.4",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"victory-vendor": "^36.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts-scale": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
@@ -13179,7 +13579,6 @@
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
@@ -13848,6 +14247,28 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"recharts": "^2.15.4",
|
||||
"shiki": "^3.20.0",
|
||||
"tailwind-merge": "^2.6.0"
|
||||
},
|
||||
@@ -31,6 +32,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"shadcn": "^3.6.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
|
||||
49
public/r/analytics-map.json
Normal file
49
public/r/analytics-map.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "analytics-map",
|
||||
"title": "Analytics Map",
|
||||
"description": "Real-time analytics overview with a world map, breakdown cards, and device stats.",
|
||||
"dependencies": [
|
||||
"recharts",
|
||||
"lucide-react"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"map",
|
||||
"card",
|
||||
"chart"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/page.tsx",
|
||||
"content": "\"use client\";\n\nimport {\n Map,\n MapControls,\n MapMarker,\n MarkerContent,\n MarkerTooltip,\n} from \"@/registry/map\";\nimport { OverviewCard } from \"./components/overview-card\";\nimport { BreakdownCard } from \"./components/breakdown-card\";\nimport {\n locations,\n visitedPagesRows,\n countriesRows,\n referrersRows,\n browsersRows,\n} from \"./data\";\n\nconst MAP_HEIGHT = \"38rem\";\n\nexport function AnalyticsMapBlock() {\n return (\n <div\n className=\"bg-background relative min-h-screen\"\n style={{ \"--map-height\": MAP_HEIGHT } as React.CSSProperties}\n >\n <div className=\"relative h-(--map-height)\">\n <Map\n center={[-2, 16]}\n zoom={1.5}\n scrollZoom={false}\n renderWorldCopies={true}\n >\n <MapControls showFullscreen />\n {locations.map((location) => (\n <MapMarker\n key={location.city}\n longitude={location.lng}\n latitude={location.lat}\n >\n <MarkerContent>\n <div\n className=\"rounded-full bg-blue-500/70\"\n style={{\n width: location.size * 3,\n height: location.size * 3,\n }}\n />\n </MarkerContent>\n <MarkerTooltip\n offset={20}\n className=\"bg-background text-foreground border\"\n >\n <p className=\"text-muted-foreground font-medium\">\n {location.city}\n </p>\n <p className=\"mt-1\">\n <span className=\"font-medium tabular-nums\">\n {location.size}\n </span>{\" \"}\n active users\n </p>\n </MarkerTooltip>\n </MapMarker>\n ))}\n </Map>\n <div\n className=\"via-background/30 to-background pointer-events-none absolute inset-x-0 bottom-0 h-40 bg-linear-to-b from-transparent\"\n aria-hidden\n />\n <OverviewCard />\n </div>\n\n <div className=\"grid gap-4 p-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <BreakdownCard title=\"Visited pages\" rows={visitedPagesRows} />\n <BreakdownCard title=\"Referrers\" rows={referrersRows} />\n <BreakdownCard title=\"Countries\" rows={countriesRows} />\n <BreakdownCard title=\"Browsers\" rows={browsersRows} />\n </div>\n </div>\n );\n}\n",
|
||||
"type": "registry:page",
|
||||
"target": "app/analytics/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/data.ts",
|
||||
"content": "import { type ChartConfig } from \"@/components/ui/chart\";\n\nexport interface LocationPoint {\n city: string;\n lng: number;\n lat: number;\n size: number;\n}\n\nexport interface BreakdownRow {\n label: string;\n value: number;\n}\n\nexport const locations: LocationPoint[] = [\n { city: \"San Francisco\", lng: -122.4194, lat: 37.7749, size: 16 },\n { city: \"New York\", lng: -74.006, lat: 40.7128, size: 15 },\n { city: \"Toronto\", lng: -79.3832, lat: 43.6532, size: 11 },\n { city: \"Mexico City\", lng: -99.1332, lat: 19.4326, size: 10 },\n { city: \"Sao Paulo\", lng: -46.6333, lat: -23.5505, size: 12 },\n { city: \"Buenos Aires\", lng: -58.3816, lat: -34.6037, size: 9 },\n { city: \"London\", lng: -0.1276, lat: 51.5074, size: 14 },\n { city: \"Berlin\", lng: 13.405, lat: 52.52, size: 11 },\n { city: \"Paris\", lng: 2.3522, lat: 48.8566, size: 13 },\n { city: \"Madrid\", lng: -3.7038, lat: 40.4168, size: 10 },\n { city: \"Cairo\", lng: 31.2357, lat: 30.0444, size: 9 },\n { city: \"Lagos\", lng: 3.3792, lat: 6.5244, size: 10 },\n { city: \"Mumbai\", lng: 72.8777, lat: 19.076, size: 13 },\n { city: \"Dubai\", lng: 55.2708, lat: 25.2048, size: 11 },\n { city: \"Seoul\", lng: 126.978, lat: 37.5665, size: 12 },\n { city: \"Singapore\", lng: 103.8198, lat: 1.3521, size: 10 },\n { city: \"Tokyo\", lng: 139.6917, lat: 35.6895, size: 12 },\n { city: \"Sydney\", lng: 151.2093, lat: -33.8688, size: 9 },\n { city: \"Auckland\", lng: 174.7633, lat: -36.8485, size: 8 },\n];\n\nexport const usersPerDay = [\n { day: \"Mon\", users: 320 },\n { day: \"Tue\", users: 410 },\n { day: \"Wed\", users: 560 },\n { day: \"Thu\", users: 640 },\n { day: \"Fri\", users: 780 },\n { day: \"Sat\", users: 690 },\n { day: \"Sun\", users: 720 },\n];\n\nexport const usersPerDayChartConfig = {\n users: {\n label: \"Users\",\n color: \"var(--color-blue-500)\",\n },\n} satisfies ChartConfig;\n\nexport const deviceCategoryData = [\n { name: \"Desktop\", value: 73.3, fill: \"var(--color-blue-500)\" },\n { name: \"Mobile\", value: 25.0, fill: \"var(--color-blue-400)\" },\n { name: \"Tablet\", value: 1.7, fill: \"var(--color-blue-300)\" },\n];\n\nexport const deviceCategoryChartConfig = {\n desktop: { label: \"Desktop\", color: \"var(--color-blue-500)\" },\n mobile: { label: \"Mobile\", color: \"var(--color-blue-400)\" },\n tablet: { label: \"Tablet\", color: \"var(--color-blue-300)\" },\n} satisfies ChartConfig;\n\nexport const visitedPagesRows: BreakdownRow[] = [\n { label: \"Home\", value: 31 },\n { label: \"Pricing\", value: 23 },\n { label: \"Docs / Basic Map\", value: 18 },\n { label: \"Installation\", value: 12 },\n { label: \"Components\", value: 9 },\n { label: \"Blog\", value: 6 },\n];\n\nexport const countriesRows: BreakdownRow[] = [\n { label: \"United States\", value: 27 },\n { label: \"India\", value: 14 },\n { label: \"United Kingdom\", value: 8 },\n { label: \"Germany\", value: 6 },\n { label: \"Japan\", value: 4 },\n { label: \"Australia\", value: 2 },\n];\n\nexport const referrersRows: BreakdownRow[] = [\n { label: \"google\", value: 38 },\n { label: \"direct\", value: 26 },\n { label: \"github.com\", value: 19 },\n { label: \"x.com\", value: 11 },\n { label: \"ui.shadcn.com\", value: 8 },\n { label: \"other\", value: 5 },\n];\n\nexport const browsersRows: BreakdownRow[] = [\n { label: \"Chrome\", value: 52 },\n { label: \"Safari\", value: 21 },\n { label: \"Firefox\", value: 14 },\n { label: \"Edge\", value: 8 },\n { label: \"Other\", value: 5 },\n];\n",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/data.ts"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/components/overview-card.tsx",
|
||||
"content": "\"use client\";\n\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\";\nimport { ChartContainer } from \"@/components/ui/chart\";\nimport { TrendingUp } from \"lucide-react\";\nimport { Area, AreaChart, Cell, Pie, PieChart } from \"recharts\";\nimport {\n deviceCategoryChartConfig,\n deviceCategoryData,\n usersPerDay,\n usersPerDayChartConfig,\n} from \"../data\";\n\nfunction MetricChart() {\n return (\n <ChartContainer\n config={usersPerDayChartConfig}\n className=\"aspect-auto h-8 w-full\"\n >\n <AreaChart data={usersPerDay} margin={{ left: 4, right: 4, top: 4 }}>\n <defs>\n <linearGradient id=\"usersGradient\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop\n offset=\"0%\"\n stopColor=\"var(--color-users)\"\n stopOpacity={0.4}\n />\n <stop\n offset=\"100%\"\n stopColor=\"var(--color-users)\"\n stopOpacity={0}\n />\n </linearGradient>\n </defs>\n\n <Area\n type=\"natural\"\n dataKey=\"users\"\n stroke=\"var(--color-users)\"\n strokeWidth={1.5}\n fill=\"url(#usersGradient)\"\n />\n </AreaChart>\n </ChartContainer>\n );\n}\n\nexport function OverviewCard() {\n return (\n <Card className=\"bg-card/70 absolute top-4 left-4 z-10 w-60 backdrop-blur-sm\">\n <CardHeader>\n <div>\n <p className=\"text-muted-foreground pb-2 text-[10px] tracking-wider uppercase\">\n Users in last 7 days\n </p>\n <p className=\"text-3xl leading-none font-semibold\">3,803</p>\n </div>\n </CardHeader>\n\n <CardContent>\n <MetricChart />\n <div className=\"mt-4 flex items-center gap-1.5 text-xs\">\n <TrendingUp className=\"size-3 text-emerald-500\" />\n <span className=\"font-medium text-emerald-500\">+12.5%</span>\n <span className=\"text-muted-foreground\">vs previous 7 days</span>\n </div>\n\n <div className=\"border-border/60 mt-4 border-t pt-4\">\n <p className=\"text-muted-foreground text-[10px] tracking-wider uppercase\">\n Device category in last 7 days\n </p>\n\n <ChartContainer\n config={deviceCategoryChartConfig}\n className=\"mx-auto mt-3 aspect-square h-32 w-32\"\n >\n <PieChart>\n <Pie\n data={deviceCategoryData}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={32}\n outerRadius={52}\n strokeWidth={2}\n >\n {deviceCategoryData.map((entry) => (\n <Cell key={entry.name} fill={entry.fill} />\n ))}\n </Pie>\n </PieChart>\n </ChartContainer>\n\n <div className=\"mt-3 grid grid-cols-3 gap-2\">\n {deviceCategoryData.map((device) => (\n <div key={device.name} className=\"text-center\">\n <p className=\"text-muted-foreground flex items-center justify-center gap-1.5 text-[10px] tracking-wide uppercase\">\n <span\n className=\"size-2 rounded-full\"\n style={{ backgroundColor: device.fill }}\n />\n {device.name}\n </p>\n <p className=\"text-foreground mt-1 leading-none font-medium tabular-nums\">\n {device.value}%\n </p>\n </div>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n );\n}\n",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/components/overview-card.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/components/breakdown-card.tsx",
|
||||
"content": "\"use client\";\n\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { type BreakdownRow } from \"../data\";\n\ninterface BreakdownCardProps {\n title: string;\n rows: BreakdownRow[];\n}\n\nexport function BreakdownCard({ title, rows }: BreakdownCardProps) {\n const maxRowValue =\n rows.length > 0 ? Math.max(...rows.map((row) => row.value)) : 0;\n\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-sm font-medium\">{title}</CardTitle>\n </CardHeader>\n\n <CardContent>\n <div className=\"text-muted-foreground mb-2 flex items-center justify-between text-[11px] tracking-wider uppercase\">\n <span>{title}</span>\n <span>Visitors</span>\n </div>\n <div className=\"space-y-3\">\n {rows.map((row) => (\n <div key={row.label} className=\"space-y-1.5\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"text-foreground/90 truncate\">{row.label}</span>\n <span className=\"text-foreground font-medium\">{row.value}</span>\n </div>\n <div className=\"bg-muted h-1 rounded-full\">\n <div\n className=\"h-full rounded-full bg-blue-500/85\"\n style={{ width: `${(row.value / maxRowValue) * 100}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n );\n}\n",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/components/breakdown-card.tsx"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"iframeHeight": "970px"
|
||||
},
|
||||
"categories": [
|
||||
"analytics",
|
||||
"dashboard"
|
||||
],
|
||||
"type": "registry:block"
|
||||
}
|
||||
31
public/r/delivery-tracker.json
Normal file
31
public/r/delivery-tracker.json
Normal file
File diff suppressed because one or more lines are too long
24
public/r/heatmap.json
Normal file
24
public/r/heatmap.json
Normal file
File diff suppressed because one or more lines are too long
55
public/r/logistics-network.json
Normal file
55
public/r/logistics-network.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -27,6 +27,114 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "analytics-map",
|
||||
"type": "registry:block",
|
||||
"title": "Analytics Map",
|
||||
"description": "Real-time analytics overview with a world map, breakdown cards, and device stats.",
|
||||
"dependencies": ["recharts", "lucide-react"],
|
||||
"registryDependencies": ["map", "card", "chart"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/analytics/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/data.ts",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/data.ts"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/components/overview-card.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/components/overview-card.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/components/breakdown-card.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/components/breakdown-card.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["analytics", "dashboard"],
|
||||
"meta": {
|
||||
"iframeHeight": "970px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logistics-network",
|
||||
"type": "registry:block",
|
||||
"title": "Logistics Network",
|
||||
"description": "Domestic logistics map with a sidebar of stats.",
|
||||
"dependencies": ["lucide-react"],
|
||||
"registryDependencies": ["map", "card", "badge", "button"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/logistics/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/data.ts",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/data.ts"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/components/map-arcs.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/components/map-arcs.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/components/filter-sidebar.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/components/filter-sidebar.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/components/network-map.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/components/network-map.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["logistics", "network"],
|
||||
"meta": {
|
||||
"iframeHeight": "800px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "heatmap",
|
||||
"type": "registry:block",
|
||||
"title": "Heatmap",
|
||||
"description": "Globe-projected heatmap visualizing earthquake density with zoom-dependent styling.",
|
||||
"dependencies": [],
|
||||
"registryDependencies": ["map", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/heatmap/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/heatmap/page.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["visualization", "heatmap"]
|
||||
},
|
||||
{
|
||||
"name": "delivery-tracker",
|
||||
"type": "registry:block",
|
||||
"title": "Delivery Tracker",
|
||||
"description": "Live order tracking with route progress, courier position, and order details.",
|
||||
"dependencies": ["lucide-react"],
|
||||
"registryDependencies": ["map", "card", "badge", "button"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/delivery-tracker/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/delivery/page.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["tracking", "delivery"],
|
||||
"meta": {
|
||||
"iframeHeight": "680px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
108
registry.json
108
registry.json
@@ -27,6 +27,114 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "analytics-map",
|
||||
"type": "registry:block",
|
||||
"title": "Analytics Map",
|
||||
"description": "Real-time analytics overview with a world map, breakdown cards, and device stats.",
|
||||
"dependencies": ["recharts", "lucide-react"],
|
||||
"registryDependencies": ["map", "card", "chart"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/analytics/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/data.ts",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/data.ts"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/components/overview-card.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/components/overview-card.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/analytics-map/components/breakdown-card.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/analytics/components/breakdown-card.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["analytics", "dashboard"],
|
||||
"meta": {
|
||||
"iframeHeight": "970px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logistics-network",
|
||||
"type": "registry:block",
|
||||
"title": "Logistics Network",
|
||||
"description": "Domestic logistics map with a sidebar of stats.",
|
||||
"dependencies": ["lucide-react"],
|
||||
"registryDependencies": ["map", "card", "badge", "button"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/logistics/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/data.ts",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/data.ts"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/components/map-arcs.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/components/map-arcs.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/components/filter-sidebar.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/components/filter-sidebar.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/registry/blocks/logistics-network/components/network-map.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "app/logistics/components/network-map.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["logistics", "network"],
|
||||
"meta": {
|
||||
"iframeHeight": "800px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "heatmap",
|
||||
"type": "registry:block",
|
||||
"title": "Heatmap",
|
||||
"description": "Globe-projected heatmap visualizing earthquake density with zoom-dependent styling.",
|
||||
"dependencies": [],
|
||||
"registryDependencies": ["map", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/heatmap/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/heatmap/page.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["visualization", "heatmap"]
|
||||
},
|
||||
{
|
||||
"name": "delivery-tracker",
|
||||
"type": "registry:block",
|
||||
"title": "Delivery Tracker",
|
||||
"description": "Live order tracking with route progress, courier position, and order details.",
|
||||
"dependencies": ["lucide-react"],
|
||||
"registryDependencies": ["map", "card", "badge", "button"],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/registry/blocks/delivery-tracker/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/delivery/page.tsx"
|
||||
}
|
||||
],
|
||||
"categories": ["tracking", "delivery"],
|
||||
"meta": {
|
||||
"iframeHeight": "680px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Map, MapMarker, MarkerContent, MarkerTooltip } from "@/registry/map";
|
||||
import { Zap } from "lucide-react";
|
||||
import { ExampleCard } from "./example-card";
|
||||
|
||||
export function EVChargingExample() {
|
||||
return (
|
||||
<ExampleCard
|
||||
label="EV Charging"
|
||||
className="aspect-square"
|
||||
delay="delay-700"
|
||||
>
|
||||
<Map center={[-122.425, 37.777]} zoom={11.5}>
|
||||
<MapMarker longitude={-122.4194} latitude={37.7749}>
|
||||
<MarkerContent>
|
||||
<div className="bg-emerald-500 rounded-full p-1.5 shadow-lg shadow-emerald-500/30">
|
||||
<Zap className="size-3 text-white fill-white" />
|
||||
</div>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div className="font-medium">Market St Station</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="size-1.5 rounded-full bg-emerald-500" />
|
||||
<span className="text-emerald-500">Available</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">150 kW • $0.35/kWh</div>
|
||||
</div>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
|
||||
<MapMarker longitude={-122.4094} latitude={37.7849}>
|
||||
<MarkerContent>
|
||||
<div className="bg-emerald-500 rounded-full p-1.5 shadow-lg shadow-emerald-500/30">
|
||||
<Zap className="size-3 text-white fill-white" />
|
||||
</div>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div className="font-medium">Union Square</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="size-1.5 rounded-full bg-emerald-500" />
|
||||
<span className="text-emerald-500">2 Available</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">50 kW • $0.28/kWh</div>
|
||||
</div>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
|
||||
<MapMarker longitude={-122.4294} latitude={37.7689}>
|
||||
<MarkerContent>
|
||||
<div className="bg-amber-500 rounded-full p-1.5 shadow-lg shadow-amber-500/30">
|
||||
<Zap className="size-3 text-white fill-white" />
|
||||
</div>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div className="font-medium">Castro Station</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="size-1.5 rounded-full bg-amber-500" />
|
||||
<span className="text-amber-500">In Use</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">~15 min remaining</div>
|
||||
</div>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
|
||||
<MapMarker longitude={-122.4394} latitude={37.7809}>
|
||||
<MarkerContent>
|
||||
<div className="bg-zinc-400 rounded-full p-1.5 shadow-lg">
|
||||
<Zap className="size-3 text-white fill-white" />
|
||||
</div>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div className="font-medium">Hayes Valley</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="size-1.5 rounded-full bg-zinc-400" />
|
||||
<span className="text-muted-foreground">Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
</Map>
|
||||
</ExampleCard>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="w-full py-5 border-t">
|
||||
<div className="w-full container flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground">mapcn</span>
|
||||
<span className="text-muted-foreground/80">•</span>
|
||||
<span>
|
||||
Built by
|
||||
<Button variant="link" size="sm" className="px-1" asChild>
|
||||
<Link
|
||||
href="https://github.com/AnmolSaini16"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Anmol
|
||||
</Link>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/docs">Documentation</Link>
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link
|
||||
href="https://github.com/AnmolSaini16/mapcn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Copy, Check, ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
const installCommand = "npx shadcn@latest add @mapcn/map";
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={copy}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-4 text-emerald-500" />
|
||||
) : (
|
||||
<Copy className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-x-0 -inset-y-32 -z-10 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.3] dark:opacity-[0.15]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle, currentColor 1px, transparent 1px)`,
|
||||
backgroundSize: "24px 24px",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-linear-to-b from-background via-transparent to-background" />
|
||||
</div>
|
||||
|
||||
<div className="container flex flex-col items-center text-center">
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-semibold tracking-tight animate-fade-up delay-100">
|
||||
<span className="bg-linear-to-b from-foreground to-foreground/60 bg-clip-text text-transparent">
|
||||
Beautiful maps, made simple.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="mt-6 text-foreground/80 text-lg md:text-xl leading-relaxed animate-fade-up delay-200 max-w-lg">
|
||||
Ready to use, customizable map components for React.
|
||||
<br className="hidden sm:block" />
|
||||
Built on MapLibre. Styled with Tailwind.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 animate-fade-up delay-300 w-full max-w-lg">
|
||||
<div className="bg-card border border-border rounded-lg shadow-xs overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50">
|
||||
<span className="size-2 rounded-full bg-foreground/20" />
|
||||
<span className="size-2 rounded-full bg-foreground/20" />
|
||||
<span className="size-2 rounded-full bg-foreground/20" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-3 font-mono text-sm">
|
||||
<span className="text-emerald-500 shrink-0">$</span>
|
||||
<code className="text-foreground/80 truncate flex-1 text-left">
|
||||
{installCommand}
|
||||
</code>
|
||||
<CopyButton text={installCommand} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-wrap justify-center items-center gap-3 animate-fade-up delay-400">
|
||||
<Button size="lg" asChild>
|
||||
<Link href="/docs">
|
||||
Get Started
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" size="lg" asChild>
|
||||
<Link href="/docs/basic-map">View Components</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Hero } from "./_components/hero";
|
||||
import { ExamplesGrid } from "./_components/examples-grid";
|
||||
import { Footer } from "./_components/footer";
|
||||
import { Header } from "./_components/header";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Header />
|
||||
|
||||
<main className="flex-1 pb-32">
|
||||
<section className="relative w-full py-24">
|
||||
<Hero />
|
||||
</section>
|
||||
|
||||
<section className="container">
|
||||
<ExamplesGrid />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { FlyToExample } from "./examples/flyto-example";
|
||||
|
||||
export function ExamplesGrid() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 animate-fade-in delay-400">
|
||||
<div className="animate-fade-in grid grid-cols-1 gap-5 delay-400 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<AnalyticsExample />
|
||||
<TrailExample />
|
||||
<FlyToExample />
|
||||
@@ -56,7 +56,7 @@ export function DeliveryExample() {
|
||||
)}
|
||||
<MapMarker longitude={store.lng} latitude={store.lat}>
|
||||
<MarkerContent>
|
||||
<div className="size-3.5 rounded-full bg-blue-500 border-2 border-white shadow-lg" />
|
||||
<div className="size-3.5 rounded-full bg-emerald-500 border-2 border-white shadow-lg" />
|
||||
<MarkerLabel>Store</MarkerLabel>
|
||||
</MarkerContent>
|
||||
</MapMarker>
|
||||
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import { Map, MapMarker, MarkerContent, MarkerTooltip } from "@/registry/map";
|
||||
import { Zap } from "lucide-react";
|
||||
import { ExampleCard } from "./example-card";
|
||||
|
||||
type Status = "available" | "in-use" | "offline";
|
||||
|
||||
interface ChargingStation {
|
||||
name: string;
|
||||
lng: number;
|
||||
lat: number;
|
||||
status: Status;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
const stations: ChargingStation[] = [
|
||||
{
|
||||
name: "Union Square",
|
||||
lng: -122.4074,
|
||||
lat: 37.7879,
|
||||
status: "available",
|
||||
detail: "50 kW • $0.28/kWh",
|
||||
},
|
||||
{
|
||||
name: "Castro Station",
|
||||
lng: -122.435,
|
||||
lat: 37.7625,
|
||||
status: "in-use",
|
||||
detail: "~15 min remaining",
|
||||
},
|
||||
{
|
||||
name: "Hayes Valley",
|
||||
lng: -122.4264,
|
||||
lat: 37.7759,
|
||||
status: "offline",
|
||||
detail: "",
|
||||
},
|
||||
{
|
||||
name: "Embarcadero",
|
||||
lng: -122.3934,
|
||||
lat: 37.7935,
|
||||
status: "available",
|
||||
detail: "350 kW • $0.40/kWh",
|
||||
},
|
||||
{
|
||||
name: "Marina District",
|
||||
lng: -122.437,
|
||||
lat: 37.801,
|
||||
status: "available",
|
||||
detail: "150 kW • $0.32/kWh",
|
||||
},
|
||||
{
|
||||
name: "SoMa Charger",
|
||||
lng: -122.401,
|
||||
lat: 37.778,
|
||||
status: "available",
|
||||
detail: "50 kW • $0.30/kWh",
|
||||
},
|
||||
{
|
||||
name: "Noe Valley",
|
||||
lng: -122.431,
|
||||
lat: 37.75,
|
||||
status: "available",
|
||||
detail: "150 kW • $0.33/kWh",
|
||||
},
|
||||
{
|
||||
name: "Richmond Charger",
|
||||
lng: -122.478,
|
||||
lat: 37.781,
|
||||
status: "in-use",
|
||||
detail: "~8 min remaining",
|
||||
},
|
||||
{
|
||||
name: "Potrero Hill",
|
||||
lng: -122.401,
|
||||
lat: 37.76,
|
||||
status: "offline",
|
||||
detail: "",
|
||||
},
|
||||
{
|
||||
name: "Mission Bay",
|
||||
lng: -122.391,
|
||||
lat: 37.77,
|
||||
status: "available",
|
||||
detail: "350 kW • $0.38/kWh",
|
||||
},
|
||||
{
|
||||
name: "Golden Gate Park",
|
||||
lng: -122.466,
|
||||
lat: 37.77,
|
||||
status: "available",
|
||||
detail: "150 kW • $0.34/kWh",
|
||||
},
|
||||
];
|
||||
|
||||
const statusConfig: Record<
|
||||
Status,
|
||||
{ bg: string; label: string; textClass: string }
|
||||
> = {
|
||||
available: {
|
||||
bg: "bg-emerald-500",
|
||||
label: "Available",
|
||||
textClass: "text-emerald-500",
|
||||
},
|
||||
"in-use": {
|
||||
bg: "bg-amber-500",
|
||||
label: "In Use",
|
||||
textClass: "text-amber-500",
|
||||
},
|
||||
offline: {
|
||||
bg: "bg-zinc-400",
|
||||
label: "Offline",
|
||||
textClass: "text-muted-foreground",
|
||||
},
|
||||
};
|
||||
|
||||
export function EVChargingExample() {
|
||||
return (
|
||||
<ExampleCard
|
||||
label="EV Charging"
|
||||
className="aspect-square"
|
||||
delay="delay-700"
|
||||
>
|
||||
<Map center={[-122.434, 37.776]} zoom={11}>
|
||||
{stations.map((station) => {
|
||||
const config = statusConfig[station.status];
|
||||
return (
|
||||
<MapMarker
|
||||
key={station.name}
|
||||
longitude={station.lng}
|
||||
latitude={station.lat}
|
||||
>
|
||||
<MarkerContent>
|
||||
<div
|
||||
className={`${config.bg} rounded-full p-1.5 shadow-lg ${
|
||||
station.status !== "offline"
|
||||
? `shadow-${config.bg.replace("bg-", "")}/30`
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Zap className="size-3 text-white fill-white" />
|
||||
</div>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div className="font-medium">{station.name}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`size-1.5 rounded-full ${config.bg}`} />
|
||||
<span className={config.textClass}>{config.label}</span>
|
||||
</div>
|
||||
{station.detail && (
|
||||
<div className="text-muted-foreground">
|
||||
{station.detail}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
);
|
||||
})}
|
||||
</Map>
|
||||
</ExampleCard>
|
||||
);
|
||||
}
|
||||
@@ -18,13 +18,13 @@ export function ExampleCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl overflow-hidden shadow-sm bg-card border border-border/50 relative animate-scale-in",
|
||||
"bg-card border-border/50 animate-scale-in relative overflow-hidden rounded-xl border shadow-sm",
|
||||
delay,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
<div className="uppercase absolute top-2 left-2 z-10 tracking-wider text-[10px] text-muted-foreground bg-background/90 backdrop-blur-sm rounded px-2 py-1">
|
||||
<div className="text-muted-foreground bg-background/90 absolute top-2 left-2 z-10 rounded px-2 py-1 text-[10px] tracking-wider uppercase backdrop-blur-sm">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
40
src/app/(main)/(home)/page.tsx
Normal file
40
src/app/(main)/(home)/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ExamplesGrid } from "./_components/examples-grid";
|
||||
import { Footer } from "@/components/footer";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderHeading,
|
||||
PageHeaderDescription,
|
||||
PageActions,
|
||||
} from "@/components/page-header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
<PageHeaderHeading>Beautiful maps, made simple</PageHeaderHeading>
|
||||
<PageHeaderDescription>
|
||||
Ready to use, customizable map components for React.
|
||||
<br className="hidden sm:block" />
|
||||
Built on MapLibre. Styled with Tailwind.
|
||||
</PageHeaderDescription>
|
||||
|
||||
<PageActions>
|
||||
<Button size="lg" asChild>
|
||||
<Link href="/docs">Get Started</Link>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" asChild>
|
||||
<Link href="/docs/basic-map">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
|
||||
<section className="container-wide">
|
||||
<ExamplesGrid />
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
src/app/(main)/blocks/_components/block-display.tsx
Normal file
40
src/app/(main)/blocks/_components/block-display.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getAllBlocks, createFileTreeForRegistryItemFiles } from "@/lib/blocks";
|
||||
import { getBlockFileSource } from "@/lib/get-block-file-source";
|
||||
import { highlightCode } from "@/lib/highlight";
|
||||
import { BlockPreview } from "./block-preview";
|
||||
import { IframePreview } from "./iframe-preview";
|
||||
|
||||
interface BlockDisplayProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function BlockDisplay({ name }: BlockDisplayProps) {
|
||||
const blocks = getAllBlocks();
|
||||
const block = blocks.find((b) => b.name === name);
|
||||
|
||||
if (!block || !block.files?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tree = createFileTreeForRegistryItemFiles(block.files);
|
||||
|
||||
const highlightedFiles = await Promise.all(
|
||||
block.files.map(async (file) => {
|
||||
const content = getBlockFileSource(file.path);
|
||||
const lang = file.path.split(".").pop() ?? "tsx";
|
||||
const highlightedContent = await highlightCode(content, lang);
|
||||
return {
|
||||
path: file.path,
|
||||
target: file.target ?? file.path,
|
||||
content,
|
||||
highlightedContent,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<BlockPreview block={block} tree={tree} highlightedFiles={highlightedFiles}>
|
||||
<IframePreview src={`/view/${block.name}`} title={block.title ?? block.name} />
|
||||
</BlockPreview>
|
||||
);
|
||||
}
|
||||
97
src/app/(main)/blocks/_components/block-preview.tsx
Normal file
97
src/app/(main)/blocks/_components/block-preview.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Code, Eye, Fullscreen, Terminal } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RegistryBlockItem, type FileTree } from "@/lib/blocks";
|
||||
import { BlockViewerCode, type HighlightedFile } from "./block-viewer-code";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Link from "next/link";
|
||||
|
||||
interface BlockPreviewProps {
|
||||
block: RegistryBlockItem;
|
||||
children: React.ReactNode;
|
||||
tree: FileTree[];
|
||||
highlightedFiles: HighlightedFile[];
|
||||
}
|
||||
|
||||
export function BlockPreview({
|
||||
block,
|
||||
children,
|
||||
tree,
|
||||
highlightedFiles,
|
||||
}: BlockPreviewProps) {
|
||||
const { name, title, description, meta } = block;
|
||||
const [copiedType, setCopiedType] = useState<"code" | "cli" | null>(null);
|
||||
|
||||
async function copyCli() {
|
||||
await navigator.clipboard.writeText(`npx shadcn@latest add @mapcn/${name}`);
|
||||
setCopiedType("cli");
|
||||
setTimeout(() => setCopiedType(null), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="space-y-4"
|
||||
style={
|
||||
{
|
||||
"--block-preview-height": meta?.iframeHeight ?? "930px",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold tracking-tight">{title}</h2>
|
||||
{description && (
|
||||
<p className="text-muted-foreground mt-1 text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<TabsList className="h-8!">
|
||||
<TabsTrigger value="preview" className="text-xs">
|
||||
<Eye className="size-3.5" />
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code" className="text-xs">
|
||||
<Code className="size-3.5" />
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
asChild
|
||||
aria-label="Open in new tab"
|
||||
>
|
||||
<Link href={`/view/${name}`} target="_blank">
|
||||
<Fullscreen />
|
||||
</Link>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Button
|
||||
onClick={copyCli}
|
||||
variant="outline"
|
||||
aria-label="Copy CLI command"
|
||||
size="sm"
|
||||
>
|
||||
{copiedType === "cli" ? <Check /> : <Terminal />}
|
||||
npx shadcn add @mapcn/{name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="preview" className="mt-2">
|
||||
{children}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="code" className="mt-2">
|
||||
<BlockViewerCode tree={tree} highlightedFiles={highlightedFiles} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
201
src/app/(main)/blocks/_components/block-viewer-code.tsx
Normal file
201
src/app/(main)/blocks/_components/block-viewer-code.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Check, ChevronRight, Copy, File, Folder } from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { type FileTree } from "@/lib/blocks";
|
||||
|
||||
export interface HighlightedFile {
|
||||
path: string;
|
||||
target: string;
|
||||
content: string;
|
||||
highlightedContent: string;
|
||||
}
|
||||
|
||||
interface BlockViewerCodeContext {
|
||||
activeFile: string;
|
||||
setActiveFile: (file: string) => void;
|
||||
highlightedFiles: HighlightedFile[];
|
||||
tree: FileTree[];
|
||||
}
|
||||
|
||||
const BlockViewerCodeCtx = React.createContext<BlockViewerCodeContext | null>(
|
||||
null
|
||||
);
|
||||
|
||||
function useBlockViewerCode() {
|
||||
const ctx = React.useContext(BlockViewerCodeCtx);
|
||||
if (!ctx) {
|
||||
throw new Error("useBlockViewerCode must be used within BlockViewerCode");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
interface BlockViewerCodeProps {
|
||||
tree: FileTree[];
|
||||
highlightedFiles: HighlightedFile[];
|
||||
}
|
||||
|
||||
export function BlockViewerCode({
|
||||
tree,
|
||||
highlightedFiles,
|
||||
}: BlockViewerCodeProps) {
|
||||
const [activeFile, setActiveFile] = React.useState<string>(
|
||||
highlightedFiles[0]?.target ?? ""
|
||||
);
|
||||
|
||||
const file = React.useMemo(
|
||||
() => highlightedFiles.find((f) => f.target === activeFile),
|
||||
[highlightedFiles, activeFile]
|
||||
);
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
return (
|
||||
<BlockViewerCodeCtx.Provider
|
||||
value={{ activeFile, setActiveFile, highlightedFiles, tree }}
|
||||
>
|
||||
<div className="flex overflow-hidden rounded-xl border h-(--block-preview-height)">
|
||||
<div className="w-64 shrink-0">
|
||||
<FileTreeSidebar />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 flex flex-col">
|
||||
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-4 text-sm">
|
||||
<span className="text-muted-foreground">{file.target}</span>
|
||||
<div className="ml-auto">
|
||||
<CopyCodeButton />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key={file.path}
|
||||
dangerouslySetInnerHTML={{ __html: file.highlightedContent }}
|
||||
className="flex-1 overflow-y-auto p-4 text-sm [&_pre]:bg-transparent! [&_code]:bg-transparent!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BlockViewerCodeCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FileTreeSidebar() {
|
||||
const { tree } = useBlockViewerCode();
|
||||
|
||||
return (
|
||||
<SidebarProvider className="flex min-h-full! flex-col border-r">
|
||||
<Sidebar collapsible="none" className="w-full flex-1 bg-card">
|
||||
<SidebarGroupLabel className="h-12 rounded-none border-b px-4 text-sm">
|
||||
Files
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="translate-x-0 gap-1.5">
|
||||
{tree.map((file, index) => (
|
||||
<TreeNode key={index} item={file} index={1} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeNode({ item, index }: { item: FileTree; index: number }) {
|
||||
const { activeFile, setActiveFile } = useBlockViewerCode();
|
||||
|
||||
if (!item.children) {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={item.path === activeFile}
|
||||
onClick={() => item.path && setActiveFile(item.path)}
|
||||
className="hover:bg-muted-foreground/15 focus:bg-muted-foreground/15 focus-visible:bg-muted-foreground/15 active:bg-muted-foreground/15 data-[active=true]:bg-muted-foreground/15 rounded-none whitespace-nowrap pl-(--index)"
|
||||
data-index={index}
|
||||
style={
|
||||
{
|
||||
"--index": `${index * (index === 2 ? 1.2 : 1.3)}rem`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<ChevronRight className="invisible" />
|
||||
<File className="size-4" />
|
||||
{item.name}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<Collapsible
|
||||
className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90"
|
||||
defaultOpen
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
className="hover:bg-muted-foreground/15 focus:bg-muted-foreground/15 focus-visible:bg-muted-foreground/15 active:bg-muted-foreground/15 data-[active=true]:bg-muted-foreground/15 rounded-none whitespace-nowrap pl-(--index)"
|
||||
style={
|
||||
{
|
||||
"--index": `${index * (index === 1 ? 1 : 1.2)}rem`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<ChevronRight className="transition-transform" />
|
||||
<Folder />
|
||||
{item.name}
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub className="m-0 w-full translate-x-0 border-none p-0">
|
||||
{item.children.map((subItem, key) => (
|
||||
<TreeNode key={key} item={subItem} index={index + 1} />
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyCodeButton() {
|
||||
const { activeFile, highlightedFiles } = useBlockViewerCode();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
const file = React.useMemo(
|
||||
() => highlightedFiles.find((f) => f.target === activeFile),
|
||||
[highlightedFiles, activeFile]
|
||||
);
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(file.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}}
|
||||
>
|
||||
{copied ? <Check /> : <Copy />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
14
src/app/(main)/blocks/_components/iframe-preview.tsx
Normal file
14
src/app/(main)/blocks/_components/iframe-preview.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
interface IframePreviewProps {
|
||||
src: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function IframePreview({ src, title }: IframePreviewProps) {
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-xl border h-(--block-preview-height)">
|
||||
<iframe src={src} title={title} className="size-full border-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/app/(main)/blocks/page.tsx
Normal file
55
src/app/(main)/blocks/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Metadata } from "next";
|
||||
import { getAllBlocks } from "@/lib/blocks";
|
||||
import { BlockDisplay } from "./_components/block-display";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Map blocks for your application",
|
||||
description:
|
||||
"Pre-built, ready-to-use map blocks. Browse, preview, and copy them into your app with one command.",
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
const blocks = getAllBlocks();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader align="left">
|
||||
<PageHeaderHeading className="md:text-5xl">
|
||||
Map blocks for your application
|
||||
</PageHeaderHeading>
|
||||
<PageHeaderDescription className="md:text-lg">
|
||||
Pre-built, ready-to-use map blocks. Browse, preview, and copy them
|
||||
into your app with one command.
|
||||
</PageHeaderDescription>
|
||||
<PageActions className="delay-300">
|
||||
<Button asChild>
|
||||
<a href="#blocks">Browse Blocks</a>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/docs">View Documentation</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
|
||||
<section
|
||||
className="animate-fade-up container scroll-mt-20 space-y-20 delay-400"
|
||||
id="blocks"
|
||||
>
|
||||
{blocks.map((block) => (
|
||||
<BlockDisplay key={block.name} name={block.name} />
|
||||
))}
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,37 +6,31 @@ import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { docsNavigation } from "@/lib/docs-navigation";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { CommandSearch } from "@/components/command-search";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { GitHubButton } from "@/components/github-button";
|
||||
import { docsNavigation } from "@/lib/site-navigation";
|
||||
|
||||
export function DocsSidebar() {
|
||||
const pathname = usePathname();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<SidebarHeader className="p-4 gap-4 pb-2">
|
||||
<Logo />
|
||||
<CommandSearch className="w-full flex" />
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<Sidebar
|
||||
className="sticky top-16 z-30 hidden h-[calc(100svh-4rem)] overscroll-none bg-transparent lg:flex"
|
||||
collapsible="none"
|
||||
>
|
||||
<SidebarContent className="pt-6">
|
||||
{docsNavigation.map((group) => (
|
||||
<SidebarGroup key={group.title}>
|
||||
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
|
||||
<SidebarGroupLabel className="text-foreground">
|
||||
{group.title}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{group.items.map((item) => (
|
||||
@@ -44,14 +38,13 @@ export function DocsSidebar() {
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={pathname === item.href}
|
||||
className="font-medium text-muted-foreground"
|
||||
className="text-muted-foreground font-medium"
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setOpenMobile(false)}
|
||||
>
|
||||
<item.icon className="size-4" />
|
||||
<span>{item.title}</span>
|
||||
{item.title}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@@ -61,14 +54,6 @@ export function DocsSidebar() {
|
||||
</SidebarGroup>
|
||||
))}
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter className="border-t">
|
||||
<div className="flex justify-between">
|
||||
<GitHubButton withCount={false} />
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ function useActiveItem(itemIds: string[]) {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0% 0% -80% 0%" }
|
||||
{ rootMargin: "0% 0% -80% 0%" },
|
||||
);
|
||||
|
||||
for (const id of itemIds ?? []) {
|
||||
@@ -58,11 +58,11 @@ export function DocsToc({ items, className }: DocsTocProps) {
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-1", className)}>
|
||||
<p className="text-foreground text-[13px] font-medium mb-2">
|
||||
<p className="text-muted-foreground mb-2 flex items-center gap-1.5 text-xs font-medium">
|
||||
On This Page
|
||||
</p>
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-1 bottom-1 w-px bg-border" />
|
||||
<div className="bg-border absolute top-1 bottom-1 left-0 w-px" />
|
||||
<div className="flex flex-col gap-1">
|
||||
{items.map((item) => {
|
||||
const isActive = item.slug === activeHeading;
|
||||
@@ -71,14 +71,14 @@ export function DocsToc({ items, className }: DocsTocProps) {
|
||||
key={item.slug}
|
||||
href={`#${item.slug}`}
|
||||
className={cn(
|
||||
"relative pl-3 py-1 text-[13px] no-underline transition-colors",
|
||||
"relative py-1 pl-3 text-[0.8rem] no-underline transition-colors",
|
||||
isActive
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1 bottom-1 w-px bg-foreground rounded-full" />
|
||||
<div className="bg-foreground absolute top-1 bottom-1 left-0 w-px rounded-full" />
|
||||
)}
|
||||
{item.title}
|
||||
</a>
|
||||
@@ -62,12 +62,10 @@ export function DocsLayout({
|
||||
toc = [],
|
||||
}: DocsLayoutProps) {
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<div className="flex-1 min-w-0 max-w-3xl mx-auto pt-10 pb-20">
|
||||
<div className="flex">
|
||||
<div className="flex-1 min-w-0 max-w-[800px] lg:px-4 mx-auto pb-20 pt-12">
|
||||
<DocsTitle title={title} description={description} />
|
||||
|
||||
<div className="mt-12 space-y-12">{children}</div>
|
||||
|
||||
{(prev || next) && (
|
||||
<div className="flex items-center justify-between gap-4 mt-16">
|
||||
{prev ? (
|
||||
@@ -118,7 +116,7 @@ interface DocsSectionProps {
|
||||
export function DocsSection({ title, children }: DocsSectionProps) {
|
||||
const id = title ? slugify(title) : undefined;
|
||||
return (
|
||||
<section className="space-y-5 scroll-mt-24" id={id}>
|
||||
<section className="space-y-5 scroll-m-24" id={id}>
|
||||
{title && (
|
||||
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
||||
{title}
|
||||
@@ -3,13 +3,12 @@ import path from "path";
|
||||
|
||||
const EXAMPLES_DIR = path.join(
|
||||
process.cwd(),
|
||||
"src/app/docs/_components/examples"
|
||||
"src/app/(main)/docs/_components/examples"
|
||||
);
|
||||
|
||||
export function getExampleSource(filename: string): string {
|
||||
const filePath = path.join(EXAMPLES_DIR, filename);
|
||||
const source = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
// Clean up the source for display:
|
||||
return source.replace(/@\/registry\/map/g, "@/components/ui/map");
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { AdvancedUsageExample } from "../_components/examples/advanced-usage-exa
|
||||
import { CustomLayerExample } from "../_components/examples/custom-layer-example";
|
||||
import { LayerMarkersExample } from "../_components/examples/layer-markers-example";
|
||||
import { CodeBlock } from "../_components/code-block";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -136,6 +136,12 @@ export default function ApiReferencePage() {
|
||||
description:
|
||||
"Callback fired continuously as the viewport changes (during pan, zoom, rotate). Can be used alone to observe changes, or with viewport prop to enable controlled mode.",
|
||||
},
|
||||
{
|
||||
name: "loading",
|
||||
type: "boolean",
|
||||
default: "false",
|
||||
description: "Show a loading indicator on the map.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DocsSection>
|
||||
@@ -8,7 +8,7 @@ import { ComponentPreview } from "../_components/component-preview";
|
||||
import { BasicMapExample } from "../_components/examples/basic-map-example";
|
||||
import { ControlledMapExample } from "../_components/examples/controlled-map-example";
|
||||
import { CustomStyleExample } from "../_components/examples/custom-style-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DocsLayout, DocsSection, DocsCode } from "../_components/docs";
|
||||
import { ComponentPreview } from "../_components/component-preview";
|
||||
import ClusterExample from "../_components/examples/cluster-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DocsLayout, DocsSection, DocsCode } from "../_components/docs";
|
||||
import { ComponentPreview } from "../_components/component-preview";
|
||||
import { MapControlsExample } from "../_components/examples/map-controls-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
17
src/app/(main)/docs/layout.tsx
Normal file
17
src/app/(main)/docs/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { DocsSidebar } from "./_components/docs-sidebar";
|
||||
|
||||
export default function DocsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="container flex flex-col flex-1">
|
||||
<SidebarProvider>
|
||||
<DocsSidebar />
|
||||
<main className="size-full">{children}</main>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { ComponentPreview } from "../_components/component-preview";
|
||||
import { MarkersExample } from "../_components/examples/markers-example";
|
||||
import { PopupExample } from "../_components/examples/popup-example";
|
||||
import { DraggableMarkerExample } from "../_components/examples/draggable-marker-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DocsLayout, DocsSection, DocsCode } from "../_components/docs";
|
||||
import { ComponentPreview } from "../_components/component-preview";
|
||||
import { StandalonePopupExample } from "../_components/examples/standalone-popup-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { ComponentPreview } from "../_components/component-preview";
|
||||
import { RouteExample } from "../_components/examples/route-example";
|
||||
import { OsrmRouteExample } from "../_components/examples/osrm-route-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { getExampleSource } from "../_components/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
14
src/app/(main)/layout.tsx
Normal file
14
src/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
export default function MainLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-background relative flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/app/(view)/view/[name]/page.tsx
Normal file
38
src/app/(view)/view/[name]/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { slugToTitle } from "@/lib/utils";
|
||||
import { blockComponents } from "@/registry/blocks/__index__";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
interface BlockViewPageProps {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export const generateMetadata = async ({ params }: BlockViewPageProps) => {
|
||||
const { name } = await params;
|
||||
const Component = blockComponents[name];
|
||||
|
||||
if (!Component) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const title = slugToTitle(name);
|
||||
|
||||
return {
|
||||
title,
|
||||
description: `View the ${title} block`,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function BlockViewPage({ params }: BlockViewPageProps) {
|
||||
const { name } = await params;
|
||||
const Component = blockComponents[name];
|
||||
|
||||
if (!Component) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background min-h-screen">
|
||||
<Component />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { docsNavigation, type NavItem } from "@/lib/docs-navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DocsHeader({ className }: { className?: string }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
let activeItem: (NavItem & { groupTitle: string }) | null = null;
|
||||
let groupHref = "/docs";
|
||||
for (const group of docsNavigation) {
|
||||
const item = group.items.find((navItem) => navItem.href === pathname);
|
||||
if (item) {
|
||||
activeItem = { ...item, groupTitle: group.title };
|
||||
groupHref = group.items[0]?.href ?? "/docs";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"w-full p-4 px-6 sticky top-0 z-50 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/70",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<nav className="flex items-center gap-3">
|
||||
<SidebarTrigger />
|
||||
<div className="h-4 w-px shrink-0 bg-border" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/docs">Docs</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={groupHref}>
|
||||
{activeItem?.groupTitle ?? "Overview"}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{pathname !== "/docs" && pathname !== "/docs/basic-map" && (
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem className="min-w-0 truncate">
|
||||
<BreadcrumbPage>
|
||||
{activeItem?.title ?? "Documentation"}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { DocsSidebar } from "./_components/docs-sidebar";
|
||||
import { DocsHeader } from "./_components/docs-header";
|
||||
|
||||
export default function DocsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<DocsSidebar />
|
||||
<SidebarInset>
|
||||
<DocsHeader />
|
||||
<main className="container">{children}</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "./globals.css";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
const geist = Geist({
|
||||
subsets: ["latin"],
|
||||
@@ -100,9 +102,9 @@ export default function RootLayout({
|
||||
className={`${geist.variable} ${geistMono.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="font-sans antialiased min-h-screen flex flex-col">
|
||||
<body className="font-sans antialiased">
|
||||
<ThemeProvider>
|
||||
<div className="flex-1">{children}</div>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<Analytics />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center px-6">
|
||||
<div className="min-h-[80vh] bg-background flex flex-col items-center justify-center px-6">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="relative">
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Kbd, KbdGroup } from "@/components/ui/kbd";
|
||||
import { docsNavigation } from "@/lib/docs-navigation";
|
||||
import { siteNavigation } from "@/lib/site-navigation";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -47,17 +47,17 @@ export function CommandSearch({ className }: { className?: string }) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="Search documentation"
|
||||
className={cn(
|
||||
"hidden group md:flex items-center w-[200px] dark:border-border/60 border-border/80 text-muted-foreground",
|
||||
"hidden md:flex h-8 w-48 items-center gap-2 rounded-md border border-border/50 bg-muted/40 px-2.5 text-muted-foreground hover:bg-muted/70 hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<SearchIcon className="size-3.5 shrink-0" />
|
||||
<span>Search docs...</span>
|
||||
<SearchIcon className="size-3.5" />
|
||||
<span>Search...</span>
|
||||
<KbdGroup className="ml-auto">
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>K</Kbd>
|
||||
@@ -66,12 +66,12 @@ export function CommandSearch({ className }: { className?: string }) {
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="Search Documentation"
|
||||
description="Search for documentation pages and components"
|
||||
title="Search..."
|
||||
description="Jump to pages, components, and docs"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<CommandInput
|
||||
placeholder="Search documentation..."
|
||||
placeholder="Search..."
|
||||
className="border-none text-sm h-10"
|
||||
/>
|
||||
<CommandList>
|
||||
@@ -81,7 +81,7 @@ export function CommandSearch({ className }: { className?: string }) {
|
||||
<span>No results found.</span>
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{docsNavigation.map((group) => (
|
||||
{siteNavigation.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((item) => (
|
||||
<CommandItem
|
||||
|
||||
117
src/components/footer.tsx
Normal file
117
src/components/footer.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import Link from "next/link";
|
||||
import { MapPin } from "lucide-react";
|
||||
|
||||
const footerLinks = {
|
||||
product: [
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "Components", href: "/docs/basic-map" },
|
||||
{ label: "Blocks", href: "/blocks" },
|
||||
],
|
||||
community: [
|
||||
{
|
||||
label: "GitHub",
|
||||
href: "https://github.com/AnmolSaini16/mapcn",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
label: "Sponsor",
|
||||
href: "https://github.com/sponsors/AnmolSaini16",
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
resources: [
|
||||
{ label: "MapLibre GL", href: "https://maplibre.org/", external: true },
|
||||
{ label: "shadcn/ui", href: "https://ui.shadcn.com/", external: true },
|
||||
{
|
||||
label: "Tailwind CSS",
|
||||
href: "https://tailwindcss.com/",
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="mt-24 border-t md:mt-32">
|
||||
<div className="container py-12 md:py-16">
|
||||
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<Link href="/" className="flex w-fit items-center gap-1.5">
|
||||
<MapPin className="size-4 shrink-0" />
|
||||
<span className="text-lg font-semibold tracking-tight">
|
||||
mapcn
|
||||
</span>
|
||||
</Link>
|
||||
<p className="text-muted-foreground mt-3 max-w-xs text-sm leading-relaxed">
|
||||
Free & open-source, ready-to-use, customizable map components for
|
||||
React.
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-3 text-sm">
|
||||
Built by{" "}
|
||||
<Link
|
||||
href="https://x.com/anmol16_"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
||||
>
|
||||
@anmol
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Product</h3>
|
||||
<ul className="space-y-2.5">
|
||||
{footerLinks.product.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Community</h3>
|
||||
<ul className="space-y-2.5">
|
||||
{footerLinks.community.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
target={link.external ? "_blank" : undefined}
|
||||
rel={link.external ? "noopener noreferrer" : undefined}
|
||||
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Resources</h3>
|
||||
<ul className="space-y-2.5">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
target={link.external ? "_blank" : undefined}
|
||||
rel={link.external ? "noopener noreferrer" : undefined}
|
||||
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Heart } from "lucide-react";
|
||||
|
||||
import { Logo } from "@/components/logo";
|
||||
import { MainNav } from "@/components/main-nav";
|
||||
import { MobileNav } from "@/components/mobile-nav";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
@@ -15,11 +17,15 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export function Header({ className }: { className?: string }) {
|
||||
return (
|
||||
<header className={cn("w-full p-4 container", className)}>
|
||||
<nav className="flex size-full items-center justify-between">
|
||||
<Logo />
|
||||
<header
|
||||
className={cn("bg-background sticky top-0 z-50 h-14 w-full", className)}
|
||||
>
|
||||
<nav className="container flex size-full items-center gap-2">
|
||||
<MobileNav />
|
||||
<Logo className="hidden shrink-0 lg:flex" />
|
||||
<MainNav className="hidden lg:flex" />
|
||||
|
||||
<div className="flex items-center gap-2 h-4.5">
|
||||
<div className="ml-auto flex h-4.5 items-center gap-2">
|
||||
<CommandSearch />
|
||||
<Separator orientation="vertical" className="hidden md:block" />
|
||||
<Tooltip>
|
||||
@@ -30,8 +36,8 @@ export function Header({ className }: { className?: string }) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Heart className="size-4 text-pink-500" />
|
||||
<span className="hidden md:inline">Sponsor</span>
|
||||
<Heart className="size-3.5 text-pink-500" />
|
||||
Sponsor
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -1,16 +1,25 @@
|
||||
import Link from "next/link";
|
||||
import { MapPin } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Logo({ className }: LogoProps) {
|
||||
export function Logo({ className, onClick }: LogoProps) {
|
||||
return (
|
||||
<Link href="/" className={cn("flex items-center gap-1.5 w-fit", className)}>
|
||||
<MapPin className="size-4 shrink-0" />
|
||||
<span className="font-semibold text-lg tracking-tight">mapcn</span>
|
||||
</Link>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className={cn("px-2.5 text-base font-semibold", className)}
|
||||
>
|
||||
<Link href="/" onClick={onClick}>
|
||||
<MapPin />
|
||||
mapcn
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
36
src/components/main-nav.tsx
Normal file
36
src/components/main-nav.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { siteNavigation } from "@/lib/site-navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function MainNav({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
const navItems = siteNavigation
|
||||
.find((group) => group.title === "Pages")
|
||||
?.items.filter((item) => item.title !== "Home");
|
||||
|
||||
if (!navItems) return null;
|
||||
|
||||
return (
|
||||
<nav className={cn("items-center gap-1", className)} {...props}>
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
key={item.href}
|
||||
variant="ghost"
|
||||
asChild
|
||||
size="sm"
|
||||
className="px-2.5"
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="relative inline-flex items-center gap-1.5"
|
||||
>
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
63
src/components/mobile-nav.tsx
Normal file
63
src/components/mobile-nav.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Menu } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { siteNavigation } from "@/lib/site-navigation";
|
||||
|
||||
export function MobileNav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Open docs menu"
|
||||
className="shrink-0 lg:hidden"
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="rounded-r-xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">Sidebar</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="overflow-y-auto px-2">
|
||||
<nav className="space-y-6">
|
||||
{siteNavigation.map((group) => (
|
||||
<div key={group.title}>
|
||||
<h3 className="text-muted-foreground mb-2 px-2 text-sm font-medium">
|
||||
{group.title}
|
||||
</h3>
|
||||
<ul>
|
||||
{group.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-center px-3 py-2.5"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
155
src/components/page-header.tsx
Normal file
155
src/components/page-header.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
type HeaderAlign = "center" | "left";
|
||||
|
||||
const PageHeaderContext = createContext<{ align: HeaderAlign }>({
|
||||
align: "center",
|
||||
});
|
||||
|
||||
function usePageHeaderContext() {
|
||||
return useContext(PageHeaderContext);
|
||||
}
|
||||
|
||||
interface PageHeaderProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
/** Show the dot grid background (default: true) */
|
||||
showBackground?: boolean;
|
||||
/** Header content alignment (default: center) */
|
||||
align?: HeaderAlign;
|
||||
}
|
||||
|
||||
function PageHeader({
|
||||
children,
|
||||
className,
|
||||
showBackground = true,
|
||||
align = "center",
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<PageHeaderContext.Provider value={{ align }}>
|
||||
<div className="relative">
|
||||
{showBackground && (
|
||||
<div className="pointer-events-none absolute inset-x-0 -inset-y-10 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.20] dark:opacity-[0.12]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle, currentColor 1px, transparent 1px)`,
|
||||
backgroundSize: "24px 24px",
|
||||
}}
|
||||
/>
|
||||
<div className="from-background to-background absolute inset-0 bg-linear-to-b via-transparent" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section
|
||||
className={cn(
|
||||
"container mx-auto flex w-full max-w-6xl flex-col gap-4 py-16 md:py-24 lg:pt-26 lg:pb-24",
|
||||
align === "center"
|
||||
? "items-center text-center"
|
||||
: "items-start text-left",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
</PageHeaderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageHeaderHeadingProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
as?: "h1" | "h2";
|
||||
}
|
||||
|
||||
function PageHeaderHeading({
|
||||
children,
|
||||
className,
|
||||
as: Comp = "h1",
|
||||
}: PageHeaderHeadingProps) {
|
||||
const { align } = usePageHeaderContext();
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"animate-fade-up max-w-4xl text-4xl font-semibold tracking-tight delay-100 sm:text-5xl md:text-6xl",
|
||||
align === "center" ? "text-center" : "text-left",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="from-foreground via-foreground to-foreground/65 bg-linear-to-b bg-clip-text text-transparent">
|
||||
{children}
|
||||
</span>
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageHeaderDescriptionProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function PageHeaderDescription({
|
||||
children,
|
||||
className,
|
||||
}: PageHeaderDescriptionProps) {
|
||||
const { align } = usePageHeaderContext();
|
||||
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"text-muted-foreground animate-fade-up max-w-2xl leading-relaxed delay-200 sm:text-lg sm:leading-relaxed md:text-xl md:leading-relaxed",
|
||||
align === "center" ? "text-center" : "text-left",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageContentProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function PageContent({ children, className }: PageContentProps) {
|
||||
return (
|
||||
<div className={cn("animate-fade-up w-full max-w-lg delay-300", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageActionsProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function PageActions({ children, className }: PageActionsProps) {
|
||||
const { align } = usePageHeaderContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"animate-fade-up mt-3 flex flex-wrap items-center gap-3 delay-400",
|
||||
align === "center" ? "justify-center" : "justify-start",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
PageHeader,
|
||||
PageHeaderHeading,
|
||||
PageHeaderDescription,
|
||||
PageContent,
|
||||
PageActions,
|
||||
};
|
||||
@@ -60,7 +60,6 @@ export function ThemeToggle() {
|
||||
size="icon-sm"
|
||||
>
|
||||
{resolvedTheme === "dark" ? <Moon /> : <Sun />}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-2 pr-1">
|
||||
|
||||
48
src/components/ui/badge.tsx
Normal file
48
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -1,30 +1,32 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/60",
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
@@ -34,7 +36,7 @@ const buttonVariants = cva(
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
@@ -44,9 +46,9 @@ function Button({
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -56,7 +58,7 @@ function Button({
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
"flex flex-col gap-4 rounded-xl border bg-card py-4 text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -20,12 +20,12 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -35,17 +35,17 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -58,27 +58,27 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn("px-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn("flex items-center px-4 [.border-t]:pt-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -89,4 +89,4 @@ export {
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
};
|
||||
|
||||
357
src/components/ui/chart.tsx
Normal file
357
src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium text-foreground tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
33
src/components/ui/collapsible.tsx
Normal file
33
src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -8,9 +8,9 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -17,7 +17,7 @@ function Separator({
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -36,7 +36,7 @@ function SheetOverlay({
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -48,9 +48,11 @@ function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@@ -58,24 +60,26 @@ function SheetContent({
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
@@ -108,7 +112,7 @@ function SheetTitle({
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
className={cn("font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -121,7 +125,7 @@ function SheetDescription({
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,57 +1,56 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Kbd, KbdGroup } from "./kbd";
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
@@ -63,36 +62,36 @@ function SidebarProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open]
|
||||
);
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
@@ -101,18 +100,18 @@ function SidebarProvider({
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
@@ -125,7 +124,7 @@ function SidebarProvider({
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
@@ -140,7 +139,7 @@ function SidebarProvider({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -149,7 +148,7 @@ function SidebarProvider({
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
@@ -160,25 +159,25 @@ function Sidebar({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
@@ -188,7 +187,7 @@ function Sidebar({
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
@@ -203,12 +202,12 @@ function Sidebar({
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
className="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
@@ -245,13 +244,13 @@ function Sidebar({
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
@@ -259,40 +258,29 @@ function SidebarTrigger({
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Toggle Sidebar{" "}
|
||||
<KbdGroup>
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>{SIDEBAR_KEYBOARD_SHORTCUT.toUpperCase()}</Kbd>
|
||||
</KbdGroup>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -303,17 +291,17 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
@@ -321,13 +309,13 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"relative flex w-full flex-1 flex-col bg-background",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
@@ -338,10 +326,10 @@ function SidebarInput({
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
className={cn("h-8 w-full bg-background shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -352,7 +340,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -363,7 +351,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
@@ -374,10 +362,10 @@ function SidebarSeparator({
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -391,7 +379,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -402,7 +390,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
@@ -410,20 +398,20 @@ function SidebarGroupLabel({
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
const Comp = asChild ? Slot.Root : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
@@ -431,14 +419,14 @@ function SidebarGroupAction({
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
@@ -446,7 +434,7 @@ function SidebarGroupAction({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
@@ -460,7 +448,7 @@ function SidebarGroupContent({
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
@@ -471,7 +459,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
@@ -482,11 +470,11 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -505,7 +493,7 @@ const sidebarMenuButtonVariants = cva(
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
@@ -516,12 +504,12 @@ function SidebarMenuButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
@@ -532,16 +520,16 @@ function SidebarMenuButton({
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -554,7 +542,7 @@ function SidebarMenuButton({
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
@@ -563,17 +551,17 @@ function SidebarMenuAction({
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
@@ -581,12 +569,12 @@ function SidebarMenuAction({
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
@@ -598,7 +586,7 @@ function SidebarMenuBadge({
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
@@ -608,7 +596,7 @@ function SidebarMenuBadge({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
@@ -616,12 +604,12 @@ function SidebarMenuSkeleton({
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -646,7 +634,7 @@ function SidebarMenuSkeleton({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
@@ -655,13 +643,13 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
@@ -675,7 +663,7 @@ function SidebarMenuSubItem({
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
@@ -685,11 +673,11 @@ function SidebarMenuSubButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
const Comp = asChild ? Slot.Root : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -698,7 +686,7 @@ function SidebarMenuSubButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
@@ -707,7 +695,7 @@ function SidebarMenuSubButton({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -735,4 +723,4 @@ export {
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
className={cn("animate-pulse rounded-md bg-accent", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
91
src/components/ui/tabs.tsx
Normal file
91
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
@@ -15,23 +15,19 @@ function TooltipProvider({
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
@@ -46,16 +42,16 @@ function TooltipContent({
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
||||
77
src/lib/blocks.ts
Normal file
77
src/lib/blocks.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import registry from "../../registry.json";
|
||||
|
||||
export interface RegistryBlockItem {
|
||||
name: string;
|
||||
type: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
files?: Array<{ path: string; target?: string }>;
|
||||
registryDependencies?: string[];
|
||||
categories?: string[];
|
||||
meta?: { iframeHeight?: string };
|
||||
}
|
||||
|
||||
interface RegistrySchema {
|
||||
items: RegistryBlockItem[];
|
||||
}
|
||||
|
||||
export interface FileTree {
|
||||
name: string;
|
||||
path?: string;
|
||||
children?: FileTree[];
|
||||
}
|
||||
|
||||
const typedRegistry = registry as RegistrySchema;
|
||||
|
||||
export function getAllBlocks(): RegistryBlockItem[] {
|
||||
return typedRegistry.items
|
||||
.filter((item) => item.type === "registry:block")
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
title: item.title ?? item.name,
|
||||
description: item.description,
|
||||
files: item.files ?? [],
|
||||
registryDependencies: item.registryDependencies ?? [],
|
||||
categories: item.categories ?? [],
|
||||
meta: item.meta,
|
||||
}));
|
||||
}
|
||||
|
||||
export function createFileTreeForRegistryItemFiles(
|
||||
files: Array<{ path: string; target?: string }>,
|
||||
): FileTree[] {
|
||||
const root: FileTree[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = file.target ?? file.path;
|
||||
const parts = filePath.split("/");
|
||||
let currentLevel = root;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isFile = i === parts.length - 1;
|
||||
const existingNode = currentLevel.find((node) => node.name === part);
|
||||
|
||||
if (existingNode) {
|
||||
if (isFile) {
|
||||
existingNode.path = filePath;
|
||||
} else {
|
||||
currentLevel = existingNode.children!;
|
||||
}
|
||||
} else {
|
||||
const newNode: FileTree = isFile
|
||||
? { name: part, path: filePath }
|
||||
: { name: part, children: [] };
|
||||
|
||||
currentLevel.push(newNode);
|
||||
|
||||
if (!isFile) {
|
||||
currentLevel = newNode.children!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
11
src/lib/get-block-file-source.ts
Normal file
11
src/lib/get-block-file-source.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const SRC_DIR = process.cwd();
|
||||
|
||||
export function getBlockFileSource(registryPath: string): string {
|
||||
const filePath = path.join(SRC_DIR, registryPath);
|
||||
const source = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
return source.replace(/@\/registry\/map/g, "@/components/ui/map");
|
||||
}
|
||||
@@ -1,29 +1,37 @@
|
||||
import {
|
||||
Map,
|
||||
BookOpen,
|
||||
Code,
|
||||
Braces,
|
||||
Code,
|
||||
Home,
|
||||
Layers,
|
||||
LayoutGrid,
|
||||
LucideIcon,
|
||||
Map,
|
||||
MapPin,
|
||||
MessageSquare,
|
||||
Route,
|
||||
Wrench,
|
||||
Settings,
|
||||
Layers,
|
||||
LucideIcon,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface NavItem {
|
||||
export interface MainNavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SiteNavigationItem {
|
||||
title: string;
|
||||
href: string;
|
||||
icon: LucideIcon;
|
||||
new?: boolean;
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
export interface SiteNavigationGroup {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
items: SiteNavigationItem[];
|
||||
}
|
||||
|
||||
export const docsNavigation: NavGroup[] = [
|
||||
export const docsNavigation: SiteNavigationGroup[] = [
|
||||
{
|
||||
title: "Basics",
|
||||
items: [
|
||||
@@ -45,3 +53,18 @@ export const docsNavigation: NavGroup[] = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const navItems: SiteNavigationItem[] = [
|
||||
{ title: "Home", href: "/", icon: Home },
|
||||
{ title: "Docs", href: "/docs", icon: BookOpen },
|
||||
{ title: "Components", href: "/docs/basic-map", icon: Map },
|
||||
{ title: "Blocks", href: "/blocks", icon: LayoutGrid },
|
||||
];
|
||||
|
||||
export const siteNavigation: SiteNavigationGroup[] = [
|
||||
{
|
||||
title: "Pages",
|
||||
items: navItems,
|
||||
},
|
||||
...docsNavigation,
|
||||
];
|
||||
@@ -4,3 +4,7 @@ import { twMerge } from "tailwind-merge";
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function slugToTitle(name: string): string {
|
||||
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
28
src/registry/blocks/__index__.tsx
Normal file
28
src/registry/blocks/__index__.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { RegistryBlockItem } from "@/lib/blocks";
|
||||
|
||||
export const blockComponents: Record<
|
||||
RegistryBlockItem["name"],
|
||||
React.LazyExoticComponent<React.ComponentType<object>>
|
||||
> = {
|
||||
"analytics-map": React.lazy(() =>
|
||||
import("./analytics-map/page").then((mod) => ({
|
||||
default: mod.AnalyticsMapBlock,
|
||||
}))
|
||||
),
|
||||
heatmap: React.lazy(() =>
|
||||
import("./heatmap/page").then((mod) => ({
|
||||
default: mod.HeatmapBlock,
|
||||
}))
|
||||
),
|
||||
"delivery-tracker": React.lazy(() =>
|
||||
import("./delivery-tracker/page").then((mod) => ({
|
||||
default: mod.DeliveryBlock,
|
||||
}))
|
||||
),
|
||||
"logistics-network": React.lazy(() =>
|
||||
import("./logistics-network/page").then((mod) => ({
|
||||
default: mod.LogisticsNetworkBlock,
|
||||
}))
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { type BreakdownRow } from "../data";
|
||||
|
||||
interface BreakdownCardProps {
|
||||
title: string;
|
||||
rows: BreakdownRow[];
|
||||
}
|
||||
|
||||
export function BreakdownCard({ title, rows }: BreakdownCardProps) {
|
||||
const maxRowValue =
|
||||
rows.length > 0 ? Math.max(...rows.map((row) => row.value)) : 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="text-muted-foreground mb-2 flex items-center justify-between text-[11px] tracking-wider uppercase">
|
||||
<span>{title}</span>
|
||||
<span>Visitors</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-foreground/90 truncate">{row.label}</span>
|
||||
<span className="text-foreground font-medium">{row.value}</span>
|
||||
</div>
|
||||
<div className="bg-muted h-1 rounded-full">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-500/85"
|
||||
style={{ width: `${(row.value / maxRowValue) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
113
src/registry/blocks/analytics-map/components/overview-card.tsx
Normal file
113
src/registry/blocks/analytics-map/components/overview-card.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { ChartContainer } from "@/components/ui/chart";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
import { Area, AreaChart, Cell, Pie, PieChart } from "recharts";
|
||||
import {
|
||||
deviceCategoryChartConfig,
|
||||
deviceCategoryData,
|
||||
usersPerDay,
|
||||
usersPerDayChartConfig,
|
||||
} from "../data";
|
||||
|
||||
function MetricChart() {
|
||||
return (
|
||||
<ChartContainer
|
||||
config={usersPerDayChartConfig}
|
||||
className="aspect-auto h-8 w-full"
|
||||
>
|
||||
<AreaChart data={usersPerDay} margin={{ left: 4, right: 4, top: 4 }}>
|
||||
<defs>
|
||||
<linearGradient id="usersGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor="var(--color-users)"
|
||||
stopOpacity={0.4}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor="var(--color-users)"
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<Area
|
||||
type="natural"
|
||||
dataKey="users"
|
||||
stroke="var(--color-users)"
|
||||
strokeWidth={1.5}
|
||||
fill="url(#usersGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverviewCard() {
|
||||
return (
|
||||
<Card className="bg-card/70 absolute top-4 left-4 z-10 w-60 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<p className="text-muted-foreground pb-2 text-[10px] tracking-wider uppercase">
|
||||
Users in last 7 days
|
||||
</p>
|
||||
<p className="text-3xl leading-none font-semibold">3,803</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<MetricChart />
|
||||
<div className="mt-4 flex items-center gap-1.5 text-xs">
|
||||
<TrendingUp className="size-3 text-emerald-500" />
|
||||
<span className="font-medium text-emerald-500">+12.5%</span>
|
||||
<span className="text-muted-foreground">vs previous 7 days</span>
|
||||
</div>
|
||||
|
||||
<div className="border-border/60 mt-4 border-t pt-4">
|
||||
<p className="text-muted-foreground text-[10px] tracking-wider uppercase">
|
||||
Device category in last 7 days
|
||||
</p>
|
||||
|
||||
<ChartContainer
|
||||
config={deviceCategoryChartConfig}
|
||||
className="mx-auto mt-3 aspect-square h-32 w-32"
|
||||
>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={deviceCategoryData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
innerRadius={32}
|
||||
outerRadius={52}
|
||||
strokeWidth={2}
|
||||
>
|
||||
{deviceCategoryData.map((entry) => (
|
||||
<Cell key={entry.name} fill={entry.fill} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
{deviceCategoryData.map((device) => (
|
||||
<div key={device.name} className="text-center">
|
||||
<p className="text-muted-foreground flex items-center justify-center gap-1.5 text-[10px] tracking-wide uppercase">
|
||||
<span
|
||||
className="size-2 rounded-full"
|
||||
style={{ backgroundColor: device.fill }}
|
||||
/>
|
||||
{device.name}
|
||||
</p>
|
||||
<p className="text-foreground mt-1 leading-none font-medium tabular-nums">
|
||||
{device.value}%
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
99
src/registry/blocks/analytics-map/data.ts
Normal file
99
src/registry/blocks/analytics-map/data.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { type ChartConfig } from "@/components/ui/chart";
|
||||
|
||||
export interface LocationPoint {
|
||||
city: string;
|
||||
lng: number;
|
||||
lat: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface BreakdownRow {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const locations: LocationPoint[] = [
|
||||
{ city: "San Francisco", lng: -122.4194, lat: 37.7749, size: 16 },
|
||||
{ city: "New York", lng: -74.006, lat: 40.7128, size: 15 },
|
||||
{ city: "Toronto", lng: -79.3832, lat: 43.6532, size: 11 },
|
||||
{ city: "Mexico City", lng: -99.1332, lat: 19.4326, size: 10 },
|
||||
{ city: "Sao Paulo", lng: -46.6333, lat: -23.5505, size: 12 },
|
||||
{ city: "Buenos Aires", lng: -58.3816, lat: -34.6037, size: 9 },
|
||||
{ city: "London", lng: -0.1276, lat: 51.5074, size: 14 },
|
||||
{ city: "Berlin", lng: 13.405, lat: 52.52, size: 11 },
|
||||
{ city: "Paris", lng: 2.3522, lat: 48.8566, size: 13 },
|
||||
{ city: "Madrid", lng: -3.7038, lat: 40.4168, size: 10 },
|
||||
{ city: "Cairo", lng: 31.2357, lat: 30.0444, size: 9 },
|
||||
{ city: "Lagos", lng: 3.3792, lat: 6.5244, size: 10 },
|
||||
{ city: "Mumbai", lng: 72.8777, lat: 19.076, size: 13 },
|
||||
{ city: "Dubai", lng: 55.2708, lat: 25.2048, size: 11 },
|
||||
{ city: "Seoul", lng: 126.978, lat: 37.5665, size: 12 },
|
||||
{ city: "Singapore", lng: 103.8198, lat: 1.3521, size: 10 },
|
||||
{ city: "Tokyo", lng: 139.6917, lat: 35.6895, size: 12 },
|
||||
{ city: "Sydney", lng: 151.2093, lat: -33.8688, size: 9 },
|
||||
{ city: "Auckland", lng: 174.7633, lat: -36.8485, size: 8 },
|
||||
];
|
||||
|
||||
export const usersPerDay = [
|
||||
{ day: "Mon", users: 320 },
|
||||
{ day: "Tue", users: 410 },
|
||||
{ day: "Wed", users: 560 },
|
||||
{ day: "Thu", users: 640 },
|
||||
{ day: "Fri", users: 780 },
|
||||
{ day: "Sat", users: 690 },
|
||||
{ day: "Sun", users: 720 },
|
||||
];
|
||||
|
||||
export const usersPerDayChartConfig = {
|
||||
users: {
|
||||
label: "Users",
|
||||
color: "var(--color-blue-500)",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const deviceCategoryData = [
|
||||
{ name: "Desktop", value: 73.3, fill: "var(--color-blue-500)" },
|
||||
{ name: "Mobile", value: 25.0, fill: "var(--color-blue-400)" },
|
||||
{ name: "Tablet", value: 1.7, fill: "var(--color-blue-300)" },
|
||||
];
|
||||
|
||||
export const deviceCategoryChartConfig = {
|
||||
desktop: { label: "Desktop", color: "var(--color-blue-500)" },
|
||||
mobile: { label: "Mobile", color: "var(--color-blue-400)" },
|
||||
tablet: { label: "Tablet", color: "var(--color-blue-300)" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const visitedPagesRows: BreakdownRow[] = [
|
||||
{ label: "Home", value: 31 },
|
||||
{ label: "Pricing", value: 23 },
|
||||
{ label: "Docs / Basic Map", value: 18 },
|
||||
{ label: "Installation", value: 12 },
|
||||
{ label: "Components", value: 9 },
|
||||
{ label: "Blog", value: 6 },
|
||||
];
|
||||
|
||||
export const countriesRows: BreakdownRow[] = [
|
||||
{ label: "United States", value: 27 },
|
||||
{ label: "India", value: 14 },
|
||||
{ label: "United Kingdom", value: 8 },
|
||||
{ label: "Germany", value: 6 },
|
||||
{ label: "Japan", value: 4 },
|
||||
{ label: "Australia", value: 2 },
|
||||
];
|
||||
|
||||
export const referrersRows: BreakdownRow[] = [
|
||||
{ label: "google", value: 38 },
|
||||
{ label: "direct", value: 26 },
|
||||
{ label: "github.com", value: 19 },
|
||||
{ label: "x.com", value: 11 },
|
||||
{ label: "ui.shadcn.com", value: 8 },
|
||||
{ label: "other", value: 5 },
|
||||
];
|
||||
|
||||
export const browsersRows: BreakdownRow[] = [
|
||||
{ label: "Chrome", value: 52 },
|
||||
{ label: "Safari", value: 21 },
|
||||
{ label: "Firefox", value: 14 },
|
||||
{ label: "Edge", value: 8 },
|
||||
{ label: "Other", value: 5 },
|
||||
];
|
||||
83
src/registry/blocks/analytics-map/page.tsx
Normal file
83
src/registry/blocks/analytics-map/page.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Map,
|
||||
MapControls,
|
||||
MapMarker,
|
||||
MarkerContent,
|
||||
MarkerTooltip,
|
||||
} from "@/registry/map";
|
||||
import { OverviewCard } from "./components/overview-card";
|
||||
import { BreakdownCard } from "./components/breakdown-card";
|
||||
import {
|
||||
locations,
|
||||
visitedPagesRows,
|
||||
countriesRows,
|
||||
referrersRows,
|
||||
browsersRows,
|
||||
} from "./data";
|
||||
|
||||
const MAP_HEIGHT = "38rem";
|
||||
|
||||
export function AnalyticsMapBlock() {
|
||||
return (
|
||||
<div
|
||||
className="bg-background relative min-h-screen"
|
||||
style={{ "--map-height": MAP_HEIGHT } as React.CSSProperties}
|
||||
>
|
||||
<div className="relative h-(--map-height)">
|
||||
<Map
|
||||
center={[-2, 16]}
|
||||
zoom={1.5}
|
||||
scrollZoom={false}
|
||||
renderWorldCopies={true}
|
||||
>
|
||||
<MapControls showFullscreen />
|
||||
{locations.map((location) => (
|
||||
<MapMarker
|
||||
key={location.city}
|
||||
longitude={location.lng}
|
||||
latitude={location.lat}
|
||||
>
|
||||
<MarkerContent>
|
||||
<div
|
||||
className="rounded-full bg-blue-500/70"
|
||||
style={{
|
||||
width: location.size * 3,
|
||||
height: location.size * 3,
|
||||
}}
|
||||
/>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip
|
||||
offset={20}
|
||||
className="bg-background text-foreground border"
|
||||
>
|
||||
<p className="text-muted-foreground font-medium">
|
||||
{location.city}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<span className="font-medium tabular-nums">
|
||||
{location.size}
|
||||
</span>{" "}
|
||||
active users
|
||||
</p>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
))}
|
||||
</Map>
|
||||
<div
|
||||
className="via-background/30 to-background pointer-events-none absolute inset-x-0 bottom-0 h-40 bg-linear-to-b from-transparent"
|
||||
aria-hidden
|
||||
/>
|
||||
<OverviewCard />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 p-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<BreakdownCard title="Visited pages" rows={visitedPagesRows} />
|
||||
<BreakdownCard title="Referrers" rows={referrersRows} />
|
||||
<BreakdownCard title="Countries" rows={countriesRows} />
|
||||
<BreakdownCard title="Browsers" rows={browsersRows} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
261
src/registry/blocks/delivery-tracker/page.tsx
Normal file
261
src/registry/blocks/delivery-tracker/page.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Clock3, Utensils, Truck, UserRound } from "lucide-react";
|
||||
|
||||
import {
|
||||
Map,
|
||||
MapMarker,
|
||||
MapRoute,
|
||||
MarkerContent,
|
||||
MarkerTooltip,
|
||||
} from "@/registry/map";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
interface DeliveryMeal {
|
||||
name: string;
|
||||
price: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface OsrmRouteData {
|
||||
coordinates: [number, number][];
|
||||
duration: number;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
const deliveryMeals: DeliveryMeal[] = [
|
||||
{
|
||||
name: "Spicy Tofu Grain Bowl",
|
||||
price: "$44.00",
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
name: "Herb Chicken Rice Box",
|
||||
price: "$58.00",
|
||||
quantity: 2,
|
||||
},
|
||||
{
|
||||
name: "Roasted Veggie Wrap",
|
||||
price: "$29.00",
|
||||
quantity: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const pickup = { lng: -122.466, lat: 37.716 };
|
||||
const dropoff = { lng: -122.399, lat: 37.683 };
|
||||
|
||||
function formatDistance(meters?: number) {
|
||||
if (!meters) return "--";
|
||||
if (meters < 1000) return `${Math.round(meters)} m`;
|
||||
return `${(meters / 1000).toFixed(1)} km`;
|
||||
}
|
||||
|
||||
function formatDuration(seconds?: number) {
|
||||
if (!seconds) return "--";
|
||||
const minutes = Math.round(seconds / 60);
|
||||
if (minutes < 60) return `${minutes} min`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
export function DeliveryBlock() {
|
||||
const [routeData, setRouteData] = useState<OsrmRouteData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchRoute() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://router.project-osrm.org/route/v1/driving/${pickup.lng},${pickup.lat};${dropoff.lng},${dropoff.lat}?overview=full&geometries=geojson`
|
||||
);
|
||||
const data = await response.json();
|
||||
const route = data?.routes?.[0];
|
||||
if (!route?.geometry?.coordinates) return;
|
||||
|
||||
setRouteData({
|
||||
coordinates: route.geometry.coordinates as [number, number][],
|
||||
duration: route.duration as number,
|
||||
distance: route.distance as number,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch route:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchRoute();
|
||||
}, []);
|
||||
|
||||
const progressCoordinates = useMemo(() => {
|
||||
const progressCount = Math.max(
|
||||
2,
|
||||
Math.floor(
|
||||
(routeData?.coordinates?.length ?? 0) * (routeData ? 0.62 : 0.66)
|
||||
)
|
||||
);
|
||||
return routeData?.coordinates?.slice(0, progressCount) ?? [];
|
||||
}, [routeData]);
|
||||
|
||||
const courierPosition = progressCoordinates[progressCoordinates.length - 1];
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="grid md:grid-cols-[1.05fr_1fr] bg-sidebar rounded-lg border md:h-[600px] max-w-7xl mx-auto">
|
||||
<div className="p-5 md:p-6 flex flex-col">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-2xl font-semibold tracking-tight">
|
||||
Track Delivery
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">Mon Feb 10 - 2-3 PM</p>
|
||||
</div>
|
||||
|
||||
<Card className="mt-5">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-medium">
|
||||
Order items ({deliveryMeals.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
{deliveryMeals.map((meal) => (
|
||||
<div key={meal.name} className="flex items-center gap-3">
|
||||
<div className="grid size-8 place-items-center rounded-full bg-muted text-xs font-medium">
|
||||
<Utensils className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-4 flex-1">
|
||||
<p className="truncate text-sm font-medium pb-1">
|
||||
{meal.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{meal.price}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 rounded-full px-2.5"
|
||||
>
|
||||
x{meal.quantity}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center justify-between border-t border-border/60 pt-3 text-sm">
|
||||
<span className="text-muted-foreground">Bundle total</span>
|
||||
<span className="font-medium">$189.00</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<Card>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Pickup confirmed
|
||||
</p>
|
||||
<p className="text-sm font-medium">Mon, Feb 10 at 1:48 PM</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Remaining travel
|
||||
</p>
|
||||
<p className="text-sm font-medium">
|
||||
{formatDuration(routeData?.duration)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center gap-2">
|
||||
<Button size="sm" className="gap-1.5">
|
||||
<Clock3 className="size-4" />
|
||||
View timeline
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="gap-1.5">
|
||||
<UserRound className="size-4" />
|
||||
Contact courier
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-[400px] overflow-hidden rounded-xl md:h-full shadow-sm">
|
||||
<Map
|
||||
loading={loading}
|
||||
center={[-122.435, 37.696]}
|
||||
zoom={12}
|
||||
minZoom={10}
|
||||
maxZoom={16}
|
||||
styles={{
|
||||
light: "https://tiles.openfreemap.org/styles/bright",
|
||||
dark: "https://tiles.openfreemap.org/styles/dark",
|
||||
}}
|
||||
>
|
||||
<MapRoute
|
||||
id="delivery-full-route"
|
||||
coordinates={routeData?.coordinates ?? []}
|
||||
color="#5b6572"
|
||||
width={5.2}
|
||||
opacity={0.3}
|
||||
interactive={false}
|
||||
/>
|
||||
<MapRoute
|
||||
id="delivery-progress-route"
|
||||
coordinates={progressCoordinates}
|
||||
color="#3b82f6"
|
||||
width={6}
|
||||
opacity={0.95}
|
||||
interactive={false}
|
||||
/>
|
||||
|
||||
{courierPosition && (
|
||||
<MapMarker
|
||||
longitude={courierPosition[0]}
|
||||
latitude={courierPosition[1]}
|
||||
offset={[0, 10]}
|
||||
>
|
||||
<MarkerContent>
|
||||
<div className="relative grid size-9 place-items-center rounded-full bg-emerald-500 dark:bg-emerald-600">
|
||||
<Truck className="size-4 text-white" />
|
||||
</div>
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<div className="space-y-0.5 text-xs">
|
||||
<p className="font-medium">
|
||||
Order {formatDuration(routeData?.duration)} away
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Route {formatDistance(routeData?.distance)}
|
||||
</p>
|
||||
</div>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
)}
|
||||
|
||||
<MapMarker longitude={pickup.lng} latitude={pickup.lat}>
|
||||
<MarkerContent>
|
||||
<div className="size-4 rounded-full border-2 border-white bg-emerald-500 shadow-sm" />
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<p className="text-xs font-medium">Origin</p>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
|
||||
<MapMarker longitude={dropoff.lng} latitude={dropoff.lat}>
|
||||
<MarkerContent>
|
||||
<div className="size-4 rounded-full border-2 border-white bg-rose-500 shadow-sm" />
|
||||
</MarkerContent>
|
||||
<MarkerTooltip>
|
||||
<p className="text-xs font-medium">Destination</p>
|
||||
</MarkerTooltip>
|
||||
</MapMarker>
|
||||
</Map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user