mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 18:24:58 -05:00
feat: sync w/ main
This commit is contained in:
parent
0f7a5e6ffb
commit
a54a27bcec
@ -40,6 +40,7 @@
|
||||
"@unocss/transformer-directives": "^0.61.5",
|
||||
"@unocss/webpack": "^0.61.5",
|
||||
"@vercel/functions": "^2.0.0",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"ag-grid-react": "^33.0.3",
|
||||
"contentlayer": "^0.3.4",
|
||||
"cron": "^3.1.7",
|
||||
@ -60,6 +61,7 @@
|
||||
"next-themes": "^0.4.3",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nprogress": "^0.2.0",
|
||||
"nuqs": "^2.4.1",
|
||||
"postcss-obfuscator": "^1.6.1",
|
||||
"prettier": "^3.3.1",
|
||||
"react": "19.0.0",
|
||||
@ -73,6 +75,7 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sonner": "^1.7.0",
|
||||
"stripe-gradient": "^1.0.1",
|
||||
"swapy": "^1.0.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
BIN
apps/www/public/branding/bg-banner.png
Normal file
BIN
apps/www/public/branding/bg-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
89
apps/www/public/fonts/Inter-Bold.woff
Normal file
89
apps/www/public/fonts/Inter-Bold.woff
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'">
|
||||
<title>Page not found · GitHub Pages</title>
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
background-color: #f1f1f1;
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.container { margin: 50px auto 40px auto; width: 600px; text-align: center; }
|
||||
|
||||
a { color: #4183c4; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
h1 { width: 800px; position:relative; left: -100px; letter-spacing: -1px; line-height: 60px; font-size: 60px; font-weight: 100; margin: 0px 0 50px 0; text-shadow: 0 1px 0 #fff; }
|
||||
p { color: rgba(0, 0, 0, 0.5); margin: 20px 0; line-height: 1.6; }
|
||||
|
||||
ul { list-style: none; margin: 25px 0; padding: 0; }
|
||||
li { display: table-cell; font-weight: bold; width: 1%; }
|
||||
|
||||
.logo { display: inline-block; margin-top: 35px; }
|
||||
.logo-img-2x { display: none; }
|
||||
@media
|
||||
only screen and (-webkit-min-device-pixel-ratio: 2),
|
||||
only screen and ( min--moz-device-pixel-ratio: 2),
|
||||
only screen and ( -o-min-device-pixel-ratio: 2/1),
|
||||
only screen and ( min-device-pixel-ratio: 2),
|
||||
only screen and ( min-resolution: 192dpi),
|
||||
only screen and ( min-resolution: 2dppx) {
|
||||
.logo-img-1x { display: none; }
|
||||
.logo-img-2x { display: inline-block; }
|
||||
}
|
||||
|
||||
#suggestions {
|
||||
margin-top: 35px;
|
||||
color: #ccc;
|
||||
}
|
||||
#suggestions a {
|
||||
color: #666666;
|
||||
font-weight: 200;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>404</h1>
|
||||
<p><strong>File not found</strong></p>
|
||||
|
||||
<p>
|
||||
The site configured at this address does not
|
||||
contain the requested file.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If this is your site, make sure that the filename case matches the URL
|
||||
as well as any file permissions.<br>
|
||||
For root URLs (like <code>http://example.com/</code>) you must provide an
|
||||
<code>index.html</code> file.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://help.github.com/pages/">Read the full documentation</a>
|
||||
for more information about using <strong>GitHub Pages</strong>.
|
||||
</p>
|
||||
|
||||
<div id="suggestions">
|
||||
<a href="https://githubstatus.com">GitHub Status</a> —
|
||||
<a href="https://twitter.com/githubstatus">@githubstatus</a>
|
||||
</div>
|
||||
|
||||
<a href="/" class="logo logo-img-1x">
|
||||
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTZCRDY3REIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTZCRDY3RUIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdCQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjdDQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+SM9MCAAAA+5JREFUeNrEV11Ik1EY3s4+ddOp29Q5b0opCgKFsoKoi5Kg6CIhuwi6zLJLoYLopq4qsKKgi4i6CYIoU/q5iDAKs6syoS76IRWtyJ+p7cdt7sf1PGOD+e0c3dygAx/67ZzzPM95/877GYdHRg3ZjMXFxepQKNS6sLCwJxqNNuFpiMfjVs4ZjUa/pmmjeD6VlJS8NpvNT4QQ7mxwjSsJiEQim/1+/9lgMHgIr5ohuxG1WCw9Vqv1clFR0dCqBODElV6v90ogEDjGdYbVjXhpaendioqK07CIR7ZAqE49PT09BPL2PMgTByQGsYiZlQD4uMXtdr+JxWINhgINYhGT2MsKgMrm2dnZXgRXhaHAg5jEJodUAHxux4LudHJE9RdEdA+i3Juz7bGHe4mhE9FNrgwBCLirMFV9Okh5eflFh8PR5nK5nDabrR2BNJlKO0T35+Li4n4+/J+/JQCxhmu5h3uJoXNHPbmWZAHMshWB8l5/ipqammaAf0zPDDx1ONV3vurdidqwAQL+pEc8sLcAe1CCvQ3YHxIW8Pl85xSWNC1hADDIv0rIE/o4J0k3kww4xSlwIhcq3EFFOm7KN/hUGOQkt0CFa5WpNJlMvxBEz/IVQAxg/ZRZl9wiHA63yDYieM7DnLP5CiAGsC7I5sgtYKJGWe2A8seFqgFJrJjEPY1Cn3pJ8/9W1e5VWsFDTEmFrBcoDhZJEQkXuhICMyKpjhahqN21hRYATKfUOlDmkygrR4o4C0VOLGJKrOITKB4jijzdXygBKixyC5TDQdnk/Pz8qRw6oOWGlsTKGOQW6OH6FBWsyePxdOXLTgxiyebILZCjz+GLgMIKnXNzc49YMlcRdHXcSwxFVgTInQhC9G33UhNoJLuqq6t345p9y3eUy8OTk5PjAHuI9uo4b07FBaOhsu0A4Unc+T1TU1Nj3KsSSE5yJ65jqF2DDd8QqWYmAZrIM2VlZTdnZmb6AbpdV9V6ec9znf5Q7HjYumdRE0JOp3MjitO4SFa+cZz8Umqe3TCbSLvdfkR/kWDdNQl5InuTcysOcpFT35ZrbBxx4p3JAHlZVVW1D/634VRt+FvLBgK/v5LV9WS+10xMTEwtRw7XvqOL+e2Q8V3AYIOIAXQ26/heWVnZCVfcyKHg2CBgTpmPmjYM8l24GyaUHyaIh7XwfR9ErE8qHoDfn2LTNAVC0HX6MFcBIP8Bi+6F6cdW/DICkANRfx99fEYFQ7Nph5i/uQiA214gno7K+guhaiKg9gC62+M8eR7XsBsYJ4ilam60Fb7r7uAj8wFyuwM1oIOWgfmDy6RXEEQzJMPe23DXrVS7rtyD3Df8z/FPgAEAzWU5Ku59ZAUAAAAASUVORK5CYII=">
|
||||
</a>
|
||||
|
||||
<a href="/" class="logo logo-img-2x">
|
||||
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEQUM1QkUxRUI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEQUM1QkUxRkI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdGQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjgwQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+hfPRaQAAB6lJREFUeNrsW2mM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">
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
89
apps/www/public/fonts/Inter-Medium.woff
Normal file
89
apps/www/public/fonts/Inter-Medium.woff
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'">
|
||||
<title>Page not found · GitHub Pages</title>
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
background-color: #f1f1f1;
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.container { margin: 50px auto 40px auto; width: 600px; text-align: center; }
|
||||
|
||||
a { color: #4183c4; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
h1 { width: 800px; position:relative; left: -100px; letter-spacing: -1px; line-height: 60px; font-size: 60px; font-weight: 100; margin: 0px 0 50px 0; text-shadow: 0 1px 0 #fff; }
|
||||
p { color: rgba(0, 0, 0, 0.5); margin: 20px 0; line-height: 1.6; }
|
||||
|
||||
ul { list-style: none; margin: 25px 0; padding: 0; }
|
||||
li { display: table-cell; font-weight: bold; width: 1%; }
|
||||
|
||||
.logo { display: inline-block; margin-top: 35px; }
|
||||
.logo-img-2x { display: none; }
|
||||
@media
|
||||
only screen and (-webkit-min-device-pixel-ratio: 2),
|
||||
only screen and ( min--moz-device-pixel-ratio: 2),
|
||||
only screen and ( -o-min-device-pixel-ratio: 2/1),
|
||||
only screen and ( min-device-pixel-ratio: 2),
|
||||
only screen and ( min-resolution: 192dpi),
|
||||
only screen and ( min-resolution: 2dppx) {
|
||||
.logo-img-1x { display: none; }
|
||||
.logo-img-2x { display: inline-block; }
|
||||
}
|
||||
|
||||
#suggestions {
|
||||
margin-top: 35px;
|
||||
color: #ccc;
|
||||
}
|
||||
#suggestions a {
|
||||
color: #666666;
|
||||
font-weight: 200;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>404</h1>
|
||||
<p><strong>File not found</strong></p>
|
||||
|
||||
<p>
|
||||
The site configured at this address does not
|
||||
contain the requested file.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If this is your site, make sure that the filename case matches the URL
|
||||
as well as any file permissions.<br>
|
||||
For root URLs (like <code>http://example.com/</code>) you must provide an
|
||||
<code>index.html</code> file.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://help.github.com/pages/">Read the full documentation</a>
|
||||
for more information about using <strong>GitHub Pages</strong>.
|
||||
</p>
|
||||
|
||||
<div id="suggestions">
|
||||
<a href="https://githubstatus.com">GitHub Status</a> —
|
||||
<a href="https://twitter.com/githubstatus">@githubstatus</a>
|
||||
</div>
|
||||
|
||||
<a href="/" class="logo logo-img-1x">
|
||||
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTZCRDY3REIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTZCRDY3RUIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdCQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjdDQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+SM9MCAAAA+5JREFUeNrEV11Ik1EY3s4+ddOp29Q5b0opCgKFsoKoi5Kg6CIhuwi6zLJLoYLopq4qsKKgi4i6CYIoU/q5iDAKs6syoS76IRWtyJ+p7cdt7sf1PGOD+e0c3dygAx/67ZzzPM95/877GYdHRg3ZjMXFxepQKNS6sLCwJxqNNuFpiMfjVs4ZjUa/pmmjeD6VlJS8NpvNT4QQ7mxwjSsJiEQim/1+/9lgMHgIr5ohuxG1WCw9Vqv1clFR0dCqBODElV6v90ogEDjGdYbVjXhpaendioqK07CIR7ZAqE49PT09BPL2PMgTByQGsYiZlQD4uMXtdr+JxWINhgINYhGT2MsKgMrm2dnZXgRXhaHAg5jEJodUAHxux4LudHJE9RdEdA+i3Juz7bGHe4mhE9FNrgwBCLirMFV9Okh5eflFh8PR5nK5nDabrR2BNJlKO0T35+Li4n4+/J+/JQCxhmu5h3uJoXNHPbmWZAHMshWB8l5/ipqammaAf0zPDDx1ONV3vurdidqwAQL+pEc8sLcAe1CCvQ3YHxIW8Pl85xSWNC1hADDIv0rIE/o4J0k3kww4xSlwIhcq3EFFOm7KN/hUGOQkt0CFa5WpNJlMvxBEz/IVQAxg/ZRZl9wiHA63yDYieM7DnLP5CiAGsC7I5sgtYKJGWe2A8seFqgFJrJjEPY1Cn3pJ8/9W1e5VWsFDTEmFrBcoDhZJEQkXuhICMyKpjhahqN21hRYATKfUOlDmkygrR4o4C0VOLGJKrOITKB4jijzdXygBKixyC5TDQdnk/Pz8qRw6oOWGlsTKGOQW6OH6FBWsyePxdOXLTgxiyebILZCjz+GLgMIKnXNzc49YMlcRdHXcSwxFVgTInQhC9G33UhNoJLuqq6t345p9y3eUy8OTk5PjAHuI9uo4b07FBaOhsu0A4Unc+T1TU1Nj3KsSSE5yJ65jqF2DDd8QqWYmAZrIM2VlZTdnZmb6AbpdV9V6ec9znf5Q7HjYumdRE0JOp3MjitO4SFa+cZz8Umqe3TCbSLvdfkR/kWDdNQl5InuTcysOcpFT35ZrbBxx4p3JAHlZVVW1D/634VRt+FvLBgK/v5LV9WS+10xMTEwtRw7XvqOL+e2Q8V3AYIOIAXQ26/heWVnZCVfcyKHg2CBgTpmPmjYM8l24GyaUHyaIh7XwfR9ErE8qHoDfn2LTNAVC0HX6MFcBIP8Bi+6F6cdW/DICkANRfx99fEYFQ7Nph5i/uQiA214gno7K+guhaiKg9gC62+M8eR7XsBsYJ4ilam60Fb7r7uAj8wFyuwM1oIOWgfmDy6RXEEQzJMPe23DXrVS7rtyD3Df8z/FPgAEAzWU5Ku59ZAUAAAAASUVORK5CYII=">
|
||||
</a>
|
||||
|
||||
<a href="/" class="logo logo-img-2x">
|
||||
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEQUM1QkUxRUI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEQUM1QkUxRkI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdGQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjgwQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+hfPRaQAAB6lJREFUeNrsW2mM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">
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
89
apps/www/public/fonts/Inter-Regular.woff
Normal file
89
apps/www/public/fonts/Inter-Regular.woff
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'">
|
||||
<title>Page not found · GitHub Pages</title>
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
background-color: #f1f1f1;
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.container { margin: 50px auto 40px auto; width: 600px; text-align: center; }
|
||||
|
||||
a { color: #4183c4; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
h1 { width: 800px; position:relative; left: -100px; letter-spacing: -1px; line-height: 60px; font-size: 60px; font-weight: 100; margin: 0px 0 50px 0; text-shadow: 0 1px 0 #fff; }
|
||||
p { color: rgba(0, 0, 0, 0.5); margin: 20px 0; line-height: 1.6; }
|
||||
|
||||
ul { list-style: none; margin: 25px 0; padding: 0; }
|
||||
li { display: table-cell; font-weight: bold; width: 1%; }
|
||||
|
||||
.logo { display: inline-block; margin-top: 35px; }
|
||||
.logo-img-2x { display: none; }
|
||||
@media
|
||||
only screen and (-webkit-min-device-pixel-ratio: 2),
|
||||
only screen and ( min--moz-device-pixel-ratio: 2),
|
||||
only screen and ( -o-min-device-pixel-ratio: 2/1),
|
||||
only screen and ( min-device-pixel-ratio: 2),
|
||||
only screen and ( min-resolution: 192dpi),
|
||||
only screen and ( min-resolution: 2dppx) {
|
||||
.logo-img-1x { display: none; }
|
||||
.logo-img-2x { display: inline-block; }
|
||||
}
|
||||
|
||||
#suggestions {
|
||||
margin-top: 35px;
|
||||
color: #ccc;
|
||||
}
|
||||
#suggestions a {
|
||||
color: #666666;
|
||||
font-weight: 200;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>404</h1>
|
||||
<p><strong>File not found</strong></p>
|
||||
|
||||
<p>
|
||||
The site configured at this address does not
|
||||
contain the requested file.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If this is your site, make sure that the filename case matches the URL
|
||||
as well as any file permissions.<br>
|
||||
For root URLs (like <code>http://example.com/</code>) you must provide an
|
||||
<code>index.html</code> file.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://help.github.com/pages/">Read the full documentation</a>
|
||||
for more information about using <strong>GitHub Pages</strong>.
|
||||
</p>
|
||||
|
||||
<div id="suggestions">
|
||||
<a href="https://githubstatus.com">GitHub Status</a> —
|
||||
<a href="https://twitter.com/githubstatus">@githubstatus</a>
|
||||
</div>
|
||||
|
||||
<a href="/" class="logo logo-img-1x">
|
||||
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMTZCRDY3REIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMTZCRDY3RUIzRjAxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdCQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjdDQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+SM9MCAAAA+5JREFUeNrEV11Ik1EY3s4+ddOp29Q5b0opCgKFsoKoi5Kg6CIhuwi6zLJLoYLopq4qsKKgi4i6CYIoU/q5iDAKs6syoS76IRWtyJ+p7cdt7sf1PGOD+e0c3dygAx/67ZzzPM95/877GYdHRg3ZjMXFxepQKNS6sLCwJxqNNuFpiMfjVs4ZjUa/pmmjeD6VlJS8NpvNT4QQ7mxwjSsJiEQim/1+/9lgMHgIr5ohuxG1WCw9Vqv1clFR0dCqBODElV6v90ogEDjGdYbVjXhpaendioqK07CIR7ZAqE49PT09BPL2PMgTByQGsYiZlQD4uMXtdr+JxWINhgINYhGT2MsKgMrm2dnZXgRXhaHAg5jEJodUAHxux4LudHJE9RdEdA+i3Juz7bGHe4mhE9FNrgwBCLirMFV9Okh5eflFh8PR5nK5nDabrR2BNJlKO0T35+Li4n4+/J+/JQCxhmu5h3uJoXNHPbmWZAHMshWB8l5/ipqammaAf0zPDDx1ONV3vurdidqwAQL+pEc8sLcAe1CCvQ3YHxIW8Pl85xSWNC1hADDIv0rIE/o4J0k3kww4xSlwIhcq3EFFOm7KN/hUGOQkt0CFa5WpNJlMvxBEz/IVQAxg/ZRZl9wiHA63yDYieM7DnLP5CiAGsC7I5sgtYKJGWe2A8seFqgFJrJjEPY1Cn3pJ8/9W1e5VWsFDTEmFrBcoDhZJEQkXuhICMyKpjhahqN21hRYATKfUOlDmkygrR4o4C0VOLGJKrOITKB4jijzdXygBKixyC5TDQdnk/Pz8qRw6oOWGlsTKGOQW6OH6FBWsyePxdOXLTgxiyebILZCjz+GLgMIKnXNzc49YMlcRdHXcSwxFVgTInQhC9G33UhNoJLuqq6t345p9y3eUy8OTk5PjAHuI9uo4b07FBaOhsu0A4Unc+T1TU1Nj3KsSSE5yJ65jqF2DDd8QqWYmAZrIM2VlZTdnZmb6AbpdV9V6ec9znf5Q7HjYumdRE0JOp3MjitO4SFa+cZz8Umqe3TCbSLvdfkR/kWDdNQl5InuTcysOcpFT35ZrbBxx4p3JAHlZVVW1D/634VRt+FvLBgK/v5LV9WS+10xMTEwtRw7XvqOL+e2Q8V3AYIOIAXQ26/heWVnZCVfcyKHg2CBgTpmPmjYM8l24GyaUHyaIh7XwfR9ErE8qHoDfn2LTNAVC0HX6MFcBIP8Bi+6F6cdW/DICkANRfx99fEYFQ7Nph5i/uQiA214gno7K+guhaiKg9gC62+M8eR7XsBsYJ4ilam60Fb7r7uAj8wFyuwM1oIOWgfmDy6RXEEQzJMPe23DXrVS7rtyD3Df8z/FPgAEAzWU5Ku59ZAUAAAAASUVORK5CYII=">
|
||||
</a>
|
||||
|
||||
<a href="/" class="logo logo-img-2x">
|
||||
<img width="32" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEQUM1QkUxRUI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEQUM1QkUxRkI0MUMxMUUyQUQzREIxQzRENUFFNUM5NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUxNkJENjdGQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUxNkJENjgwQjNGMDExRTJBRDNEQjFDNEQ1QUU1Qzk2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+hfPRaQAAB6lJREFUeNrsW2mM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">
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -43,6 +43,7 @@ import { FontBoundary } from "@/components/util/font-boundary";
|
||||
import { ClerkProvider } from "@/components/util/clerk-provider";
|
||||
import { Toaster } from "sonner";
|
||||
import { Footer } from "@/components/feat/footer/footer";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@ -75,16 +76,18 @@ export default function RootLayout({
|
||||
>
|
||||
<ClerkProvider>
|
||||
<IsScript>
|
||||
<FontBoundary>
|
||||
<TooltipProvider>
|
||||
<Toaster richColors position="top-center" />
|
||||
<ClerkProvider>
|
||||
<NavBar />
|
||||
<div className="pt-16 min-h-screen">{children}</div>
|
||||
<Footer />
|
||||
</ClerkProvider>
|
||||
</TooltipProvider>
|
||||
</FontBoundary>
|
||||
<NuqsAdapter>
|
||||
<FontBoundary>
|
||||
<TooltipProvider>
|
||||
<Toaster richColors position="top-center" />
|
||||
<ClerkProvider>
|
||||
<NavBar />
|
||||
<div className="pt-16 min-h-screen">{children}</div>
|
||||
<Footer />
|
||||
</ClerkProvider>
|
||||
</TooltipProvider>
|
||||
</FontBoundary>
|
||||
</NuqsAdapter>
|
||||
</IsScript>
|
||||
</ClerkProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { ServerProvider } from "@/components/feat/server-page/server-provider";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@ -7,19 +9,67 @@ export async function generateMetadata({
|
||||
params: Promise<{ server: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const id = (await params).server;
|
||||
const { server }: { server: ServerResponse } = await (
|
||||
const { server }: { server: ServerResponse | undefined } = await (
|
||||
await fetch("https://api.minehut.com/server/" + id)
|
||||
).json();
|
||||
|
||||
if (server === null) return notFound();
|
||||
|
||||
// Default fallback values
|
||||
const defaultName = "Server not found";
|
||||
const defaultDescription = "A server on Minehut, find it on MHSF!";
|
||||
|
||||
// Get server name or use fallback
|
||||
const serverName = server?.name || defaultName;
|
||||
|
||||
// Generate the absolute URL for the OG image
|
||||
const ogImageUrl = new URL(
|
||||
`/api/og/server/${id}`,
|
||||
process.env.NEXT_PUBLIC_APP_URL || "https://mhsf.app"
|
||||
).toString();
|
||||
|
||||
return {
|
||||
applicationName: "MHSF",
|
||||
title: `${server.name} | MHSF`,
|
||||
title: `${serverName} | MHSF`,
|
||||
openGraph: {
|
||||
title: server.name,
|
||||
description: "A server on Minehut, find it on MHSF!",
|
||||
title: serverName,
|
||||
description: defaultDescription,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${serverName} server statistics`,
|
||||
},
|
||||
],
|
||||
},
|
||||
description: "A server on Minehut, find it on MHSF!",
|
||||
};
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: serverName,
|
||||
description: defaultDescription,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${serverName} server statistics`,
|
||||
},
|
||||
],
|
||||
},
|
||||
description: defaultDescription,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ServerPage() {}
|
||||
export default async function ServerPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ server: string }>;
|
||||
}) {
|
||||
const slug = (await params).server;
|
||||
|
||||
return (
|
||||
<main>
|
||||
<ServerProvider serverId={slug} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
apps/www/src/app/api/og/fonts/Inter-Bold.ttf
Normal file
BIN
apps/www/src/app/api/og/fonts/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
apps/www/src/app/api/og/fonts/Inter-Medium.ttf
Normal file
BIN
apps/www/src/app/api/og/fonts/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
apps/www/src/app/api/og/fonts/Inter-Regular.ttf
Normal file
BIN
apps/www/src/app/api/og/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
16
apps/www/src/app/api/og/fonts/index.ts
Normal file
16
apps/www/src/app/api/og/fonts/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// For Edge runtime, we need to use fetch instead of fs
|
||||
export async function loadFonts() {
|
||||
const interRegularFontP = fetch(
|
||||
new URL("./Inter-Regular.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
const interMediumFontP = fetch(
|
||||
new URL("./Inter-Medium.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
const interBoldFontP = fetch(
|
||||
new URL("./Inter-Bold.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
return Promise.all([interRegularFontP, interMediumFontP, interBoldFontP]);
|
||||
}
|
||||
486
apps/www/src/app/api/og/server/[id]/route.tsx
Normal file
486
apps/www/src/app/api/og/server/[id]/route.tsx
Normal file
@ -0,0 +1,486 @@
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { NextRequest } from "next/server";
|
||||
import { miniMessage } from "minimessage-js";
|
||||
import { loadFonts } from "../../fonts";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
// Function to parse MiniMessage and create JSX elements with styling
|
||||
function parseMotdToJsx(text: string) {
|
||||
try {
|
||||
// First convert to HTML
|
||||
const htmlContent = miniMessage().toHTML(miniMessage().deserialize(text));
|
||||
|
||||
// Simple parsing of the HTML to extract basic formatting
|
||||
// This is a simplified approach that handles common formatting
|
||||
const parts = [];
|
||||
let currentText = "";
|
||||
let inBold = false;
|
||||
let inItalic = false;
|
||||
let inUnderline = false;
|
||||
let currentColor = "white";
|
||||
let partIndex = 0;
|
||||
|
||||
// Create a simple parser for the HTML content
|
||||
let i = 0;
|
||||
while (i < htmlContent.length) {
|
||||
// Handle opening tags
|
||||
if (htmlContent[i] === "<") {
|
||||
// Add current text if any
|
||||
if (currentText) {
|
||||
parts.push(
|
||||
<span
|
||||
key={`part-${partIndex++}`}
|
||||
style={{
|
||||
color: currentColor,
|
||||
fontWeight: inBold ? "bold" : "normal",
|
||||
fontStyle: inItalic ? "italic" : "normal",
|
||||
textDecoration: inUnderline ? "underline" : "none",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{currentText}
|
||||
</span>
|
||||
);
|
||||
currentText = "";
|
||||
}
|
||||
|
||||
// Find the end of the tag
|
||||
const tagEnd = htmlContent.indexOf(">", i);
|
||||
if (tagEnd === -1) break;
|
||||
|
||||
const tag = htmlContent.substring(i, tagEnd + 1);
|
||||
|
||||
// Handle specific tags
|
||||
if (tag.includes('span style="font-weight: bold"') || tag === "<b>") {
|
||||
inBold = true;
|
||||
} else if (tag === "</b>" || (tag === "</span>" && inBold)) {
|
||||
inBold = false;
|
||||
} else if (
|
||||
tag.includes('span style="font-style: italic"') ||
|
||||
tag === "<i>"
|
||||
) {
|
||||
inItalic = true;
|
||||
} else if (tag === "</i>" || (tag === "</span>" && inItalic)) {
|
||||
inItalic = false;
|
||||
} else if (
|
||||
tag.includes('span style="text-decoration: underline"') ||
|
||||
tag === "<u>"
|
||||
) {
|
||||
inUnderline = true;
|
||||
} else if (tag === "</u>" || (tag === "</span>" && inUnderline)) {
|
||||
inUnderline = false;
|
||||
} else if (tag.includes('span style="color:')) {
|
||||
// Extract color
|
||||
const colorMatch = tag.match(/color:\s*([^;"]+)/);
|
||||
if (colorMatch?.[1]) {
|
||||
currentColor = colorMatch[1];
|
||||
}
|
||||
} else if (tag === "</span>" && currentColor !== "white") {
|
||||
currentColor = "white";
|
||||
}
|
||||
|
||||
i = tagEnd + 1;
|
||||
} else {
|
||||
currentText += htmlContent[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text
|
||||
if (currentText) {
|
||||
parts.push(
|
||||
<span
|
||||
key={`part-${partIndex++}`}
|
||||
style={{
|
||||
color: currentColor,
|
||||
fontWeight: inBold ? "bold" : "normal",
|
||||
fontStyle: inItalic ? "italic" : "normal",
|
||||
textDecoration: inUnderline ? "underline" : "none",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{currentText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return parts.length > 0
|
||||
? parts
|
||||
: [
|
||||
<span key="empty" style={{ display: "flex" }}>
|
||||
No description available
|
||||
</span>,
|
||||
];
|
||||
} catch (error) {
|
||||
console.error("Error parsing MOTD:", error);
|
||||
return [
|
||||
<span key="error" style={{ display: "flex" }}>
|
||||
No description available
|
||||
</span>,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
// Load banner image
|
||||
const bannerImageData = await fetch(
|
||||
new URL("/branding/bg-banner.png", request.url)
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
const id = (await params).id;
|
||||
|
||||
// Load fonts
|
||||
const [interRegular, interMedium, interBold] = await loadFonts();
|
||||
|
||||
// Fetch server data
|
||||
const response = await fetch(`https://api.minehut.com/server/${id}`);
|
||||
const { server }: { server: ServerResponse | undefined } =
|
||||
await response.json();
|
||||
|
||||
if (!server) {
|
||||
// Return a default banner for server not found
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 60,
|
||||
color: "white",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "50px 50px",
|
||||
textAlign: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundImage: `url(data:image/png;base64,${Buffer.from(bannerImageData).toString("base64")})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
fontFamily: "Inter",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>Server not found</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
data: interRegular,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Format player count
|
||||
const playerCount = server.playerCount || 0;
|
||||
const maxPlayers = server.maxPlayers || 0;
|
||||
|
||||
// Format last online date
|
||||
const lastOnline = server.last_online
|
||||
? new Date(server.last_online).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
: "Unknown";
|
||||
|
||||
// Format creation date
|
||||
const creationDate = server.creation
|
||||
? new Date(server.creation).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
: "Unknown";
|
||||
|
||||
// Determine status color
|
||||
const statusColor = server.online ? "#4ade80" : "#ef4444";
|
||||
|
||||
// Parse MOTD to JSX with formatting
|
||||
const motdElements = parseMotdToJsx(
|
||||
server.motd || "No description available"
|
||||
);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "50px 50px",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
fontFamily: "Inter",
|
||||
backgroundImage: `url(data:image/png;base64,${Buffer.from(bannerImageData).toString("base64")})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
{/* Decorative elements */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-100px",
|
||||
right: "-100px",
|
||||
width: "400px",
|
||||
height: "400px",
|
||||
borderRadius: "50%",
|
||||
background: "rgba(79, 70, 229, 0.1)",
|
||||
filter: "blur(40px)",
|
||||
display: "flex",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "-100px",
|
||||
left: "-100px",
|
||||
width: "300px",
|
||||
height: "300px",
|
||||
borderRadius: "50%",
|
||||
background: "rgba(236, 72, 153, 0.1)",
|
||||
filter: "blur(40px)",
|
||||
display: "flex",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header with server name and status */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "50%",
|
||||
background: statusColor,
|
||||
marginRight: "15px",
|
||||
display: "flex",
|
||||
}}
|
||||
/>
|
||||
<h1 style={{ fontSize: 40, fontWeight: "bold", margin: 0 }}>
|
||||
{server.name}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* MOTD with formatting */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
marginBottom: "30px",
|
||||
opacity: 0.9,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
{motdElements}
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "30px",
|
||||
marginTop: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 24, opacity: 0.7, display: "flex" }}>
|
||||
<span>Players</span>
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: 36, fontWeight: "bold", display: "flex" }}
|
||||
>
|
||||
<span>
|
||||
{playerCount}/{maxPlayers}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 24, opacity: 0.7, display: "flex" }}>
|
||||
<span>Plan</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: "bold",
|
||||
display: "flex",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{server.server_plan.toLocaleLowerCase().split("_")[0] ||
|
||||
"Unknown"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 24, opacity: 0.7, display: "flex" }}>
|
||||
<span>Created</span>
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: 36, fontWeight: "bold", display: "flex" }}
|
||||
>
|
||||
<span>{creationDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 24, opacity: 0.7, display: "flex" }}>
|
||||
<span>Last Online</span>
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: 36, fontWeight: "bold", display: "flex" }}
|
||||
>
|
||||
<span>{lastOnline}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: "40px",
|
||||
fontSize: 24,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>mhsf.app</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
data: interRegular,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: interBold,
|
||||
style: "normal",
|
||||
weight: 700,
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: interMedium,
|
||||
style: "normal",
|
||||
weight: 500,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const [interRegular, interMedium, interBold] = await loadFonts();
|
||||
console.error("Error generating OG image:", error);
|
||||
|
||||
// Try to load the banner image again in case it failed earlier
|
||||
let bannerImageData: ArrayBuffer | undefined;
|
||||
try {
|
||||
bannerImageData = await fetch(
|
||||
new URL("/branding/dark-banner.png", request.url)
|
||||
).then((res) => res.arrayBuffer());
|
||||
} catch (e) {
|
||||
// If banner image fails to load, use a solid color background
|
||||
console.error("Failed to load banner image for error page:", e);
|
||||
}
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 60,
|
||||
color: "white",
|
||||
background: bannerImageData ? undefined : "#121212",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "50px 50px",
|
||||
textAlign: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontFamily: "Inter",
|
||||
...(bannerImageData && {
|
||||
backgroundImage: `url(data:image/png;base64,${Buffer.from(bannerImageData).toString("base64")})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>Error generating image</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
data: interRegular,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,99 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
/* Workaround for Tailwind being stupid */
|
||||
border-color: hsl(214.3 31.8% 91.4%);
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--border: 216 34% 17%;
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
@apply border-zinc-800;
|
||||
}
|
||||
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--animate-spin: spin 1s linear infinite;
|
||||
--animate-scale-in: scaleIn 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
@ -171,101 +264,6 @@
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
/* Workaround for Tailwind being stupid */
|
||||
border-color: hsl(214.3 31.8% 91.4%);
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--border: 216 34% 17%;
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
@apply border-zinc-800;
|
||||
}
|
||||
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
body {
|
||||
@apply bg-slate-50 text-slate-900 dark:bg-zinc-950 dark:text-zinc-100 accent-slate-950 dark:accent-zinc-50;
|
||||
|
||||
@ -47,6 +47,7 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<noscript>{children}</noscript>
|
||||
<script src="https://unpkg.com/react-scan/dist/auto.global.js" />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -8,10 +8,25 @@ import { cn } from "@/lib/utils";
|
||||
export function FooterStatus() {
|
||||
const { loading, incidents, statusURL } = useStatus();
|
||||
|
||||
const determineIfOutage = () => {
|
||||
return (
|
||||
!loading && incidents !== null && (incidents as Array<any>).length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const determineWhatOutage = () => {
|
||||
if (incidents !== null)
|
||||
return {
|
||||
name: (incidents as Array<any>)[0].attributes.name,
|
||||
id: (incidents as Array<any>)[0].id,
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!loading)
|
||||
return (
|
||||
<Link
|
||||
href={`https://${statusURL as string}`}
|
||||
href={`https://${statusURL as string}${determineIfOutage() ? `/incident/${determineWhatOutage()?.id}` : ""}`}
|
||||
noExtraIcons
|
||||
target="_blank"
|
||||
>
|
||||
@ -19,18 +34,25 @@ export function FooterStatus() {
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm flex items-center gap-2 font-normal",
|
||||
"text-blue-600"
|
||||
determineIfOutage() ? "text-orange-400" : "text-blue-600"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="items-center bg-blue-600 dark:bg-blue-600"
|
||||
className={cn(
|
||||
"items-center",
|
||||
determineIfOutage()
|
||||
? "bg-orange-400"
|
||||
: "bg-blue-600 dark:bg-blue-600"
|
||||
)}
|
||||
style={{
|
||||
width: ".5rem",
|
||||
height: ".5rem",
|
||||
borderRadius: "9999px",
|
||||
}}
|
||||
/>
|
||||
All systems normal
|
||||
{determineIfOutage()
|
||||
? determineWhatOutage()?.name
|
||||
: "All systems normal"}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -39,7 +39,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||
import { allTags } from "@/config/tags";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
@ -52,25 +52,21 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Copy } from "lucide-react";
|
||||
import useClipboard from "@/lib/useClipboard";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { MOTDRenderer } from "../server-page/motd/motd-renderer";
|
||||
|
||||
export default function ServerCard({
|
||||
server,
|
||||
motd,
|
||||
}: {
|
||||
server: OnlineServer;
|
||||
motd: string | undefined;
|
||||
}) {
|
||||
export default function ServerCard({ server }: { server: OnlineServer }) {
|
||||
const clipboard = useClipboard();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Material
|
||||
key={server.name}
|
||||
className="min-h-[250px] max-h-[250px] cursor-pointer outline-0 group hover:drop-shadow-card-hover focus:drop-shadow-card-hover transition-all"
|
||||
onClick={() => toast.success("pluh")}
|
||||
onClick={() => router.push(`/server/${server.staticInfo._id}`)}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
// Only send user when they hit "Enter"
|
||||
if (e.key === "Enter") toast.success("keyboard");
|
||||
if (e.key === "Enter") router.push(`/server/${server.staticInfo._id}`);
|
||||
}}
|
||||
>
|
||||
<span className="text-sm hidden group-focus-visible:block text-muted-foreground mb-2">
|
||||
@ -132,12 +128,14 @@ export default function ServerCard({
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TagShower server={server} className="mt-1" />
|
||||
{motd && (
|
||||
<span
|
||||
<TagShower server={server} className="mt-1 " />
|
||||
{server.motd && (
|
||||
<MOTDRenderer
|
||||
className="block break-all overflow-hidden mt-3"
|
||||
dangerouslySetInnerHTML={{ __html: motd }}
|
||||
/>
|
||||
minecraftFont
|
||||
>
|
||||
{server.motd}
|
||||
</MOTDRenderer>
|
||||
)}
|
||||
</Material>
|
||||
);
|
||||
@ -157,7 +155,8 @@ export type BadgeColor =
|
||||
| "gray-subtle"
|
||||
| "blue-subtle"
|
||||
| "purple-subtle"
|
||||
| "custom";
|
||||
| "custom"
|
||||
| "rainbow";
|
||||
|
||||
export function TagShower(props: {
|
||||
server: OnlineServer;
|
||||
@ -179,7 +178,7 @@ export function TagShower(props: {
|
||||
if (loading) {
|
||||
allTags.forEach((tag) => {
|
||||
if (!tag.condition) {
|
||||
tag.name(props.server).then((n) => {
|
||||
tag.name({ online: props.server }).then((n) => {
|
||||
compatiableTags.push({
|
||||
name: n,
|
||||
docsName: tag.docsName,
|
||||
@ -190,9 +189,9 @@ export function TagShower(props: {
|
||||
setLoading(false);
|
||||
});
|
||||
} else
|
||||
tag.condition(props.server).then((b) => {
|
||||
if (b && tag.primary) {
|
||||
tag.name(props.server).then((n) => {
|
||||
tag.condition({ online: props.server }).then((b) => {
|
||||
if (b) {
|
||||
tag.name({ online: props.server }).then((n) => {
|
||||
compatiableTags.push({
|
||||
name: n,
|
||||
docsName: tag.docsName,
|
||||
|
||||
@ -36,33 +36,11 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Statistics } from "./statistics";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
|
||||
import { useEffect, useState } from "react";
|
||||
import MiniMessage from "minimessage-js";
|
||||
|
||||
export function ServerList() {
|
||||
const { servers, loading, serverCount, playerCount } = useServers();
|
||||
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||
useInfiniteScrolling(servers);
|
||||
const [motdList, setMotdList] = useState<{ name: string; motd: string }[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const result: Array<{ name: string; motd: string }> = [];
|
||||
const mm = MiniMessage.miniMessage();
|
||||
servers.forEach((c) => {
|
||||
try {
|
||||
result.push({
|
||||
name: c.name,
|
||||
motd: mm.toHTML(mm.deserialize(c.motd)),
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
setMotdList(result);
|
||||
}, [servers]);
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
@ -97,11 +75,7 @@ export function ServerList() {
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mt-3">
|
||||
{data.map((c) => (
|
||||
<ServerCard
|
||||
server={c}
|
||||
key={c.name}
|
||||
motd={motdList.find((x) => x.name === c.name)?.motd}
|
||||
/>
|
||||
<ServerCard server={c} key={c.staticInfo._id} />
|
||||
))}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
|
||||
BIN
apps/www/src/components/feat/server-page/motd/motd-font.ttf
Normal file
BIN
apps/www/src/components/feat/server-page/motd/motd-font.ttf
Normal file
Binary file not shown.
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { miniMessage } from "minimessage-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import localFont from "next/font/local";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
|
||||
const Font = localFont({ src: "./motd-font.ttf" });
|
||||
|
||||
export function MOTDRenderer({
|
||||
className,
|
||||
children,
|
||||
minecraftFont,
|
||||
}: {
|
||||
className?: string;
|
||||
children: string;
|
||||
minecraftFont?: boolean;
|
||||
}) {
|
||||
const [result, setResult] = useState("");
|
||||
const [error, setError] = useState(false);
|
||||
const { get } = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
setResult(miniMessage().toHTML(miniMessage().deserialize(children)));
|
||||
} catch (e) {
|
||||
setError(true);
|
||||
setResult(
|
||||
"Error while parsing MOTD. \n Please let the server owners know."
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: result,
|
||||
}}
|
||||
className={cn(
|
||||
className,
|
||||
minecraftFont
|
||||
? error
|
||||
? ""
|
||||
: (get("mc-font") ?? "true") === "true"
|
||||
? Font.className
|
||||
: ""
|
||||
: ""
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
71
apps/www/src/components/feat/server-page/motd/motd-row.tsx
Normal file
71
apps/www/src/components/feat/server-page/motd/motd-row.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { MOTDRenderer } from "./motd-renderer";
|
||||
import useClipboard from "@/lib/useClipboard";
|
||||
import { miniMessage } from "minimessage-js";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function MOTDRow({ server }: { server: ServerResponse }) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
<div className="border rounded-xl p-4 relative max-h-[250px] ">
|
||||
<strong className="text-lg">MOTD</strong>
|
||||
<br />
|
||||
<Separator className="my-2" />
|
||||
<MOTDRenderer
|
||||
className={cn("mt-2 break-all overflow-y-auto max-h-[150px]")}
|
||||
minecraftFont
|
||||
>
|
||||
{server.motd}
|
||||
</MOTDRenderer>
|
||||
<br />
|
||||
<small className="absolute bottom-[10px]">
|
||||
{server.motd.length} characters,{" "}
|
||||
<button
|
||||
className="cursor-pointer underline"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
clipboard.writeText(
|
||||
miniMessage().toHTML(miniMessage().deserialize(server.motd))
|
||||
);
|
||||
toast.success("Copied to clipboard.");
|
||||
}}
|
||||
>
|
||||
click to copy HTML
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||
import { Heart, Star } from "lucide-react";
|
||||
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
||||
import { useState } from "react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
export function ServerPageButtons({ server }: { server: ServerResponse }) {
|
||||
const clerk = useClerk();
|
||||
const favoritesStore = useFavoriteStore(server.name);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SignedIn>
|
||||
<Button
|
||||
className="flex items-center gap-2 text-sm"
|
||||
variant={favoritesStore.isFavorite ? "secondary" : "default"}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await favoritesStore.toggleFavorite(server.name);
|
||||
setLoading(false);
|
||||
}}
|
||||
disabled={loading || favoritesStore.isFavorite === null}
|
||||
>
|
||||
<Heart
|
||||
size={16}
|
||||
fill={favoritesStore.isFavorite ? "red" : "transparent"}
|
||||
color="red"
|
||||
/>
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>{favoritesStore.favoriteNumber}</code>
|
||||
)}
|
||||
{loading && <Spinner />}
|
||||
</Button>
|
||||
</SignedIn>
|
||||
<SignedOut>
|
||||
<Button
|
||||
className="flex items-center gap-2 text-sm"
|
||||
onClick={() => clerk.openSignUp()}
|
||||
>
|
||||
<Star size={16} />
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>{favoritesStore.favoriteNumber}</code>
|
||||
)}
|
||||
</Button>
|
||||
</SignedOut>
|
||||
</>
|
||||
);
|
||||
}
|
||||
184
apps/www/src/components/feat/server-page/server-page-tags.tsx
Normal file
184
apps/www/src/components/feat/server-page/server-page-tags.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import type { BadgeColor } from "../server-list/server-card";
|
||||
import { allTags } from "@/config/tags";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||
import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs";
|
||||
|
||||
export function ServerPageTags(props: {
|
||||
server: ServerResponse;
|
||||
onlineServer?: OnlineServer;
|
||||
className?: string;
|
||||
unclickable?: boolean;
|
||||
}) {
|
||||
const [loading, setLoading] = useState<boolean | undefined>(undefined);
|
||||
const [compatibleTags, setCompatibleTags] = useState<
|
||||
Array<{
|
||||
name: ReactNode;
|
||||
docsName?: string;
|
||||
tooltip: string;
|
||||
htmlDocs: string;
|
||||
role: BadgeColor;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
const [tagOpen, setTagOpen] = useQueryState(
|
||||
"t",
|
||||
parseAsArrayOf(parseAsString).withOptions({
|
||||
history: "push",
|
||||
shallow: true,
|
||||
})
|
||||
);
|
||||
|
||||
useEffectOnce(() => {
|
||||
if (loading === undefined) {
|
||||
setLoading(true);
|
||||
setCompatibleTags([]);
|
||||
|
||||
const tagPromises = allTags.map(async (tag) => {
|
||||
try {
|
||||
if (
|
||||
!tag.condition ||
|
||||
(await tag.condition({
|
||||
server: props.server,
|
||||
online: props.onlineServer,
|
||||
}))
|
||||
) {
|
||||
const name = await tag.name({
|
||||
server: props.server,
|
||||
online: props.onlineServer,
|
||||
});
|
||||
return {
|
||||
name,
|
||||
docsName: tag.docsName,
|
||||
tooltip: tag.tooltipDesc,
|
||||
htmlDocs: tag.htmlDocs,
|
||||
role: tag.role === undefined ? "default" : tag.role,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing tag:", error);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
Promise.all(tagPromises)
|
||||
.then((results) => {
|
||||
setCompatibleTags(results.filter(Boolean) as any[]);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error loading tags:", error);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const toggleTagInQuery = async (
|
||||
tagName: string | undefined,
|
||||
shouldAdd: boolean
|
||||
) => {
|
||||
if (!tagName) return;
|
||||
|
||||
try {
|
||||
const currentTags = tagOpen || [];
|
||||
|
||||
if (shouldAdd) {
|
||||
if (!currentTags.includes(btoa(tagName))) {
|
||||
await setTagOpen([...currentTags, btoa(tagName)]);
|
||||
}
|
||||
} else {
|
||||
const filteredTags = currentTags.filter((tag) => tag !== btoa(tagName));
|
||||
await setTagOpen(filteredTags.length > 0 ? filteredTags : null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update URL params:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="font-normal tracking-normal flex flex-wrap"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{compatibleTags.map((t) => {
|
||||
const tagName = t.docsName || "";
|
||||
const isOpen = tagOpen?.includes(btoa(tagName)) || false;
|
||||
|
||||
return (
|
||||
<span key={tagName || String(t.name)} className="mr-1">
|
||||
{props.unclickable ? (
|
||||
<Badge variant={t.role} className={props.className}>
|
||||
{t.name}
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleTagInQuery(tagName, true);
|
||||
}}
|
||||
>
|
||||
<Badge variant={t.role} className={props.className}>
|
||||
{t.name}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-normal">
|
||||
{t.tooltip}
|
||||
<br />
|
||||
Click the tag to learn more about it.
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
toggleTagInQuery(tagName, open);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{'"'}
|
||||
{t.docsName === undefined ? t.name : t.docsName}
|
||||
{'"'} documentation
|
||||
</DialogTitle>
|
||||
<DialogDescription className="font-normal">
|
||||
<div
|
||||
className="prose prose-sm max-w-none dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: t.htmlDocs }}
|
||||
/>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
apps/www/src/components/feat/server-page/server-page.tsx
Normal file
32
apps/www/src/components/feat/server-page/server-page.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
import IconDisplay from "../icons/minecraft-icon-display";
|
||||
import { ServerPageTags } from "./server-page-tags";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ServerRows } from "./server-rows";
|
||||
import { ServerPageButtons } from "./server-page-buttons";
|
||||
|
||||
export function ServerMainPage({ server }: { server: ServerResponse }) {
|
||||
return (
|
||||
<div className="pt-[150px] xl:px-[100px]">
|
||||
<span className="flex items-center gap-2 w-full">
|
||||
<div className="bg-secondary p-4 rounded-lg ml-4">
|
||||
<IconDisplay server={server} />
|
||||
</div>
|
||||
<p className="w-full">
|
||||
<div className="flex justify-between w-full">
|
||||
<h1 className="text-2xl font-bold">{server.name}</h1>
|
||||
<span>
|
||||
<ServerPageButtons server={server} />
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex items-center gap-2 flex-wrap">
|
||||
<ServerPageTags server={server} className="mt-1" />
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
<Separator className="my-6" />
|
||||
<ServerRows server={server} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
apps/www/src/components/feat/server-page/server-provider.tsx
Normal file
40
apps/www/src/components/feat/server-page/server-provider.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
import { Placeholder } from "@/components/ui/placeholder";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useServer } from "@/lib/hooks/use-server";
|
||||
import type { OnlineServer } from "@/lib/types/mh-server";
|
||||
import { X } from "lucide-react";
|
||||
import { ServerMainPage } from "./server-page";
|
||||
|
||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||
const { server, error, loading } = useServer({ id: serverId });
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div className="absolute top-[50%] left-[50%]">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error !== null)
|
||||
return (
|
||||
<div className="absolute top-[50%] left-[50%]">
|
||||
<Placeholder
|
||||
icon={<X />}
|
||||
title="Error while fetching server"
|
||||
description={
|
||||
<>
|
||||
Try again later <br /> If this occurs again, please contact
|
||||
support or make a GitHub issue. <br /> {error}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="px-10">
|
||||
<ServerMainPage server={server as OnlineServer} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
apps/www/src/components/feat/server-page/server-rows.tsx
Normal file
45
apps/www/src/components/feat/server-page/server-rows.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
import useClipboard from "@/lib/useClipboard";
|
||||
import { MOTDRow } from "./motd/motd-row";
|
||||
import { StatisticsMainRow } from "./stats/stats-main-row";
|
||||
|
||||
export function ServerRows({ server }: { server: ServerResponse }) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
<span className="lg:grid lg:grid-cols-3 w-full gap-3">
|
||||
<MOTDRow server={server} />
|
||||
<StatisticsMainRow server={server} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
|
||||
export function StatisticsMainRow({ server }: { server: ServerResponse }) {
|
||||
return (
|
||||
<span className="border rounded-xl p-4 relative col-span-2 min-h-[250px] max-h-[250px]">
|
||||
<strong className="text-lg">Statistics</strong>
|
||||
<br />
|
||||
<Separator className="my-2" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -46,14 +46,17 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export function BrowserSettings() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const [fontFamily, setFontFamily] = useState("inter");
|
||||
const [mcFont, setMcFont] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
}, [settingsStore]);
|
||||
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Material className="mt-6 grid gap-4">
|
||||
@ -69,6 +72,24 @@ export function BrowserSettings() {
|
||||
<ModeToggle />
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Use Minecraft font</SettingTitle>
|
||||
<SettingDescription>
|
||||
Use Minecraft font for MOTD. Turning this off restores font
|
||||
settings for MOTD's to a v1-like state.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch
|
||||
checked={mcFont}
|
||||
onCheckedChange={(c) => {
|
||||
settingsStore.set("mc-font", c, false);
|
||||
setMcFont(c);
|
||||
}}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
|
||||
@ -60,6 +60,8 @@ const badgeVariants = cva(
|
||||
ring-blue-400 dark:ring-blue-400/30`,
|
||||
"purple-subtle": `bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400
|
||||
ring-purple-400 dark:ring-purple-500/30`,
|
||||
rainbow:
|
||||
"text-white ring-transparent [background:_linear-gradient(45deg,rgba(255,_0,_0,_1)_0%,rgba(255,_154,_0,_1)_10%,rgba(208,_222,_33,_1)_20%,rgba(79,_220,_74,_1)_30%,rgba(63,_218,_216,_1)_40%,rgba(47,_201,_226,_1)_50%,rgba(28,_127,_238,_1)_60%,rgba(95,_21,_242,_1)_70%,rgba(186,_12,_248,_1)_80%,rgba(251,_7,_217,_1)_90%,rgba(255,_0,_0,_1)_100%);] backdrop-blur-sm opacity-60 ",
|
||||
custom: "",
|
||||
},
|
||||
allowIconOnly: {
|
||||
|
||||
@ -49,7 +49,7 @@ const buttonVariants = cva(
|
||||
hover:bg-slate-100 dark:hover:bg-zinc-800 dark:hover:border-zinc-700 dark:hover:border-zinc-700 dark:active:bg-zinc-900 active:bg-slate-200`,
|
||||
|
||||
tertiary:
|
||||
"border border-transparent bg-transparent hover:bg-slate-100 dark:hover:bg-zinc-700/30 dark:text-zinc-200",
|
||||
"bg-transparent hover:bg-slate-100 dark:hover:bg-zinc-700/30 dark:text-zinc-200",
|
||||
|
||||
danger:
|
||||
"border border-red-500 bg-red-500 hover:text-red-500 hover:bg-transparent text-white",
|
||||
|
||||
@ -126,7 +126,7 @@ const ContextMenuItem = React.forwardRef<
|
||||
className={cn(
|
||||
"w-full px-2 rounded-lg z-100 min-h-[36px] font-normal flex items-center cursor-pointer dark:hover:bg-zinc-800/70",
|
||||
props.disabled ? "opacity-70 pointer-events-none cursor-not-allowed" : "",
|
||||
"duration-100 border border-transparent bg-transparent hover:bg-slate-100 dark:text-zinc-200",
|
||||
"duration-100 bg-transparent hover:bg-slate-100 dark:text-zinc-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -128,7 +128,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
className={cn(
|
||||
"w-full px-2 rounded-lg z-100 outline-hidden min-h-[36px] font-normal flex items-center cursor-pointer dark:hover:bg-zinc-800/70",
|
||||
props.disabled ? "opacity-70 pointer-events-none cursor-not-allowed" : "",
|
||||
"duration-100 border border-transparent bg-transparent hover:bg-slate-100 dark:text-zinc-200",
|
||||
"duration-100 bg-transparent hover:bg-slate-100 dark:text-zinc-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -41,10 +41,9 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex w-11 h-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs",
|
||||
"transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
"focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-shadcn-primary",
|
||||
"data-[state=unchecked]:bg-input",
|
||||
"peer inline-flex w-11 h-6 shrink-0 cursor-pointer items-center rounded-full border-2 data-[state=unchecked]:border-transparent data-[state=checked]:!border-shadcn-primary shadow-xs",
|
||||
"transition-colors focus-visible:outline-hidden ",
|
||||
"data-[state=unchecked]:bg-input data-[state=checked]:bg-shadcn-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -28,10 +28,11 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { BadgeColor } from "@/components/feat/server-list/server-card";
|
||||
import { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import { ServerCog } from "lucide-react";
|
||||
import { ReactNode } from "react";
|
||||
import type { BadgeColor } from "@/components/feat/server-list/server-card";
|
||||
import { isFavorited } from "@/lib/api";
|
||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import { Cake, ServerCog } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const serverCache: any = {};
|
||||
|
||||
@ -43,19 +44,22 @@ const serverCache: any = {};
|
||||
// htmlDocs: when clicked, what appears (formatted in HTML, string, using the `` string format)
|
||||
// docsName: name appearing on the title in the docs. (string)
|
||||
// role?: the role used on the badge (https://ui.shadcn.com/docs/components/badge + some custom others, string)
|
||||
// primary: does this tag appear **just** in the home screen (true), or **just** inside the server screen (false)
|
||||
// __disab: you shouldn't mess with this flag
|
||||
// __filter: if your name isn't static, set this to true
|
||||
//
|
||||
// You may also use `requestServer()` to grab the offline version of the server from the API, which may get you more information about the server (ServerResponse)
|
||||
export const allTags: Array<{
|
||||
name: (server: OnlineServer) => Promise<string | ReactNode>;
|
||||
condition?: (server: OnlineServer) => Promise<boolean>;
|
||||
listCondition?: (server: ServerResponse) => Promise<boolean>;
|
||||
name: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
}) => Promise<string | ReactNode>;
|
||||
condition?: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
}) => Promise<boolean>;
|
||||
tooltipDesc: string;
|
||||
htmlDocs: string;
|
||||
docsName: string;
|
||||
primary: boolean;
|
||||
role?: BadgeColor;
|
||||
__disab?: boolean;
|
||||
__filter?: boolean;
|
||||
@ -71,15 +75,23 @@ export const allTags: Array<{
|
||||
borderRadius: "9999px",
|
||||
}}
|
||||
/>
|
||||
{c.playerData.playerCount} online
|
||||
{String(
|
||||
c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount
|
||||
)}{" "}
|
||||
online
|
||||
</>
|
||||
),
|
||||
condition: async (c) => c.playerData.playerCount !== 0,
|
||||
condition: async (c) =>
|
||||
(c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount) !== 0,
|
||||
htmlDocs:
|
||||
"'Players Online' specifies the amount of players currently online. If this server is a network, the amount of players may not be accurate as this counter only counts the number of players coming directly from Minehut",
|
||||
tooltipDesc:
|
||||
"'Players Online' specifies the amount of players currently online.",
|
||||
primary: true,
|
||||
|
||||
role: "green-subtle",
|
||||
docsName: "Players Online",
|
||||
__filter: true,
|
||||
@ -98,10 +110,12 @@ export const allTags: Array<{
|
||||
0 online
|
||||
</>
|
||||
),
|
||||
condition: async (c) => c.playerData.playerCount === 0,
|
||||
condition: async (c) =>
|
||||
(c.online === undefined ? c.server?.playerCount : c.online.playerData) ===
|
||||
0,
|
||||
htmlDocs: "Nobody is online this server.",
|
||||
tooltipDesc: "Nobody is online this server.",
|
||||
primary: true,
|
||||
|
||||
role: "gray-subtle",
|
||||
docsName: "Nobody Online",
|
||||
__filter: true,
|
||||
@ -113,48 +127,91 @@ export const allTags: Array<{
|
||||
Always Online
|
||||
</>
|
||||
),
|
||||
condition: async (b: any) => b.staticInfo.alwaysOnline,
|
||||
condition: async (b) =>
|
||||
b.online !== undefined && b.online.staticInfo?.alwaysOnline,
|
||||
tooltipDesc:
|
||||
'"Always online" means that the server will not shut down until the plan associated with it expires.',
|
||||
htmlDocs: `
|
||||
This tag appears on servers where the plan they are under allows the server to be always online. However, if the plan associated with the tag expires, the server will no longer be Always Online. <em>This is in servers with one of the more expensive plans, or just a server that is external.</em>
|
||||
`,
|
||||
primary: true,
|
||||
|
||||
docsName: "Always Online",
|
||||
role: "blue",
|
||||
role: "blue-subtle",
|
||||
__disab: true,
|
||||
},
|
||||
{
|
||||
name: async (s) => s.staticInfo.planMaxPlayers + " max players",
|
||||
condition: async (s) => s.staticInfo.planMaxPlayers != null,
|
||||
name: async (s) =>
|
||||
(s.online !== undefined
|
||||
? s.online.staticInfo.planMaxPlayers
|
||||
: s.server?.maxPlayers) + " max players",
|
||||
condition: async (s) =>
|
||||
s.online !== undefined
|
||||
? s.online.staticInfo.planMaxPlayers != null
|
||||
: s.server?.maxPlayers != null,
|
||||
tooltipDesc:
|
||||
"This tag represents the maximum amount of players the server can have at one time.",
|
||||
docsName: "Max Players",
|
||||
htmlDocs:
|
||||
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
|
||||
primary: true,
|
||||
role: "default",
|
||||
|
||||
role: "blue",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async () => "Partner",
|
||||
condition: async (s) => s.name === "CoreBoxx",
|
||||
condition: async (s) =>
|
||||
(s.server ?? s.online ?? { name: "" }).name === "CoreBoxx",
|
||||
tooltipDesc: "This server is a partner with MHSF.",
|
||||
docsName: "Partner",
|
||||
htmlDocs: "This tag represents that this server is a partner with MHSF.",
|
||||
primary: true,
|
||||
role: "purple",
|
||||
role: "rainbow",
|
||||
},
|
||||
{
|
||||
name: async (s) => <>{s.staticInfo.serverPlan.split(" ")[0]}</>,
|
||||
name: async (s) => (
|
||||
<span className="capitalize">
|
||||
{(s.online !== undefined
|
||||
? s.online.staticInfo.serverPlan
|
||||
: (s.server?.server_plan ?? "")
|
||||
)
|
||||
.split(" ")[0]
|
||||
.split("_")[0]
|
||||
.toLocaleLowerCase()}
|
||||
</span>
|
||||
),
|
||||
tooltipDesc: "This tag represents the server plan this server is using.",
|
||||
docsName: "Server Plan",
|
||||
htmlDocs:
|
||||
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
|
||||
primary: true,
|
||||
|
||||
role: "red-subtle",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async (s) => (
|
||||
<span className="flex items-center gap-2">
|
||||
<Cake size={16} /> Created {timeConverter(s.server?.creation)}
|
||||
</span>
|
||||
),
|
||||
condition: async (s) => s.server !== undefined,
|
||||
tooltipDesc: "This tag represents the date this server was created.",
|
||||
docsName: "Creation Date",
|
||||
htmlDocs: "This tag represents the date this server was created.",
|
||||
role: "gray",
|
||||
},
|
||||
{
|
||||
name: async (s) => "Favorited",
|
||||
condition: async (s) => {
|
||||
const favorited = await isFavorited(
|
||||
(s.online ?? s.server ?? { name: "" }).name
|
||||
);
|
||||
return favorited;
|
||||
},
|
||||
tooltipDesc: "This tag represents that you favorited this server.",
|
||||
docsName: "Favorited",
|
||||
htmlDocs:
|
||||
"This tag shows that you favorited this server in MHSF. The amount of favorites is publicly shown to other users using MHSF. We do not provide server owners with data about who favorites a server, unlike traditional voting systems.",
|
||||
role: "red",
|
||||
},
|
||||
// deprecated
|
||||
/**{
|
||||
name: async () => "Velocity",
|
||||
@ -167,7 +224,7 @@ export const allTags: Array<{
|
||||
htmlDocs:
|
||||
'Does this server use <a href="https://papermc.io/software/velocity">Velocity</a>? This means that the server has multiple minigames/other servers gamemodes that are private, and this server is the lobby.',
|
||||
docsName: "Velocity",
|
||||
primary: true,
|
||||
|
||||
role: "violet",
|
||||
}, */
|
||||
];
|
||||
@ -175,161 +232,143 @@ export const allTags: Array<{
|
||||
export const allCategories: Array<{
|
||||
name: string;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
primary: boolean;
|
||||
role?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "red"
|
||||
| "orange"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "lime"
|
||||
| "blue"
|
||||
| "teal"
|
||||
| "cyan"
|
||||
| "violet"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "fuchsia"
|
||||
| "pink";
|
||||
role?: BadgeColor;
|
||||
}> = [
|
||||
{
|
||||
name: "Farming",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("farming");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "SMP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("smp");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Factions",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("factions");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Meme",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("meme");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Puzzle",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("puzzle");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Box",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("box");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Minigames",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("minigames");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "RPG",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("rpg");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Parkour",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("parkour");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Lifesteal",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("lifesteal");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Prison",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("prison");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Gens",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("gens");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Skyblock",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("skyblock");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Roleplay",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("roleplay");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "PvP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("pvp");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Modded",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("modded");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Creative",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("creative");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
|
||||
role: "default",
|
||||
},
|
||||
];
|
||||
|
||||
@ -344,3 +383,26 @@ async function requestServer(s: OnlineServer): Promise<ServerResponse> {
|
||||
}
|
||||
return serverCache[s.name];
|
||||
}
|
||||
|
||||
function timeConverter(UNIX_timestamp: any) {
|
||||
const a = new Date(UNIX_timestamp);
|
||||
const months = [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
];
|
||||
const year = a.getFullYear();
|
||||
const month = months[a.getMonth()];
|
||||
const date = a.getDate();
|
||||
const time = month + "/" + date + "/" + year;
|
||||
return time;
|
||||
}
|
||||
|
||||
5
apps/www/src/lib/hooks/use-customizations.tsx
Normal file
5
apps/www/src/lib/hooks/use-customizations.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { useEffectOnce } from "../useEffectOnce";
|
||||
|
||||
export function useCustomizations() {
|
||||
useEffectOnce(() => {});
|
||||
}
|
||||
59
apps/www/src/lib/hooks/use-favorite-store.tsx
Normal file
59
apps/www/src/lib/hooks/use-favorite-store.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useClerk } from "@clerk/nextjs";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
favoriteServer,
|
||||
getAccountFavorites,
|
||||
getCommunityServerFavorites,
|
||||
isFavorited,
|
||||
} from "../api";
|
||||
|
||||
export function useFavoriteStore(server?: string) {
|
||||
const [favorites, setFavorites] = useState<string[] | null>(null);
|
||||
const [isFavorite, setIsFavorite] = useState<boolean | null>(null);
|
||||
const [favoriteNumber, setFavoriteNumber] = useState<number | null>(null);
|
||||
const { isSignedIn } = useClerk();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSignedIn) {
|
||||
getAccountFavorites().then((favorites) => setFavorites(favorites));
|
||||
}
|
||||
if (server) {
|
||||
getCommunityServerFavorites(server).then((number) =>
|
||||
setFavoriteNumber(number)
|
||||
);
|
||||
if (isFavorite === null) {
|
||||
isFavorited(server).then((isFavorite) => setIsFavorite(isFavorite));
|
||||
}
|
||||
}
|
||||
}, [isSignedIn, server, isFavorite]);
|
||||
|
||||
return {
|
||||
reloadFavorites: () => {
|
||||
if (isSignedIn) {
|
||||
getAccountFavorites().then((favorites) => setFavorites(favorites));
|
||||
} else throw new Error("Not signed in");
|
||||
},
|
||||
favorites,
|
||||
loading: favorites === null,
|
||||
loadingNumber: favoriteNumber === null,
|
||||
favoriteNumber,
|
||||
isFavorite,
|
||||
toggleFavorite: async (server: string) => {
|
||||
if (isFavorite === null) throw new Error("Hold up lemme load rq");
|
||||
if (favoriteNumber === null) throw new Error("Nah");
|
||||
await favoriteServer(server);
|
||||
|
||||
// Resolve remote differences
|
||||
if (isFavorite === true) {
|
||||
setIsFavorite(false);
|
||||
setFavoriteNumber(favoriteNumber - 1);
|
||||
}
|
||||
if (isFavorite === false) {
|
||||
setIsFavorite(true);
|
||||
setFavoriteNumber(favoriteNumber + 1);
|
||||
}
|
||||
},
|
||||
getServerFavoritesNumber: async (server: string) =>
|
||||
await getCommunityServerFavorites(server),
|
||||
};
|
||||
}
|
||||
@ -34,7 +34,7 @@ import type { OnlineServer } from "../types/mh-server";
|
||||
const itemsPerScroll = 40;
|
||||
|
||||
export function useInfiniteScrolling(servers: OnlineServer[]) {
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const [currentOffset, setCurrentOffset] = useState(itemsPerScroll);
|
||||
const [data, setData] = useState<OnlineServer[]>([]);
|
||||
const [hasMoreData, setHasMoreData] = useState(true);
|
||||
|
||||
@ -46,6 +46,7 @@ export function useInfiniteScrolling(servers: OnlineServer[]) {
|
||||
return {
|
||||
itemsLength: currentOffset + itemsPerScroll,
|
||||
fetchMoreData: () => {
|
||||
setData([]);
|
||||
setCurrentOffset(currentOffset + itemsPerScroll);
|
||||
const currentData = data;
|
||||
const dataSlice = servers.slice(
|
||||
|
||||
31
apps/www/src/lib/hooks/use-server.tsx
Normal file
31
apps/www/src/lib/hooks/use-server.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import type { OnlineServer } from "../types/mh-server";
|
||||
import { useEffectOnce } from "../useEffectOnce";
|
||||
|
||||
export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [server, setServer] = useState<OnlineServer | null>(null);
|
||||
|
||||
useEffectOnce(() => {
|
||||
try {
|
||||
(async () => {
|
||||
const res = await fetch(
|
||||
`https://api.minehut.com/server/${serverSpecifier.id || serverSpecifier.name}${serverSpecifier.name ? "?byName=true" : ""}`
|
||||
);
|
||||
const json = await res.json();
|
||||
console.log(json);
|
||||
if (json.server === null) throw new Error("Server not found");
|
||||
|
||||
setServer(json.server);
|
||||
setLoading(false);
|
||||
})();
|
||||
} catch (e) {
|
||||
console.log("Error occurred while fetching server data", e);
|
||||
setError((e as Error).message);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return { loading, error, server };
|
||||
}
|
||||
@ -52,16 +52,24 @@ export function useSettingsStore() {
|
||||
}
|
||||
return localStorage.getItem(key);
|
||||
},
|
||||
set: async (key: string, value: string, userEntry: boolean) => {
|
||||
set: async (
|
||||
key: string,
|
||||
value: string | boolean,
|
||||
userEntry: boolean,
|
||||
__unsafeMetadata = false
|
||||
) => {
|
||||
if (isSignedIn && userEntry === true && __unsafeMetadata) {
|
||||
await user.update({ unsafeMetadata: { [key]: value } });
|
||||
}
|
||||
if (isSignedIn && userEntry === true) {
|
||||
await fetch("/api/v0/account-sl/change", {
|
||||
body: JSON.stringify({ [key]: value }),
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
if (!isSignedIn && userEntry)
|
||||
throw new Error("How is this even possible?!?!");
|
||||
if (!isSignedIn && userEntry) localStorage.setItem(key, value.toString());
|
||||
if (userEntry === false) {
|
||||
localStorage.setItem(key, value);
|
||||
localStorage.setItem(key, value.toString());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -58,7 +58,7 @@ export default async function handler(
|
||||
}
|
||||
|
||||
export async function increaseNum(client: MongoClient, server: string) {
|
||||
const db = client.db("mhsf");
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const collection = db.collection("meta");
|
||||
const find = await collection.find({ server: server }).toArray();
|
||||
|
||||
@ -74,7 +74,7 @@ export async function increaseNum(client: MongoClient, server: string) {
|
||||
}
|
||||
|
||||
export async function decreaseNum(client: MongoClient, server: string) {
|
||||
const db = client.db("mhsf");
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const collection = db.collection("meta");
|
||||
const find = await collection.find({ server: server }).toArray();
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ export default async function handler(
|
||||
const collection = db.collection("favorites");
|
||||
const find = await collection.find({ user: userId }).toArray();
|
||||
|
||||
if (find.length == 0) {
|
||||
if (find.length === 0) {
|
||||
collection.insertOne({ user: userId, favorites: [server] });
|
||||
await increaseNum(client, server);
|
||||
|
||||
@ -72,7 +72,7 @@ export default async function handler(
|
||||
if (index > -1) {
|
||||
existingFavorites.splice(index, 1);
|
||||
}
|
||||
collection.replaceOne(
|
||||
await collection.replaceOne(
|
||||
{ _id: new ObjectId(collect._id) },
|
||||
{
|
||||
user: userId,
|
||||
@ -88,7 +88,7 @@ export default async function handler(
|
||||
} else {
|
||||
existingFavorites.push(server);
|
||||
await increaseNum(client, server);
|
||||
collection.replaceOne(
|
||||
await collection.replaceOne(
|
||||
{ _id: new ObjectId(collect._id) },
|
||||
{
|
||||
user: userId,
|
||||
|
||||
@ -1,34 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"contentlayer/generated": ["./.contentlayer/generated"],
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".contentlayer/generated",
|
||||
"docs/legal/external-content-agreement.mdx"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"contentlayer/generated": [
|
||||
"./.contentlayer/generated"
|
||||
],
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".contentlayer/generated",
|
||||
"docs/legal/external-content-agreement.mdx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user