mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-15 09:18:01 -05:00
feat: achievements
This commit is contained in:
parent
190024aef5
commit
5699da4dc4
10
biome.json
Normal file
10
biome.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useTemplate": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,16 +10,16 @@ console.log();
|
||||
import { MongoClient, WithId } from "mongodb";
|
||||
import { config } from "dotenv";
|
||||
import {
|
||||
OnlineServer,
|
||||
OnlineServerExtended,
|
||||
ServerResponse,
|
||||
OnlineServer,
|
||||
OnlineServerExtended,
|
||||
ServerResponse,
|
||||
} from "./types/mh-server.js";
|
||||
import { CronJob } from "cron";
|
||||
import { Achievement } from "./types/achievement.js";
|
||||
|
||||
// set-up config
|
||||
config({
|
||||
path: process.env.MHC_DOCKER != "true" ? "../.env.local" : "./.env.local",
|
||||
path: process.env.MHC_DOCKER != "true" ? "../.env.local" : "./.env.local",
|
||||
});
|
||||
|
||||
let mongo = new MongoClient(process.env.MONGO_DB as string);
|
||||
@ -32,28 +32,28 @@ const INFO = chalk.blueBright("INFO");
|
||||
console.log(INFO, "Starting cron job #1");
|
||||
|
||||
CronJob.from({
|
||||
cronTime: "*/30 * * * *",
|
||||
onTick: function () {
|
||||
periodicCronJob().catch((e) => {
|
||||
console.log(chalk.red("[CRON] " + ERROR + " Error while running: "));
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
start: true,
|
||||
timeZone: "America/Los_Angeles",
|
||||
cronTime: "*/30 * * * *",
|
||||
onTick: function () {
|
||||
periodicCronJob().catch((e) => {
|
||||
console.log(chalk.red("[CRON] " + ERROR + " Error while running: "));
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
start: true,
|
||||
timeZone: "America/Los_Angeles",
|
||||
});
|
||||
|
||||
console.log(INFO, "Starting cron job #2");
|
||||
CronJob.from({
|
||||
cronTime: "0 */12 * * *",
|
||||
onTick: function () {
|
||||
achievementTask().catch((e) => {
|
||||
console.log(chalk.red("[CRON] " + ERROR + " Error while running: "));
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
start: true,
|
||||
timeZone: "America/Los_Angeles",
|
||||
cronTime: "0 */12 * * *",
|
||||
onTick: function () {
|
||||
achievementTask().catch((e) => {
|
||||
console.log(chalk.red("[CRON] " + ERROR + " Error while running: "));
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
start: true,
|
||||
timeZone: "America/Los_Angeles",
|
||||
});
|
||||
|
||||
/**
|
||||
@ -65,223 +65,213 @@ CronJob.from({
|
||||
* If an error occurs, it logs the error and closes the MongoDB connection.
|
||||
*/
|
||||
async function periodicCronJob() {
|
||||
try {
|
||||
// No more mumbo jumbo
|
||||
const mh = await (
|
||||
await fetch("https://api.minehut.com/servers", {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"accept-language": Math.random().toString(),
|
||||
priority: "u=1, i",
|
||||
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"macOS"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"Content-Type": "application/json",
|
||||
Referer: "http://localhost:3000/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
},
|
||||
body: null,
|
||||
method: "GET",
|
||||
})
|
||||
).json();
|
||||
console.log("[CRON] " + SUCCESS + " Found", mh.servers.length, "servers");
|
||||
try {
|
||||
// No more mumbo jumbo
|
||||
const mh = await (
|
||||
await fetch("https://api.minehut.com/servers", {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"accept-language": Math.random().toString(),
|
||||
priority: "u=1, i",
|
||||
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"macOS"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"Content-Type": "application/json",
|
||||
Referer: "http://localhost:3000/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
},
|
||||
body: null,
|
||||
method: "GET",
|
||||
})
|
||||
).json();
|
||||
console.log("[CRON] " + SUCCESS + " Found", mh.servers.length, "servers");
|
||||
|
||||
const mha = mongo.db("mhsf").collection("mh");
|
||||
const meta = mongo.db("mhsf").collection("meta");
|
||||
const dbl = mongo.db("mhsf").collection("history");
|
||||
const mha = mongo.db("mhsf").collection("mh");
|
||||
const meta = mongo.db("mhsf").collection("meta");
|
||||
const dbl = mongo.db("mhsf").collection("history");
|
||||
|
||||
await mha.insertOne({
|
||||
total_players: mh.total_players,
|
||||
total_servers: mh.total_servers,
|
||||
date: new Date(),
|
||||
});
|
||||
await mha.insertOne({
|
||||
total_players: mh.total_players,
|
||||
total_servers: mh.total_servers,
|
||||
date: new Date(),
|
||||
});
|
||||
|
||||
let y = 0;
|
||||
let y = 0;
|
||||
|
||||
mh.servers.forEach(async (server: OnlineServer, i: number) => {
|
||||
const serverFavoritesObject = await meta.findOne({
|
||||
server: server.name,
|
||||
});
|
||||
let favorites = 0;
|
||||
if (serverFavoritesObject != undefined)
|
||||
favorites = serverFavoritesObject.favorites;
|
||||
mh.servers.forEach(async (server: OnlineServer, i: number) => {
|
||||
const serverFavoritesObject = await meta.findOne({
|
||||
server: server.name,
|
||||
});
|
||||
let favorites = 0;
|
||||
if (serverFavoritesObject != undefined)
|
||||
favorites = serverFavoritesObject.favorites;
|
||||
|
||||
await dbl.insertOne({
|
||||
player_count: server.playerData.playerCount,
|
||||
favorites,
|
||||
server: server.name,
|
||||
date: new Date(),
|
||||
});
|
||||
await dbl.insertOne({
|
||||
player_count: server.playerData.playerCount,
|
||||
favorites,
|
||||
server: server.name,
|
||||
date: new Date(),
|
||||
});
|
||||
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
"[CRON] " +
|
||||
INFO +
|
||||
" Remaining servers: " +
|
||||
(y + "/" + mh.servers.length)
|
||||
);
|
||||
y++;
|
||||
if (y == mh.servers.length) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
"[CRON] " + SUCCESS + " Finished! Closing MongoDB connection."
|
||||
);
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
"[CRON] " +
|
||||
INFO +
|
||||
" Remaining servers: " +
|
||||
(y + "/" + mh.servers.length),
|
||||
);
|
||||
y++;
|
||||
if (y == mh.servers.length) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
"[CRON] " + SUCCESS + " Finished! Closing MongoDB connection.",
|
||||
);
|
||||
|
||||
// Close connection
|
||||
await mongo
|
||||
.close()
|
||||
.catch((e) =>
|
||||
console.log(
|
||||
"[CRON] " + WARN + " Error while closing MongoDB connection:",
|
||||
e
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("[CRON] " + ERROR + " Error while parsing JSON:", e);
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("[CRON] " + ERROR + " Error while parsing JSON:", e);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function achievementTask() {
|
||||
try {
|
||||
const mh = await (
|
||||
await fetch("https://api.minehut.com/servers", {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"accept-language": Math.random().toString(),
|
||||
priority: "u=1, i",
|
||||
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"macOS"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"Content-Type": "application/json",
|
||||
Referer: "http://localhost:3000/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
},
|
||||
body: null,
|
||||
method: "GET",
|
||||
})
|
||||
).json();
|
||||
const meta = mongo.db("mhsf").collection("meta");
|
||||
const achievements = mongo.db("mhsf").collection("achievements");
|
||||
console.log("adding achievements");
|
||||
try {
|
||||
const mh = await (
|
||||
await fetch("https://api.minehut.com/servers", {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"accept-language": Math.random().toString(),
|
||||
priority: "u=1, i",
|
||||
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"macOS"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"Content-Type": "application/json",
|
||||
Referer: "http://localhost:3000/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
},
|
||||
body: null,
|
||||
method: "GET",
|
||||
})
|
||||
).json();
|
||||
const meta = mongo.db("mhsf").collection("meta");
|
||||
const achievements = mongo.db("mhsf").collection("achievements");
|
||||
console.log("adding achievements");
|
||||
|
||||
mh.servers.forEach(async (server: OnlineServer, i: number) => {
|
||||
const serverFavoritesObject = await meta.findOne({
|
||||
server: server.name,
|
||||
});
|
||||
let favorites = 0;
|
||||
if (serverFavoritesObject != undefined)
|
||||
favorites = serverFavoritesObject.favorites;
|
||||
mh.servers.forEach(async (server: OnlineServer, i: number) => {
|
||||
const serverFavoritesObject = await meta.findOne({
|
||||
server: server.name,
|
||||
});
|
||||
let favorites = 0;
|
||||
if (serverFavoritesObject != undefined)
|
||||
favorites = serverFavoritesObject.favorites;
|
||||
|
||||
const srvExt: OnlineServerExtended = {
|
||||
...server,
|
||||
favorites,
|
||||
position: { joins: i + 1 },
|
||||
};
|
||||
const prevAchievements = ((await achievements.findOne({
|
||||
name: server.name,
|
||||
})) || { _id: "", name: server.name, achievements: [] }) as WithId<{
|
||||
name: string;
|
||||
achievements: Achievement[];
|
||||
}>;
|
||||
const srvExt: OnlineServerExtended = {
|
||||
...server,
|
||||
favorites,
|
||||
position: { joins: i + 1 },
|
||||
};
|
||||
const prevAchievements = ((await achievements.findOne({
|
||||
name: server.name,
|
||||
})) || { _id: "", name: server.name, achievements: [] }) as WithId<{
|
||||
name: string;
|
||||
achievements: Achievement[];
|
||||
}>;
|
||||
|
||||
const achievementsTsk = await achievementEngine(
|
||||
srvExt,
|
||||
prevAchievements.achievements
|
||||
);
|
||||
const achievementsTsk = await achievementEngine(
|
||||
srvExt,
|
||||
prevAchievements.achievements,
|
||||
);
|
||||
|
||||
await achievements.insertOne({
|
||||
name: server.name,
|
||||
achievements: achievementsTsk,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("[CRON] " + ERROR + " Error while parsing JSON:", e);
|
||||
await achievements.insertOne({
|
||||
name: server.name,
|
||||
achievements: achievementsTsk,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("[CRON] " + ERROR + " Error while parsing JSON:", e);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function achievementEngine(
|
||||
server: OnlineServerExtended,
|
||||
currentAchievements: Achievement[]
|
||||
server: OnlineServerExtended,
|
||||
currentAchievements: Achievement[],
|
||||
): Promise<Achievement[]> {
|
||||
const achievements: Array<Achievement> = [];
|
||||
const achievements: Array<Achievement> = [];
|
||||
|
||||
if (
|
||||
server.favorites >= 1000 &&
|
||||
currentAchievements.find((c) => c.type == "has1kFavorites") === undefined
|
||||
) {
|
||||
achievements.push({
|
||||
type: "has1kFavorites",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
if (
|
||||
server.favorites >= 1000 &&
|
||||
currentAchievements.find((c) => c.type == "has1kFavorites") === undefined
|
||||
) {
|
||||
achievements.push({
|
||||
type: "has1kFavorites",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
server.favorites >= 100000 &&
|
||||
currentAchievements.find((c) => c.type == "has100kFavorites") === undefined
|
||||
) {
|
||||
achievements.push({
|
||||
type: "has100kFavorites",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
if (
|
||||
server.favorites >= 100000 &&
|
||||
currentAchievements.find((c) => c.type == "has100kFavorites") === undefined
|
||||
) {
|
||||
achievements.push({
|
||||
type: "has100kFavorites",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
server.playerData.playerCount >= 2 &&
|
||||
currentAchievements.find((c) => c.type == "has1kTotalJoins") === undefined
|
||||
) {
|
||||
const v: { server: ServerResponse } = await (
|
||||
await fetch(
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true"
|
||||
)
|
||||
).json();
|
||||
if (
|
||||
server.playerData.playerCount >= 2 &&
|
||||
currentAchievements.find((c) => c.type == "has1kTotalJoins") === undefined
|
||||
) {
|
||||
const v: { server: ServerResponse } = await (
|
||||
await fetch(
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true",
|
||||
)
|
||||
).json();
|
||||
|
||||
if (v.server.joins >= 1000) {
|
||||
achievements.push({
|
||||
type: "has1kTotalJoins",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
server.playerData.playerCount >= 10 &&
|
||||
currentAchievements.find((c) => c.type == "has100kTotalJoins") === undefined
|
||||
) {
|
||||
const v: { server: ServerResponse } = await (
|
||||
await fetch(
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true"
|
||||
)
|
||||
).json();
|
||||
if (v.server.joins >= 1000) {
|
||||
achievements.push({
|
||||
type: "has1kTotalJoins",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
server.playerData.playerCount >= 10 &&
|
||||
currentAchievements.find((c) => c.type == "has100kTotalJoins") === undefined
|
||||
) {
|
||||
const v: { server: ServerResponse } = await (
|
||||
await fetch(
|
||||
"https://api.minehut.com/server/" + server.name + "?byName=true",
|
||||
)
|
||||
).json();
|
||||
|
||||
if (v.server.joins >= 100000) {
|
||||
achievements.push({
|
||||
type: "has100kTotalJoins",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (v.server.joins >= 100000) {
|
||||
achievements.push({
|
||||
type: "has100kTotalJoins",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (server.position.joins === 1) {
|
||||
achievements.push({
|
||||
type: "mostJoined",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
if (server.position.joins === 1) {
|
||||
achievements.push({
|
||||
type: "mostJoined",
|
||||
date: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return achievements;
|
||||
return achievements;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ title: "Troubleshooting: Making external servers on Minehut"
|
||||
|
||||
# External Servers on Minehut
|
||||
I think creating external servers on Minehut is an advanced subject, and it is not documented well enough for the circumstances that
|
||||
might occur with server owners. This is a [extension/rephrasing of the offical wiki guide](https://minehut.wiki.gg/wiki/External). All points in **bold** are things you shouldn't miss, and are commonly misread.
|
||||
might occur with server owners. This is a [extension/rephrasing of the offical wiki guide](Wiki:External). All points in **bold** are things you shouldn't miss, and are commonly misread.
|
||||
|
||||
<br/>
|
||||
<Separator/>
|
||||
@ -87,7 +87,7 @@ proxy-protocol: true
|
||||
```
|
||||
|
||||
## Thats it!
|
||||
After this, there are mostly no more common issues. Continue on [the wiki](https://minehut.wiki.gg/wiki/External#Connect_Your_External_Server_Plan_on_Minehut)!
|
||||
After this, there are mostly no more common issues. Continue on [the wiki](Wiki:External#Connect_Your_External_Server_Plan_on_Minehut)!
|
||||
|
||||
## Have any issues?
|
||||
Go to the offical Minehut Discord server and go into the [#ask-for-help](https://discord.com/channels/239599059415859200/1014801630295760897) channel and create a thread.
|
||||
Go to the offical <Discord/> Minehut Discord server and go into the [#ask-for-help](https://discord.com/channels/239599059415859200/1014801630295760897) channel and create a thread.
|
||||
@ -28,4 +28,4 @@ No. Your Minehut account is not associated with your MHSF one, and consequently,
|
||||
|
||||
## Conclusion
|
||||
|
||||
If you'd like to use MHSF, go to the server list [here](/) to try it out! You may also give MHSF a star on GitHub if you feel like this project deserves it.
|
||||
If you'd like to use MHSF, go to the server list [here](Special:Root) to try it out! You may also give MHSF a star on GitHub if you feel like this project deserves it.
|
||||
|
||||
@ -3,10 +3,10 @@ title: "Customization"
|
||||
---
|
||||
|
||||
# Customize your server
|
||||
Customizing your server is very easy after you have [linked your account](/docs/guides/linking). Make sure you have done that before then.
|
||||
Customizing your server is very easy after you have [linked your account](Docs:guides/linking). Make sure you have done that before then.
|
||||
|
||||
## Getting started
|
||||
Make sure you've also [owned your server](/docs/guides/owning-a-server).
|
||||
Make sure you've also [owned your server](Docs:guides/owning-a-server).
|
||||
|
||||
## Customization Types
|
||||
### Discord Server
|
||||
@ -22,4 +22,4 @@ You can pick any color in the box and choosing a color scheme to show on your se
|
||||
You can use Markdown formatting to add a description to describe what your server is.
|
||||
|
||||
## Thats it!
|
||||
If you have additional customization types or other things you'd like us to add, [feel free to hit us with an issue on GitHub!](https://github.com/DeveloLongScript/MHSF)
|
||||
If you have additional customization types or other things you'd like us to add, [feel free to hit us with an issue on GitHub!](Special:GitHub)
|
||||
@ -22,7 +22,7 @@ Login to the server `MHSFPV.minehut.gg`. (its on a free plan, you may have to st
|
||||
|
||||
You can do many things with a linked account:
|
||||
|
||||
- [Own a server](/docs/guides/owning-a-server)
|
||||
- [Customize a server](/docs/guides/customization)
|
||||
- [Own a server](Docs:guides/owning-a-server)
|
||||
- [Customize a server](Docs:guides/customization)
|
||||
|
||||
More will be coming in future updates, however the only thing you can do with a linked account is server-based functions.
|
||||
|
||||
@ -4,11 +4,11 @@ title: "Own a server"
|
||||
|
||||
# Owning a server
|
||||
|
||||
Owning a server is quite simple and allows you to [customize your server](/docs/guides/customization) your server and make it stand out from other servers. Before owning your server, make sure you agree to the [ECA](/docs/legal/external-content-agreement).
|
||||
Owning a server is quite simple and allows you to [customize your server](/docs/guides/customization) your server and make it stand out from other servers. Before owning your server, make sure you agree to the [ECA](Docs:legal/external-content-agreement).
|
||||
|
||||
## Linking
|
||||
|
||||
Find the server you would like to own (either by looking for it, or using the keyboard shortcut `Ctrl`+`Shift`+`K` and searching for it), and make sure your account has [already been linked with your Minecraft account](/docs/guides/linking). Go to the server, and hit the Customization tab. If the owner of the server, and the user your linked to match, you will gain access to the server.
|
||||
Find the server you would like to own (either by looking for it, or using the keyboard shortcut `Ctrl`+`Shift`+`K` and searching for it), and make sure your account has [already been linked with your Minecraft account](Docs:guides/linking). Go to the server, and hit the Customization tab. If the owner of the server, and the user your linked to match, you will gain access to the server.
|
||||
If they match, you should see a button named Click to own. Press that button, and you should automagically own the server. Congratulations!
|
||||
|
||||
## I can't link my server, because my server doesn't have a author
|
||||
|
||||
@ -4,7 +4,7 @@ title: "Reporting a server"
|
||||
|
||||
# Reporting a server
|
||||
|
||||
If you believe a server that you've seen is under breach of the [ECA Agreement](/docs/legal/external-content-agreement), you may request the server in question to be taken down.
|
||||
If you believe a server that you've seen is under breach of the [ECA Agreement](Docs:legal/external-content-agreement), you may request the server in question to be taken down.
|
||||
Make sure you are logged into a account, and go to the server page and hit the customization tab. Hit the Report button, and add a reason to your report.
|
||||
Your report will be processed and the appropriate action will be taken.
|
||||
|
||||
|
||||
@ -12,14 +12,14 @@ uploaded onto the platform are.
|
||||
|
||||
## Source Code
|
||||
|
||||
The source code for MHSF is defined by the [MIT License](https://github.com/DeveloLongScript/MHSF/blob/main/LICENSE). You are free to use MHSF for commercial use, and you may modify the software however you'd like. Taking copies of the software (aka _"forking"_) is also freely allowed.
|
||||
The source code for MHSF is defined by the [MIT License](Special:GitHub/blob/main/LICENSE). You are free to use MHSF for commercial use, and you may modify the software however you'd like. Taking copies of the software (aka _"forking"_) is also freely allowed.
|
||||
|
||||
## What your limits are
|
||||
|
||||
When creating content, if its a matter of making a profile picture,
|
||||
or editing the description for a server, (and more), you must follow
|
||||
the underlying agreements below.<br/>
|
||||
For making banners & descriptions, you must follow [Minehuts Terms of Service](https://minehut.wiki.gg/wiki/Rules) _as all content made is associated to Minehut (as the server is mostly on a community for Minehut)._<br/>
|
||||
For making banners & descriptions, you must follow [Minehuts Terms of Service](Wiki:Rules) _as all content made is associated to Minehut (as the server is mostly on a community for Minehut)._<br/>
|
||||
For making Discord server embeds, you must follow [Discords Terms of Service](https://discord.com/terms/) _as all content made is associated to Discord._
|
||||
|
||||
### All other content
|
||||
|
||||
30
docs/reading.mdx
Normal file
30
docs/reading.mdx
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Reading the docs"
|
||||
---
|
||||
|
||||
# Reading the docs
|
||||
The documentation in MHSF has some special symbols used in the docs that might be useful to know.
|
||||
|
||||
<Separator/>
|
||||
|
||||
## Icons
|
||||
When looking at a link, there will be symbols used that indicate where the link is going: *(these apply to the whole site)*
|
||||
- <Notebook className="inline-block mb-1" size={20}/> indicates the link will link to the [official Minehut wiki](Wiki:/)
|
||||
- <Book className="inline-block mb-1" size={20} /> indicates the link will link to another page on the docs
|
||||
- <ExternalLink className="inline-block mb-1" size={20}/> indicates the link will go to an external site
|
||||
|
||||
When contributing, these links are as follows:
|
||||
```
|
||||
Wiki:<wiki url after https://minehut.wiki.gg/wiki/>
|
||||
Docs:<docs url after /docs/>
|
||||
Special:Root (links back to /)
|
||||
https://example.com
|
||||
|
||||
**Example:**
|
||||
[Ranks](Wiki:Ranks)
|
||||
[Getting Started](Docs:Getting-started)
|
||||
https://google.com
|
||||
```
|
||||
|
||||
The icons above will be automatically added when using the syntax above. <br/>
|
||||
<small>The source code for above is stored [here](Special:GitHub/edit/main/src/components/misc/Link.tsx)</small>
|
||||
@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@biomejs/biome": "^1.8.3",
|
||||
"@clerk/nextjs": "^5.1.3",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
@ -48,8 +49,6 @@
|
||||
"react-dom": "^18",
|
||||
"react-fade-in": "^2.0.1",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-marquee-text": "^1.0.4",
|
||||
"react-smart-ticker": "^1.2.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
|
||||
17
src/app/account/settings/layout.tsx
Normal file
17
src/app/account/settings/layout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
import { Sidebar } from "@/components/PreferencesSidebar";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<span className="pt-[48px]">
|
||||
<Sidebar curPage={pathname as string}>{children}</Sidebar>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -1,14 +1,7 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useClerk, useUser } from "@clerk/nextjs";
|
||||
import { ExternalLink, KeyRound, UserPen, Link, Cog } from "lucide-react";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { default as NextLink } from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SLCustomize } from "@/components/SLCustomizePage";
|
||||
|
||||
export default function Settings() {
|
||||
const clerk = useClerk();
|
||||
@ -20,348 +13,11 @@ export default function Settings() {
|
||||
}, [user, isSignedIn]);
|
||||
|
||||
return (
|
||||
<main className="pt-[48px] p-4">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="min-h-[calc(100vh-70px)] "
|
||||
>
|
||||
<ResizablePanel className="min-w-[285px] max-w-[285px] w-[285px] max-md:hidden">
|
||||
<div className="w-[300px] mt-[20px] ml-[10px]">
|
||||
<NextLink href="/account/settings" className="text-inherit">
|
||||
<Button className="mb-[2px] w-[250px]" variant="ghost">
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
</NextLink>
|
||||
<Button className="mb-[2px] w-[250px]">
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<UserPen size={16} className="mr-2" /> Profile{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<KeyRound size={16} className="mr-2" /> Security{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="max-md:hidden" />
|
||||
<ResizablePanel>
|
||||
<div className="p-4">
|
||||
<div className="md:hidden">
|
||||
<NextLink href="/account/settings" className="text-inherit">
|
||||
<Button className="mr-[2px]" variant="ghost">
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
</NextLink>
|
||||
|
||||
<Button className="mr-[2px] ">
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<UserPen size={16} className="mr-2" /> Profile{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-[2px] mb-[30px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<KeyRound size={16} className="mr-2" /> Security{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
<strong className="text-3xl">Profile Preferences</strong>
|
||||
<br />
|
||||
<br />
|
||||
<div className="md:grid md:grid-cols-3 gap-1.5">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<br />
|
||||
<PaddingRadioForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<br className="md:hidden" />
|
||||
<Card>
|
||||
<CardContent>
|
||||
<br />
|
||||
<RowRadioForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<br className="md:hidden" />
|
||||
<Card>
|
||||
<CardContent>
|
||||
<br />
|
||||
<ServerPaddingForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
<main className="p-4">
|
||||
<strong className="text-3xl">Profile Preferences</strong>
|
||||
<br />
|
||||
<br />
|
||||
<SLCustomize />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { setAccountSL } from "@/lib/api";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const FormSchema = z.object({
|
||||
type: z.enum(["15", "30", "40", "60", "100", "200"], {
|
||||
required_error: "You need to select a valid padding.",
|
||||
}),
|
||||
});
|
||||
|
||||
const FormSchemaGrid = z.object({
|
||||
grid: z.enum(["4", "5", "6"], {
|
||||
required_error: "You need to select a valid grid type.",
|
||||
}),
|
||||
});
|
||||
|
||||
function RowRadioForm() {
|
||||
const { user } = useUser();
|
||||
const form = useForm<z.infer<typeof FormSchemaGrid>>({
|
||||
resolver: zodResolver(FormSchemaGrid),
|
||||
defaultValues: {
|
||||
grid: user?.publicMetadata.ipr as "4" | "5" | "6" | undefined,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchemaGrid>) {
|
||||
toast.promise(setAccountSL(Number.parseInt(data.grid), "ipr"), {
|
||||
loading: "Saving...",
|
||||
success: "Saved!",
|
||||
error: "Failed to save",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="grid"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Items per row</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="4" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
4 items per row
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="5" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
5 items per row
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="6" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
6 items per row
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function PaddingRadioForm() {
|
||||
const { user } = useUser();
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
|
||||
defaultValues: {
|
||||
type: user?.publicMetadata.pad as
|
||||
| "15"
|
||||
| "30"
|
||||
| "40"
|
||||
| "60"
|
||||
| "100"
|
||||
| "200"
|
||||
| undefined,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast.promise(setAccountSL(Number.parseInt(data.type), "pad"), {
|
||||
loading: "Saving...",
|
||||
success: "Saved!",
|
||||
error: "Failed to save",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Padding of servers</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="15" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">15px</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="30" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">30px</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="40" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">40px</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="60" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">60px</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="100" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">100px</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="200" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">200px</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button
|
||||
onClick={() => setAccountSL(null, "pad")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
className="ml-2"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
const FormSchemaCB = z.object({
|
||||
padding: z.boolean().default(false).optional(),
|
||||
});
|
||||
|
||||
function ServerPaddingForm() {
|
||||
const { user } = useUser();
|
||||
const srv = user?.publicMetadata.srv as boolean | undefined;
|
||||
const form = useForm<z.infer<typeof FormSchemaCB>>({
|
||||
resolver: zodResolver(FormSchemaCB),
|
||||
defaultValues: {
|
||||
padding: srv === undefined ? false : srv,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchemaCB>) {
|
||||
toast.promise(
|
||||
setAccountSL(data.padding === undefined ? false : data.padding, "srv"),
|
||||
{
|
||||
loading: "Saving...",
|
||||
success: "Saved!",
|
||||
error: "Failed to save",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="padding"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
Use padding on the sides of only the servers, not the whole
|
||||
server list
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Only show the padding settings on the servers themselves, not
|
||||
the whole entire page of the server list.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useClerk, useUser } from "@clerk/nextjs";
|
||||
import { ExternalLink, KeyRound, UserPen, Link, Cog } from "lucide-react";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { default as NextLink } from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
import { unlinkMCAccount } from "@/lib/api";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -16,154 +9,81 @@ import { DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import CodeDialog from "@/components/misc/LinkDialog";
|
||||
|
||||
export default function Settings() {
|
||||
const clerk = useClerk();
|
||||
const clerk = useClerk();
|
||||
|
||||
const { user, isSignedIn } = useUser();
|
||||
const [linked, setLinked] = useState(false);
|
||||
useEffect(() => {
|
||||
setLinked(user?.publicMetadata.player != undefined);
|
||||
}, [user, isSignedIn]);
|
||||
const { user, isSignedIn } = useUser();
|
||||
const [linked, setLinked] = useState(false);
|
||||
useEffect(() => {
|
||||
setLinked(user?.publicMetadata.player != undefined);
|
||||
}, [user, isSignedIn]);
|
||||
|
||||
return (
|
||||
<main className="pt-[48px] p-4">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="min-h-[calc(100vh-70px)]"
|
||||
>
|
||||
<ResizablePanel className="max-md:hidden min-w-[285px] max-w-[285px] w-[285px]">
|
||||
<div className="w-[300px] mt-[20px] ml-[10px]">
|
||||
<Button className="mb-[2px] w-[250px]">
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
<NextLink href="/account/settings/options" className="text-inherit">
|
||||
<Button className="mb-[2px] w-[250px] " variant="ghost">
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
</NextLink>
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<UserPen size={16} className="mr-2" /> Profile{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<KeyRound size={16} className="mr-2" /> Security{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="max-md:hidden" />
|
||||
<ResizablePanel>
|
||||
<div className="p-4">
|
||||
<div className="md:hidden">
|
||||
<Button className="mr-[2px]">
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
<NextLink
|
||||
href="/account/settings/options"
|
||||
className="text-inherit"
|
||||
>
|
||||
<Button className="mr-[2px] " variant="ghost">
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
</NextLink>
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<UserPen size={16} className="mr-2" /> Profile{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-[2px] mb-[30px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<KeyRound size={16} className="mr-2" /> Security{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
<strong className="text-3xl">Linking</strong>
|
||||
<br />
|
||||
<br />
|
||||
<strong className="font-bold">Link Account</strong>
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Link a Minecraft account to customize a server you own.
|
||||
<br />{" "}
|
||||
{user?.publicMetadata.player != undefined && linked && (
|
||||
<>
|
||||
Currently linked to {user?.publicMetadata.player as string}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
return (
|
||||
<main className="p-4">
|
||||
<strong className="text-3xl">Linking</strong>
|
||||
<br />
|
||||
<br />
|
||||
<strong className="font-bold">Link Account</strong>
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Link a Minecraft account to customize a server you own.
|
||||
<br />{" "}
|
||||
{user?.publicMetadata.player != undefined && linked && (
|
||||
<>Currently linked to {user?.publicMetadata.player as string}</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
{!linked && (
|
||||
<Button className="h-[30px] ml-2">Link Account</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<CodeDialog
|
||||
linked={linked}
|
||||
setLinked={(c) => {
|
||||
setLinked(c);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
{!linked && <Button className="h-[30px] ml-2">Link Account</Button>}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<CodeDialog
|
||||
linked={linked}
|
||||
setLinked={(c) => {
|
||||
setLinked(c);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{linked && (
|
||||
<Button className="h-[30px] ml-2" disabled>
|
||||
Already linked
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
<strong className="font-bold">Unlink Account</strong>
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Unlink your Minecraft acconut if you have already linked one.
|
||||
</p>
|
||||
{linked && (
|
||||
<Button className="h-[30px] ml-2" disabled>
|
||||
Already linked
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
<strong className="font-bold">Unlink Account</strong>
|
||||
<div className="flex items-center">
|
||||
<p>Unlink your Minecraft acconut if you have already linked one.</p>
|
||||
|
||||
{!linked && (
|
||||
<Button className="h-[30px] ml-2" disabled>
|
||||
No linked account
|
||||
</Button>
|
||||
)}
|
||||
{!linked && (
|
||||
<Button className="h-[30px] ml-2" disabled>
|
||||
No linked account
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{linked && (
|
||||
<Button
|
||||
className="h-[30px] ml-2"
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
await toast.promise(unlinkMCAccount(), {
|
||||
success: "Unlinked account!",
|
||||
loading: "Unlinking...",
|
||||
error: "Error while unlinking account.",
|
||||
});
|
||||
setLinked(false);
|
||||
}}
|
||||
>
|
||||
Unlink account
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<small className="mt-0">
|
||||
All of your customizations stay the same, and can be changed if{" "}
|
||||
another account links your Minecraft account.
|
||||
</small>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</main>
|
||||
);
|
||||
{linked && (
|
||||
<Button
|
||||
className="h-[30px] ml-2"
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
await toast.promise(unlinkMCAccount(), {
|
||||
success: "Unlinked account!",
|
||||
loading: "Unlinking...",
|
||||
error: "Error while unlinking account.",
|
||||
});
|
||||
setLinked(false);
|
||||
}}
|
||||
>
|
||||
Unlink account
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<small className="mt-0">
|
||||
All of your customizations stay the same, and can be changed if another
|
||||
account links your Minecraft account.
|
||||
</small>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ import TableOfContent from "@/components/docs/TOC";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { allDocs } from "contentlayer/generated";
|
||||
import { useMDXComponent } from "next-contentlayer/hooks";
|
||||
import Link from "next/link";
|
||||
import NextLink from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ALegacy } from "@/components/misc/Link";
|
||||
import { MDXElements } from "@/components/misc/MDXElements";
|
||||
|
||||
export const generateStaticParams = async () =>
|
||||
allDocs.map((post) => ({ slug: [post._raw.flattenedPath] }));
|
||||
@ -34,7 +36,13 @@ const PostLayout = ({ params }: { params: { slug: string[] } }) => {
|
||||
<main className="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]">
|
||||
<div className="mx-auto w-full min-w-0">
|
||||
<div className="pb-12 pt-8 prose dark:prose-invert">
|
||||
<MDXContent components={{ Separator }} />
|
||||
<MDXContent
|
||||
components={{
|
||||
Separator,
|
||||
a: (props) => <ALegacy {...props} />,
|
||||
...MDXElements,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{doc.toc && (
|
||||
@ -53,7 +61,7 @@ const PostLayout = ({ params }: { params: { slug: string[] } }) => {
|
||||
<p className="font-medium">Contribute</p>
|
||||
<ul className="m-0 list-none">
|
||||
<li className="mt-0 pt-2">
|
||||
<Link
|
||||
<NextLink
|
||||
href={
|
||||
"https://github.com/DeveloLongScript/MHSF/edit/main/docs/" +
|
||||
doc._raw.flattenedPath +
|
||||
@ -71,10 +79,10 @@ const PostLayout = ({ params }: { params: { slug: string[] } }) => {
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
/>
|
||||
</svg>
|
||||
Edit page on GitHub
|
||||
</Link>
|
||||
</NextLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -24,10 +24,17 @@ import { BrandingGenericIcon } from "@/components/Icon";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata = {
|
||||
twitter: {
|
||||
images: [
|
||||
{
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: "/branding/meta-banner.png",
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -48,7 +48,36 @@ export async function generateMetadata(
|
||||
: "https://minehut-server-icons-live.s3.us-west-2.amazonaws.com/" +
|
||||
(json.server.icon == undefined ? "OAK_SIGN" : json.server.icon) +
|
||||
".png",
|
||||
twitter: {},
|
||||
twitter: {
|
||||
title:
|
||||
json.server == null
|
||||
? "Server doesn't exist | MHSF"
|
||||
: json.server.name +
|
||||
", " +
|
||||
(json.server.online
|
||||
? json.server.playerCount +
|
||||
(json.server.maxPlayers != 10
|
||||
? "/" + json.server.maxPlayers
|
||||
: "") +
|
||||
" online"
|
||||
: "Offline") +
|
||||
" | MHSF",
|
||||
description:
|
||||
json.server == null
|
||||
? `The server ${server} doesn't exist.`
|
||||
: `View ${server} on Minehut Server Finder!`,
|
||||
images: [
|
||||
{
|
||||
url:
|
||||
"https://minehut-server-icons-live.s3.us-west-2.amazonaws.com/" +
|
||||
json.server.icon +
|
||||
".png",
|
||||
},
|
||||
{
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
openGraph: {
|
||||
type: "profile",
|
||||
siteName: "MHSF (Minehut Server Finder)",
|
||||
@ -61,7 +90,7 @@ export async function generateMetadata(
|
||||
".png",
|
||||
},
|
||||
{
|
||||
url: "/favicon.ico",
|
||||
url: "/public/imgs/icon-cf.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -17,6 +17,7 @@ import { Copy, Info } from "lucide-react";
|
||||
import toast, { CheckmarkIcon } from "react-hot-toast";
|
||||
import { MHSF } from "@/lib/mhsf";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
import AchievementList from "./feat/AchievementList";
|
||||
|
||||
export default function AfterServerView({ server }: { server: string }) {
|
||||
const [description, setDescription] = useState("");
|
||||
@ -24,7 +25,9 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
const [mhsf, setMHSF] = useState(new MHSF());
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [view, setView] = useState("desc");
|
||||
const [view, setView] = useState(
|
||||
description !== "" || discord !== "" ? "desc" : "extra"
|
||||
);
|
||||
const [serverObject, setServerObject] = useState<ServerResponse | undefined>(
|
||||
undefined
|
||||
);
|
||||
@ -51,20 +54,16 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
return (
|
||||
<>
|
||||
<FadeIn>
|
||||
<div
|
||||
className={
|
||||
description != "" || discord != ""
|
||||
? "grid sm:grid-cols-6 h-full pl-4 pr-4"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div className="grid sm:grid-cols-6 h-full pl-4 pr-4">
|
||||
<div className="ml-5 mb-2 flex items-center sm:hidden">
|
||||
<Button
|
||||
variant={view == "desc" ? undefined : "ghost"}
|
||||
onClick={() => setView("desc")}
|
||||
>
|
||||
Description
|
||||
</Button>
|
||||
{(description != "" || discord != "") && (
|
||||
<Button
|
||||
variant={view == "desc" ? undefined : "ghost"}
|
||||
onClick={() => setView("desc")}
|
||||
>
|
||||
Description
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={view == "extra" ? undefined : "ghost"}
|
||||
onClick={() => setView("extra")}
|
||||
@ -72,25 +71,38 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
>
|
||||
Server Information
|
||||
</Button>
|
||||
<Button
|
||||
variant={view == "achievements" ? undefined : "ghost"}
|
||||
onClick={() => setView("achievements")}
|
||||
className="ml-2"
|
||||
>
|
||||
Achievements
|
||||
</Button>
|
||||
</div>
|
||||
{(description != "" || discord != "") && (
|
||||
<div className="max-sm:hidden">
|
||||
<div className="grid">
|
||||
<div className="max-sm:hidden">
|
||||
<div className="grid">
|
||||
{(description != "" || discord != "") && (
|
||||
<Button
|
||||
variant={view == "desc" ? undefined : "ghost"}
|
||||
onClick={() => setView("desc")}
|
||||
>
|
||||
Description
|
||||
</Button>
|
||||
<Button
|
||||
variant={view == "extra" ? undefined : "ghost"}
|
||||
onClick={() => setView("extra")}
|
||||
>
|
||||
Server Information
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant={view == "extra" ? undefined : "ghost"}
|
||||
onClick={() => setView("extra")}
|
||||
>
|
||||
Server Information
|
||||
</Button>
|
||||
<Button
|
||||
variant={view == "achievements" ? undefined : "ghost"}
|
||||
onClick={() => setView("achievements")}
|
||||
>
|
||||
Achievements
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-4 pl-4 pr-4 gap-3.5 col-span-5">
|
||||
{description != "" && view == "desc" && (
|
||||
@ -118,7 +130,12 @@ export default function AfterServerView({ server }: { server: string }) {
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}{" "}
|
||||
{((description == "" && discord == "") || view == "extra") && (
|
||||
{view == "achievements" && (
|
||||
<div className="col-span-4">
|
||||
<AchievementList server={server} />
|
||||
</div>
|
||||
)}
|
||||
{view == "extra" && (
|
||||
<div className="sm:grid sm:grid-cols-3 col-span-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
105
src/components/PreferencesSidebar.tsx
Normal file
105
src/components/PreferencesSidebar.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { Cog, ExternalLink, KeyRound, Link, UserPen } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "./ui/resizable";
|
||||
import NextLink from "next/link";
|
||||
import { useClerk } from "@clerk/nextjs";
|
||||
|
||||
export function Sidebar({
|
||||
children,
|
||||
curPage,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
curPage: string;
|
||||
}) {
|
||||
const clerk = useClerk();
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="min-h-[calc(100vh-70px)] pt-[70px]"
|
||||
>
|
||||
<ResizablePanel className="max-md:hidden min-w-[285px] max-w-[285px] w-[285px]">
|
||||
<div className="w-[300px] ml-[10px]">
|
||||
<NextLink href="/account/settings" className="text-inherit">
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant={curPage !== "/account/settings" ? "ghost" : "default"}
|
||||
>
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
</NextLink>
|
||||
<NextLink href="/account/settings/options" className="text-inherit">
|
||||
<Button
|
||||
className="mb-[2px] w-[250px] "
|
||||
variant={
|
||||
curPage !== "/account/settings/options" ? "ghost" : "default"
|
||||
}
|
||||
>
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
</NextLink>
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<UserPen size={16} className="mr-2" /> Profile{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mb-[2px] w-[250px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<KeyRound size={16} className="mr-2" /> Security{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="max-md:hidden" />
|
||||
<ResizablePanel>
|
||||
<div className="md:hidden ml-2">
|
||||
<NextLink href="/account/settings" className="text-inherit">
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant={curPage !== "/account/settings" ? "ghost" : "default"}
|
||||
>
|
||||
<Link size={16} className="mr-2" /> Linking
|
||||
</Button>
|
||||
</NextLink>
|
||||
<NextLink href="/account/settings/options" className="text-inherit">
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant={
|
||||
curPage !== "/account/settings/options" ? "ghost" : "default"
|
||||
}
|
||||
>
|
||||
<Cog size={16} className="mr-2" /> Options
|
||||
</Button>
|
||||
</NextLink>
|
||||
<Button
|
||||
className="mr-[2px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<UserPen size={16} className="mr-2" /> Profile{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-[2px] mb-[30px]"
|
||||
variant="ghost"
|
||||
onClick={() => clerk.openUserProfile({})}
|
||||
>
|
||||
<KeyRound size={16} className="mr-2" /> Security{" "}
|
||||
<ExternalLink size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
{children}{" "}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
189
src/components/SLCustomizePage.tsx
Normal file
189
src/components/SLCustomizePage.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { setAccountSL } from "@/lib/api";
|
||||
import toast from "react-hot-toast";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
export function SLCustomize() {
|
||||
const [padding, setPadding] = useState("0");
|
||||
const [itemsPerRow, setItemsPerRow] = useState("4");
|
||||
const [usePaddingOnSides, setUsePaddingOnSides] = useState(false);
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const { user } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
setItemsPerRow((user?.publicMetadata.ipr as string | undefined) || "4");
|
||||
setPadding((user?.publicMetadata.pad as string | undefined) || "0");
|
||||
setUsePaddingOnSides(
|
||||
(user?.publicMetadata.srv as boolean | undefined) || false
|
||||
);
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (padding) setAccountSL(Number(padding), "pad");
|
||||
if (usePaddingOnSides) setAccountSL(usePaddingOnSides, "srv");
|
||||
if (itemsPerRow) setAccountSL(Number(itemsPerRow), "ipr");
|
||||
toast.success("Set account preferences");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle>Server Display Settings</CardTitle>
|
||||
<CardDescription className="pt-2">
|
||||
Customize how servers are displayed in the list
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="advanced-mode"
|
||||
checked={advanced}
|
||||
onCheckedChange={setAdvanced}
|
||||
/>
|
||||
<Label htmlFor="advanced-mode">Advanced Mode</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Padding of servers</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adjust the spacing between server items in the list.
|
||||
</p>
|
||||
<RadioGroup value={padding} onValueChange={setPadding}>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="0" id="padding-normal" />
|
||||
<Label htmlFor="padding-normal">
|
||||
Normal{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">(0px)</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Standard spacing, balanced appearance.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="15" id="padding-relaxed" />
|
||||
<Label htmlFor="padding-normal">
|
||||
Relaxed{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">(15px)</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
More spacious, easier to distinguish between servers.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="40" id="padding-comfortable" />
|
||||
<Label htmlFor="padding-comfortable">
|
||||
Comfortable{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">(40px)</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Better for larger screens.
|
||||
</p>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Items per row</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Set how many server items appear in each row of the list.
|
||||
</p>
|
||||
<RadioGroup value={itemsPerRow} onValueChange={setItemsPerRow}>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="4" id="items-4" />
|
||||
<Label htmlFor="items-4">
|
||||
4 items{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">per row</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Balanced size and quantity, suitable for most screens.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="5" id="items-5" />
|
||||
<Label htmlFor="items-5">
|
||||
5 items{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">per row</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
More compact view, fit more servers on screen.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<RadioGroupItem value="6" id="items-6" />
|
||||
<Label htmlFor="items-6">
|
||||
6 items{" "}
|
||||
{advanced && (
|
||||
<span className="text-muted-foreground">per row</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
Great for monitors/screens with more space.
|
||||
</p>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="use-padding-sides"
|
||||
checked={usePaddingOnSides}
|
||||
onCheckedChange={(checked) =>
|
||||
setUsePaddingOnSides(checked as boolean)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="use-padding-sides"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Use padding on the sides of only the servers, not the whole
|
||||
server list
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground ml-6">
|
||||
When enabled, applies padding to individual server items instead
|
||||
of the entire list container. This can create a more distinct
|
||||
separation between servers.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit">Save Settings</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -70,6 +70,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { SignInPopover } from "./clerk/SignInPopoverButton";
|
||||
import { BentoCard, BentoGrid } from "./effects/bento-grid";
|
||||
import { pageFind } from "./misc/Link";
|
||||
|
||||
const features = [
|
||||
{
|
||||
@ -227,7 +228,7 @@ export default function ServerList() {
|
||||
<div className="p-0 branding-hero">
|
||||
<>
|
||||
{(!isSignedIn || hero) && (
|
||||
<div className=" py-[300px] relative mx-auto mt-20 max-w-7xl px-6 text-center md:px-8 ">
|
||||
<div className=" pb-[300px] relative mx-auto mt-20 max-w-7xl px-6 text-center md:px-8 ">
|
||||
<Particles
|
||||
className="absolute inset-0 -z-10 block"
|
||||
quantity={100}
|
||||
@ -320,7 +321,9 @@ export default function ServerList() {
|
||||
"border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05] " +
|
||||
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
|
||||
)}
|
||||
onClick={() => router.push(`/server/${server.name}`)}
|
||||
onClick={() =>
|
||||
router.push(pageFind(`Server:${server.name}`))
|
||||
}
|
||||
>
|
||||
<div className="items-center gap-2 p-4">
|
||||
<div>
|
||||
|
||||
131
src/components/feat/AchievementList.tsx
Normal file
131
src/components/feat/AchievementList.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { getAchievements } from "@/lib/api";
|
||||
import type { Achievement } from "@/lib/types/achievement";
|
||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { Medal, Sparkle, Sparkles, Users } from "lucide-react";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import A from "../misc/Link";
|
||||
|
||||
export default function AchievementList({ server }: { server: string }) {
|
||||
const [achievements, setAchievements] = useState<
|
||||
Array<WithInterval<Achievement>>
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffectOnce(() => {
|
||||
setAchievements(() => []);
|
||||
getAchievements(server).then((v) => {
|
||||
for (const a of v)
|
||||
a.achievements.forEach((item, interval) =>
|
||||
setAchievements((prev) => [...prev, { interval, ...item }])
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div>
|
||||
<Skeleton className="w-full h-[112px] my-4" />
|
||||
<Skeleton className="w-full h-[112px] my-4" />
|
||||
<Skeleton className="w-full h-[112px] my-4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
Achievements are earned automatically when the server is online. See{" "}
|
||||
<A alt="Achievement collection">Special:Root</A>
|
||||
</span>
|
||||
{achievements
|
||||
.filter(
|
||||
(value, index) => listify(achievements).indexOf(value.type) === index
|
||||
)
|
||||
.map((a) => {
|
||||
const Icon = formalNames[a.type].icon;
|
||||
return (
|
||||
<div key={`${a.date}--${a.interval}`}>
|
||||
<Card className="my-4">
|
||||
<CardContent className="pt-4">
|
||||
<span
|
||||
className="flex items-center"
|
||||
style={{ color: formalNames[a.type].color }}
|
||||
>
|
||||
<Icon size={16} className="mr-2" />
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formalNames[a.type].title,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<p>{formalNames[a.type].description}</p>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Achieved on {new Date(a.date).getMonth()}/
|
||||
{new Date(a.date).getDate()}/
|
||||
{new Date(a.date).getFullYear()}{" "}
|
||||
<span className="text-muted-foreground/70">
|
||||
{new Date(a.date).toLocaleTimeString()}
|
||||
</span>
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formalNames = {
|
||||
mostJoined: {
|
||||
title:
|
||||
"At one time, <b>this server had the most players on the platform!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had the number 1 permission at the time of the achievements getting resolved.",
|
||||
color: "#9aedff",
|
||||
icon: Medal,
|
||||
},
|
||||
has1kFavorites: {
|
||||
title: "This server has more than <b>1,000 favorites on MHSF!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 1,000 favorites at the time of the achievements getting resolved.",
|
||||
color: "#d064ff",
|
||||
icon: Sparkle,
|
||||
},
|
||||
has1kTotalJoins: {
|
||||
title: "This server has more than <b>1,000 total joins on Minehut!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 1,000 total joins at the time of the achievements getting resolved.",
|
||||
color: "#aefa1f",
|
||||
icon: Users,
|
||||
},
|
||||
has100kFavorites: {
|
||||
title: "This server has more than <b>100,000 favorites on MHSF!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 100,000 favorites at the time of the achievements getting resolved.",
|
||||
color: "#fa5b07",
|
||||
icon: Sparkles,
|
||||
},
|
||||
has100kTotalJoins: {
|
||||
title: "This server has more than <b>100,000 total joins on Minehut!</b>",
|
||||
description:
|
||||
"This is awarded to servers that had 100,000 total joins at the time of the achievements getting resolved.",
|
||||
color: "#bdcffa",
|
||||
icon: Users,
|
||||
},
|
||||
};
|
||||
|
||||
type WithInterval<K> = K & {
|
||||
interval: number;
|
||||
};
|
||||
|
||||
const listify = (list: WithInterval<Achievement>[]) => {
|
||||
const newL: Array<string> = [];
|
||||
|
||||
list.forEach((c) => newL.push(c.type));
|
||||
|
||||
return newL;
|
||||
};
|
||||
@ -43,8 +43,8 @@ export function DiscordPopover({ server, get }: { server: string; get: any }) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(get.discord == null ? "" : get.discord);
|
||||
form.reset({ id: get.discord == null ? "" : get.discord });
|
||||
setValue(get.discord === undefined ? "" : get.discord);
|
||||
form.reset({ id: get.discord === undefined ? "" : get.discord });
|
||||
}, [get]);
|
||||
|
||||
return (
|
||||
|
||||
65
src/components/misc/Link.tsx
Normal file
65
src/components/misc/Link.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { default as NextLink } from "next/link";
|
||||
import { Book, ExternalLink, NotebookText } from "lucide-react";
|
||||
|
||||
export default function A({
|
||||
children,
|
||||
alt,
|
||||
}: {
|
||||
children: string;
|
||||
alt: string | ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<NextLink href={pageFind(children)}>
|
||||
{children.startsWith("Docs:") && <Book />}
|
||||
{alt}
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function ALegacy({
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
children?: string | ReactNode;
|
||||
href?: string;
|
||||
}) {
|
||||
return (
|
||||
<NextLink
|
||||
href={pageFind(href || "")}
|
||||
className="no-underline transition duration-300 hover:underline "
|
||||
>
|
||||
{(href || "").startsWith("Docs:") && (
|
||||
<Book size={16} className="mr-[2px] inline-flex" />
|
||||
)}
|
||||
{(href || "").startsWith("Wiki:") && (
|
||||
<NotebookText size={14} className="mr-[2px] mb-[3px] inline-flex" />
|
||||
)}
|
||||
{children}
|
||||
{(href || "").startsWith("https") && (
|
||||
<ExternalLink size={12} className="ml-[2px] mb-[3px] inline-flex" />
|
||||
)}
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
export const pageFind = (text: string) => {
|
||||
if (text.startsWith("Docs:")) {
|
||||
return "/docs/" + text.substring(5);
|
||||
}
|
||||
if (text === "Special:Root") return "/";
|
||||
if (text === "Special:Preferences") return "/account/settings";
|
||||
if (text === "Special:AccountOptions") return "/account/settings/options";
|
||||
if (text.startsWith("Server:") && text.endsWith("/Customization"))
|
||||
return "/server/" + text.substring(7, text.length - 14) + "/customization";
|
||||
if (text.startsWith("Server:")) return "/server/" + text.substring(7);
|
||||
if (text.startsWith("Wiki:"))
|
||||
return "https://minehut.wiki.gg/wiki/" + text.substring(5);
|
||||
if (text.startsWith("GitHub:"))
|
||||
return "https://github.com/" + text.substring(7);
|
||||
if (text === "Special:GitHub")
|
||||
return "https://github.com/DeveloLongScript/MHSF";
|
||||
if (text.startsWith("Special:GitHub/"))
|
||||
return "https://github.com/DeveloLongScript/MHSF/" + text.substring(15);
|
||||
return text;
|
||||
};
|
||||
30
src/components/misc/MDXElements.tsx
Normal file
30
src/components/misc/MDXElements.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Book, ExternalLink, NotebookText } from "lucide-react";
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
type MDXElementType = {
|
||||
[key: string]: (props: any) => JSX.Element;
|
||||
};
|
||||
|
||||
const Discord = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
viewBox="0 0 256 199"
|
||||
width="1em"
|
||||
height="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
className="inline-block mb-1"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
|
||||
fill="#5865F2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MDXElements: MDXElementType = {
|
||||
Discord,
|
||||
Book: (props) => <Book {...props} />,
|
||||
Notebook: (props) => <NotebookText {...props} />,
|
||||
ExternalLink: (props) => <ExternalLink {...props} />,
|
||||
};
|
||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none 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-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@ -1,48 +1,52 @@
|
||||
export const allFolders: (DocsFolder | Docs)[] = [
|
||||
{
|
||||
title: "Getting Started",
|
||||
url: "/docs/getting-started",
|
||||
},
|
||||
{
|
||||
name: "Guides",
|
||||
docs: [
|
||||
{
|
||||
title: "Linking",
|
||||
url: "/docs/guides/linking",
|
||||
},
|
||||
{
|
||||
title: "Owning a Server",
|
||||
url: "/docs/guides/owning-a-server",
|
||||
},
|
||||
{
|
||||
title: "Server Customization",
|
||||
url: "/docs/guides/customization",
|
||||
},
|
||||
{ title: "Reporting a server", url: "/docs/guides/reporting-server" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Advanced",
|
||||
docs: [
|
||||
{ title: "Tech Stack", url: "/docs/advanced/tech-stack" },
|
||||
{ title: "Using the Command-bar", url: "/docs/advanced/command-bar" },
|
||||
{ title: "Tips with external servers", url: "/docs/advanced/external"}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Legal",
|
||||
docs: [
|
||||
{ title: "ECA Agreement", url: "/docs/legal/external-content-agreement" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Getting Started",
|
||||
url: "/docs/getting-started",
|
||||
},
|
||||
{
|
||||
title: "Reading",
|
||||
url: "/docs/reading",
|
||||
},
|
||||
{
|
||||
name: "Guides",
|
||||
docs: [
|
||||
{
|
||||
title: "Linking",
|
||||
url: "/docs/guides/linking",
|
||||
},
|
||||
{
|
||||
title: "Owning a Server",
|
||||
url: "/docs/guides/owning-a-server",
|
||||
},
|
||||
{
|
||||
title: "Server Customization",
|
||||
url: "/docs/guides/customization",
|
||||
},
|
||||
{ title: "Reporting a server", url: "/docs/guides/reporting-server" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Advanced",
|
||||
docs: [
|
||||
{ title: "Tech Stack", url: "/docs/advanced/tech-stack" },
|
||||
{ title: "Using the Command-bar", url: "/docs/advanced/command-bar" },
|
||||
{ title: "Tips with external servers", url: "/docs/advanced/external" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Legal",
|
||||
docs: [
|
||||
{ title: "ECA Agreement", url: "/docs/legal/external-content-agreement" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export type Docs = {
|
||||
title: string;
|
||||
url: string;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type DocsFolder = {
|
||||
name: string;
|
||||
docs: Array<Docs>;
|
||||
name: string;
|
||||
docs: Array<Docs>;
|
||||
};
|
||||
|
||||
@ -15,62 +15,62 @@ const serverCache: any = {};
|
||||
// __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 var allTags: Array<{
|
||||
name: (server: OnlineServer) => Promise<string>;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
listCondition?: (server: ServerResponse) => Promise<boolean>;
|
||||
tooltipDesc: string;
|
||||
htmlDocs: string;
|
||||
docsName: string;
|
||||
primary: boolean;
|
||||
role?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "red"
|
||||
| "orange"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "lime"
|
||||
| "blue"
|
||||
| "teal"
|
||||
| "cyan"
|
||||
| "violet"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "fuchsia"
|
||||
| "pink";
|
||||
__disab?: boolean;
|
||||
__filter?: boolean;
|
||||
export const allTags: Array<{
|
||||
name: (server: OnlineServer) => Promise<string>;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
listCondition?: (server: ServerResponse) => Promise<boolean>;
|
||||
tooltipDesc: string;
|
||||
htmlDocs: string;
|
||||
docsName: string;
|
||||
primary: boolean;
|
||||
role?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "red"
|
||||
| "orange"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "lime"
|
||||
| "blue"
|
||||
| "teal"
|
||||
| "cyan"
|
||||
| "violet"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "fuchsia"
|
||||
| "pink";
|
||||
__disab?: boolean;
|
||||
__filter?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: async () => "Always Online",
|
||||
condition: async (b: any) => b.staticInfo.alwaysOnline,
|
||||
tooltipDesc:
|
||||
'"Always online" means that the server will not shut down until the plan associated with it expires.',
|
||||
htmlDocs: `
|
||||
{
|
||||
name: async () => "Always Online",
|
||||
condition: async (b: any) => b.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: "secondary",
|
||||
__disab: true,
|
||||
},
|
||||
{
|
||||
name: async (s) => s.staticInfo.planMaxPlayers + " max players",
|
||||
condition: async (s) => s.staticInfo.planMaxPlayers != 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: "secondary",
|
||||
__filter: true,
|
||||
},
|
||||
// deprecated
|
||||
/**{
|
||||
primary: true,
|
||||
docsName: "Always Online",
|
||||
role: "secondary",
|
||||
__disab: true,
|
||||
},
|
||||
{
|
||||
name: async (s) => s.staticInfo.planMaxPlayers + " max players",
|
||||
condition: async (s) => s.staticInfo.planMaxPlayers != 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: "secondary",
|
||||
__filter: true,
|
||||
},
|
||||
// deprecated
|
||||
/**{
|
||||
name: async () => "Velocity",
|
||||
condition: async (s) => {
|
||||
var type = await requestServer(s);
|
||||
@ -86,176 +86,176 @@ export var allTags: Array<{
|
||||
}, */
|
||||
];
|
||||
|
||||
export var 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";
|
||||
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";
|
||||
}> = [
|
||||
{
|
||||
name: "Farming",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("farming");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "SMP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("smp");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Factions",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("factions");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Meme",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("meme");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Puzzle",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("puzzle");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Box",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("box");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Minigames",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("minigames");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "RPG",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("rpg");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Parkour",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("parkour");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Lifesteal",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("lifesteal");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Prison",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("prison");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Gens",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("gens");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Skyblock",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("skyblock");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Roleplay",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("roleplay");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "PvP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("pvp");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Modded",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("modded");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Creative",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("creative");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Farming",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("farming");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "SMP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("smp");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Factions",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("factions");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Meme",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("meme");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Puzzle",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("puzzle");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Box",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("box");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Minigames",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("minigames");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "RPG",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("rpg");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Parkour",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("parkour");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Lifesteal",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("lifesteal");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Prison",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("prison");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Gens",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("gens");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Skyblock",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("skyblock");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Roleplay",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("roleplay");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "PvP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("pvp");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Modded",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("modded");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Creative",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("creative");
|
||||
},
|
||||
primary: true,
|
||||
role: "secondary",
|
||||
},
|
||||
];
|
||||
|
||||
async function requestServer(s: OnlineServer): Promise<ServerResponse> {
|
||||
if (serverCache[s.name] == undefined) {
|
||||
const re = await fetch(
|
||||
"https://api.minehut.com/server/" + s.name + "?byName=true"
|
||||
);
|
||||
const json = await re.json();
|
||||
serverCache[s.name] = json.server;
|
||||
return json.server;
|
||||
} else {
|
||||
return serverCache[s.name];
|
||||
}
|
||||
if (serverCache[s.name] == undefined) {
|
||||
const re = await fetch(
|
||||
"https://api.minehut.com/server/" + s.name + "?byName=true",
|
||||
);
|
||||
const json = await re.json();
|
||||
serverCache[s.name] = json.server;
|
||||
return json.server;
|
||||
} else {
|
||||
return serverCache[s.name];
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,12 +5,30 @@
|
||||
*/
|
||||
//
|
||||
|
||||
import { Achievement } from "./types/achievement";
|
||||
|
||||
const connector = (
|
||||
endpoint: string,
|
||||
options: { version: number; starting?: string },
|
||||
) =>
|
||||
`${options.starting == undefined ? "/" : `${options.starting}/`}api/v${options.version}${endpoint}`;
|
||||
|
||||
async function apiConstructor<K>(
|
||||
connector: string,
|
||||
requestInit: RequestInit,
|
||||
modifier: (data: any) => K,
|
||||
): Promise<K> {
|
||||
try {
|
||||
const response = await fetch(connector, requestInit);
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw Error("Error while running API");
|
||||
}
|
||||
return modifier(await response.json());
|
||||
} catch {
|
||||
throw Error("Error while running API");
|
||||
}
|
||||
}
|
||||
export async function getMOTDFromServer(
|
||||
list: Array<{ server: string; motd: string }>,
|
||||
): Promise<Array<{ server: string; motd: string }>> {
|
||||
@ -428,3 +446,10 @@ export async function setAccountSL(
|
||||
throw Error("Error while running API");
|
||||
}
|
||||
}
|
||||
|
||||
export const getAchievements = async (server: string) =>
|
||||
apiConstructor<{ _id: string; name: string; achievements: Achievement[] }[]>(
|
||||
connector(`/achievements/${server}`, { version: 0 }),
|
||||
{},
|
||||
(data) => data.result,
|
||||
);
|
||||
|
||||
18
src/lib/types/achievement.ts
Normal file
18
src/lib/types/achievement.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export type Achievement = {
|
||||
type:
|
||||
| "mostJoined"
|
||||
| "has1kFavorites"
|
||||
| "has1kTotalJoins"
|
||||
| "has100kFavorites"
|
||||
| "has100kTotalJoins";
|
||||
/** The ISO time of the gaining of the achievement. */
|
||||
date: string;
|
||||
};
|
||||
|
||||
export const orderOfAchievements = [
|
||||
"mostJoined",
|
||||
"has100kFavorites",
|
||||
"has100kTotalJoins",
|
||||
"has1kFavorites",
|
||||
"has1kTotalJoins",
|
||||
];
|
||||
18
src/pages/api/v0/achievements/[server].ts
Normal file
18
src/pages/api/v0/achievements/[server].ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { server } = req.query;
|
||||
if (!server) return res.status(400).send({ error: "No server was provided" });
|
||||
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db("mhsf");
|
||||
const collection = db.collection("achievements");
|
||||
|
||||
res.send({ result: await collection.find({ name: server }).toArray() });
|
||||
}
|
||||
1
src/pages/api/v0/achievements/multiple.ts
Normal file
1
src/pages/api/v0/achievements/multiple.ts
Normal file
@ -0,0 +1 @@
|
||||
// TODO: make multiple endpoint to allow achievements to be shown on the server-list
|
||||
151
yarn.lock
151
yarn.lock
@ -150,6 +150,60 @@
|
||||
"@babel/helper-validator-identifier" "^7.24.7"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@biomejs/biome@^1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.8.3.tgz#3b5eecea90d973f71618aae3e6e8be4d2ca23e42"
|
||||
integrity sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==
|
||||
optionalDependencies:
|
||||
"@biomejs/cli-darwin-arm64" "1.8.3"
|
||||
"@biomejs/cli-darwin-x64" "1.8.3"
|
||||
"@biomejs/cli-linux-arm64" "1.8.3"
|
||||
"@biomejs/cli-linux-arm64-musl" "1.8.3"
|
||||
"@biomejs/cli-linux-x64" "1.8.3"
|
||||
"@biomejs/cli-linux-x64-musl" "1.8.3"
|
||||
"@biomejs/cli-win32-arm64" "1.8.3"
|
||||
"@biomejs/cli-win32-x64" "1.8.3"
|
||||
|
||||
"@biomejs/cli-darwin-arm64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz#be2bfdd445cd2d3cb0ff41a96a72ec761753997c"
|
||||
integrity sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==
|
||||
|
||||
"@biomejs/cli-darwin-x64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz#47d408edd9f5c04069fbcf8610bacf1db8c6c0d9"
|
||||
integrity sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz#44df284383d57cf4f28daeedd080dad7be05df78"
|
||||
integrity sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==
|
||||
|
||||
"@biomejs/cli-linux-arm64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz#6a6b1da1dfce0294a028cbb5d6c40d73691dd713"
|
||||
integrity sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==
|
||||
|
||||
"@biomejs/cli-linux-x64-musl@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz#ceef30a8ee1a00d4ad31e32dd31ba2a661f2719d"
|
||||
integrity sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==
|
||||
|
||||
"@biomejs/cli-linux-x64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz#665df74d19fb8f83001a9d80824d3a1723e2123f"
|
||||
integrity sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==
|
||||
|
||||
"@biomejs/cli-win32-arm64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz#0fb6f58990f4de0331a6ed22c47c66f5a89133cc"
|
||||
integrity sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==
|
||||
|
||||
"@biomejs/cli-win32-x64@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz#6a9dc5a4e13357277da43c015cd5cdc374035448"
|
||||
integrity sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==
|
||||
|
||||
"@clerk/backend@1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/@clerk/backend/-/backend-1.2.2.tgz"
|
||||
@ -1089,20 +1143,19 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
|
||||
"@radix-ui/react-checkbox@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz"
|
||||
integrity sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==
|
||||
"@radix-ui/react-checkbox@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.1.tgz#a559c4303957d797acee99914480b755aa1f27d6"
|
||||
integrity sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collection@1.0.3":
|
||||
version "1.0.3"
|
||||
@ -1506,29 +1559,28 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
|
||||
"@radix-ui/react-primitive@2.0.0":
|
||||
"@radix-ui/react-primitive@2.0.0", "@radix-ui/react-primitive@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
|
||||
integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==
|
||||
dependencies:
|
||||
"@radix-ui/react-slot" "1.1.0"
|
||||
|
||||
"@radix-ui/react-radio-group@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz"
|
||||
integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==
|
||||
"@radix-ui/react-radio-group@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.2.0.tgz#f937dd6b9436ded80c4bebdf3901c20cb8bcbb5a"
|
||||
integrity sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-direction" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-roving-focus" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.0.4":
|
||||
version "1.0.4"
|
||||
@ -1599,6 +1651,19 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
|
||||
"@radix-ui/react-switch@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz#fcf8e778500f1d60d4b2bec2fc3fad77a7c118e3"
|
||||
integrity sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-previous" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
|
||||
"@radix-ui/react-tabs@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz#0a6db1caed56776a1176aae68532060e301cc1c0"
|
||||
@ -1693,6 +1758,11 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"
|
||||
integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==
|
||||
|
||||
"@radix-ui/react-use-rect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz"
|
||||
@ -1930,6 +2000,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/luxon@~3.4.0":
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7"
|
||||
integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==
|
||||
|
||||
"@types/mdast@^3.0.0":
|
||||
version "3.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
|
||||
@ -2846,6 +2921,14 @@ core-util-is@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
cron@^3.1.7:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.7.tgz#3423d618ba625e78458fff8cb67001672d49ba0d"
|
||||
integrity sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==
|
||||
dependencies:
|
||||
"@types/luxon" "~3.4.0"
|
||||
luxon "~3.4.0"
|
||||
|
||||
cross-fetch@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
|
||||
@ -4905,6 +4988,11 @@ lucide-react@^0.416.0:
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.416.0.tgz#657da248f9b862703d7d80aafb912e79ad886313"
|
||||
integrity sha512-wPWxTzdss1CTz2aqcNWNlbh4YSnH9neJWP3RaeXepxpLCTW+pmu7WcT/wxJe+Q7Y7DqGOxAqakJv0pIK3431Ag==
|
||||
|
||||
luxon@~3.4.0:
|
||||
version "3.4.4"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af"
|
||||
integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==
|
||||
|
||||
magic-bytes.js@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92"
|
||||
@ -6552,6 +6640,11 @@ react-fade-in@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fade-in/-/react-fade-in-2.0.1.tgz#b4bcd7dac63d6857ebcd68facbff2f5f9616278f"
|
||||
integrity sha512-oqS/WT4znaXEHmL+yo0IDUDY7uC9K4RP35j1SdRUEBspR09B2iIC0i8oJ28tPOr6Ez/L2aktF9p89j+DbsTVNw==
|
||||
|
||||
react-fast-marquee@^1.6.5:
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-marquee/-/react-fast-marquee-1.6.5.tgz#98929ae93eef087a607a71e9d45ab76bba97dc16"
|
||||
integrity sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==
|
||||
|
||||
react-hook-form@^7.52.2:
|
||||
version "7.52.2"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.2.tgz#ff40f4776250b86ddfcde6be68d34aa82b1c60fe"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user