forked from simrail/simrail.pro
feat(): Profiles page, added hover informations on user icons, show 50 records in leaderboard.
This commit is contained in:
parent
9f8308fe93
commit
fe0d59cad3
@ -71,7 +71,7 @@ export class ActivePlayersRoute
|
|||||||
|
|
||||||
if (s)
|
if (s)
|
||||||
{
|
{
|
||||||
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.trainName) || c.test(d.trainNumber) ).length === s.length);
|
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.trainName) || c.test(d.trainNumber)).length === s.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
a = arrayGroupBy(a, d => d.server);
|
a = arrayGroupBy(a, d => d.server);
|
||||||
@ -110,7 +110,7 @@ export class ActivePlayersRoute
|
|||||||
|
|
||||||
if (s)
|
if (s)
|
||||||
{
|
{
|
||||||
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.stationName) || c.test(d.stationShort) ).length === s.length);
|
a = a.filter(d => s.filter(c => c.test(d.server) || c.test(d.username) || c.test(d.steam) || c.test(d.steam) || c.test(d.stationName) || c.test(d.stationShort)).length === s.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ const sortyByMap: Record<string, any> = {
|
|||||||
time: { trainTime: -1 },
|
time: { trainTime: -1 },
|
||||||
points: { trainPoints: -1 },
|
points: { trainPoints: -1 },
|
||||||
distance: { trainDistance: -1 },
|
distance: { trainDistance: -1 },
|
||||||
}
|
};
|
||||||
|
|
||||||
export class LeaderboardRoute
|
export class LeaderboardRoute
|
||||||
{
|
{
|
||||||
@ -48,9 +48,9 @@ export class LeaderboardRoute
|
|||||||
const filter: PipelineStage[] = [
|
const filter: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
flags: { $nin: ["hidden", "leaderboard_hidden"] }
|
flags: { $nin: [ "hidden", "leaderboard_hidden" ] },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
s && filter.push({
|
s && filter.push({
|
||||||
@ -61,11 +61,11 @@ export class LeaderboardRoute
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortBy = sortyByMap[req.query.s?.toString() ?? 'distance'] ?? sortyByMap.distance;
|
const sortBy = sortyByMap[ req.query.s?.toString() ?? "distance" ] ?? sortyByMap.distance;
|
||||||
|
|
||||||
const records = await MProfile.aggregate(filter)
|
const records = await MProfile.aggregate(filter)
|
||||||
.sort(sortBy)
|
.sort(sortBy)
|
||||||
.limit(10);
|
.limit(50);
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||||
@ -75,7 +75,6 @@ export class LeaderboardRoute
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.get("/station", async (req, res) =>
|
app.get("/station", async (req, res) =>
|
||||||
{
|
{
|
||||||
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
@ -83,9 +82,9 @@ export class LeaderboardRoute
|
|||||||
const filter: PipelineStage[] = [
|
const filter: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
flags: { $nin: ["hidden", "leaderboard_hidden"] }
|
flags: { $nin: [ "hidden", "leaderboard_hidden" ] },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
s && filter.push({
|
s && filter.push({
|
||||||
$match: {
|
$match: {
|
||||||
@ -97,7 +96,7 @@ export class LeaderboardRoute
|
|||||||
|
|
||||||
const records = await MProfile.aggregate(filter)
|
const records = await MProfile.aggregate(filter)
|
||||||
.sort({ dispatcherTime: -1 })
|
.sort({ dispatcherTime: -1 })
|
||||||
.limit(10);
|
.limit(50);
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||||
|
@ -15,8 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
import { PipelineStage } from "mongoose";
|
||||||
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js";
|
||||||
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
import { PlayerUtil } from "../../util/PlayerUtil.js";
|
||||||
|
import { IProfile, MProfile } from "../../mongo/profile.js";
|
||||||
|
import { escapeRegexString, removeProperties } from "../../util/functions.js";
|
||||||
|
|
||||||
|
const generateSearch = (regex: RegExp) => [
|
||||||
|
{
|
||||||
|
id: { $regex: regex },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: { $regex: regex },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
export class ProfilesRoute
|
export class ProfilesRoute
|
||||||
{
|
{
|
||||||
@ -40,14 +53,14 @@ export class ProfilesRoute
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.flags.includes('hidden'))
|
if (player.flags.includes("hidden"))
|
||||||
{
|
{
|
||||||
res.status(403).json(new ErrorResponseBuilder()
|
res.status(403).json(new ErrorResponseBuilder()
|
||||||
.setCode(403).setData("Profile blocked!"));
|
.setCode(403).setData("Profile blocked!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.flags.includes('private'))
|
if (player.flags.includes("private"))
|
||||||
{
|
{
|
||||||
res.status(404).json(new ErrorResponseBuilder()
|
res.status(404).json(new ErrorResponseBuilder()
|
||||||
.setCode(404).setData("Profile is private!"));
|
.setCode(404).setData("Profile is private!"));
|
||||||
@ -58,12 +71,43 @@ export class ProfilesRoute
|
|||||||
new SuccessResponseBuilder()
|
new SuccessResponseBuilder()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({
|
.setData({
|
||||||
player
|
player,
|
||||||
})
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/", async (req, res) =>
|
||||||
|
{
|
||||||
|
const s = req.query.q?.toString().split(",").map(x => new RegExp(escapeRegexString(x), "i"));
|
||||||
|
|
||||||
|
const filter: PipelineStage[] = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
flags: { $nin: [ "hidden" ] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
s && filter.push({
|
||||||
|
$match: {
|
||||||
|
$and: [
|
||||||
|
...s.map(x => ({ $or: generateSearch(x) })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const records = await MProfile.aggregate(filter)
|
||||||
|
.limit(50);
|
||||||
|
|
||||||
|
res.json(
|
||||||
|
new SuccessResponseBuilder<{ records: Omit<IProfile, "_id" | "__v">[] }>()
|
||||||
|
.setCode(200)
|
||||||
|
.setData({ records: records.map(x => removeProperties<Omit<IProfile, "_id" | "__v">>(x, [ "_id", "__v" ])) })
|
||||||
|
.toJSON(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -67,7 +67,7 @@ export class TrainsRoute
|
|||||||
new SuccessResponseBuilder<{ records: ITrainLog[] }>()
|
new SuccessResponseBuilder<{ records: ITrainLog[] }>()
|
||||||
.setCode(200)
|
.setCode(200)
|
||||||
.setData({
|
.setData({
|
||||||
records
|
records,
|
||||||
})
|
})
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
);
|
);
|
||||||
|
@ -104,7 +104,10 @@ export class StationsModule
|
|||||||
|
|
||||||
player.flags = player.flags.filter(x => x !== "private");
|
player.flags = player.flags.filter(x => x !== "private");
|
||||||
|
|
||||||
if (typeof player.createdAt !== 'number') player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
if (typeof player.createdAt !== "number")
|
||||||
|
{
|
||||||
|
player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||||
|
@ -61,10 +61,14 @@ export class TrainsModule
|
|||||||
const vehicleName = getVehicle(vehicle) ?? vehicle;
|
const vehicleName = getVehicle(vehicle) ?? vehicle;
|
||||||
|
|
||||||
if (!isTruthyAndGreaterThanZero(distance))
|
if (!isTruthyAndGreaterThanZero(distance))
|
||||||
|
{
|
||||||
distance = 0;
|
distance = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isTruthyAndGreaterThanZero(points))
|
if (!isTruthyAndGreaterThanZero(points))
|
||||||
|
{
|
||||||
points = 0;
|
points = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!player.trainStats)
|
if (!player.trainStats)
|
||||||
@ -133,14 +137,17 @@ export class TrainsModule
|
|||||||
|
|
||||||
player.flags = player.flags.filter(x => x !== "private");
|
player.flags = player.flags.filter(x => x !== "private");
|
||||||
|
|
||||||
if (typeof player.createdAt !== 'number') player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
if (typeof player.createdAt !== "number")
|
||||||
|
{
|
||||||
|
player.createdAt = new Date(parseInt(player._id.toString().substring(0, 8), 16) * 1000).getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
const playerData = await PlayerUtil.getPlayerSteamData(player.id);
|
||||||
|
|
||||||
!stats && !player.flags.includes('private') && player.flags.push("private");
|
!stats && !player.flags.includes("private") && player.flags.push("private");
|
||||||
|
|
||||||
player.flags = [...new Set(player.flags)];
|
player.flags = [ ...new Set(player.flags) ];
|
||||||
|
|
||||||
player.username = playerData?.personaname ?? player.username;
|
player.username = playerData?.personaname ?? player.username;
|
||||||
player.avatar = playerData?.avatarfull ?? player.avatar;
|
player.avatar = playerData?.avatarfull ?? player.avatar;
|
||||||
|
@ -35,6 +35,6 @@ export const MAdmin = model<IAdmin>("admin", schema);
|
|||||||
|
|
||||||
export interface IAdmin
|
export interface IAdmin
|
||||||
{
|
{
|
||||||
token: string
|
token: string;
|
||||||
username: string
|
username: string;
|
||||||
}
|
}
|
@ -55,8 +55,8 @@ export const raw_schema = {
|
|||||||
|
|
||||||
player: {
|
player: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: "profile"
|
ref: "profile",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = new Schema<IStationLog>(raw_schema);
|
const schema = new Schema<IStationLog>(raw_schema);
|
||||||
|
@ -63,8 +63,8 @@ export const raw_schema = {
|
|||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: "profile"
|
ref: "profile",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = new Schema<ITrainLog>(raw_schema);
|
const schema = new Schema<ITrainLog>(raw_schema);
|
||||||
|
@ -85,14 +85,14 @@ export class PlayerUtil
|
|||||||
|
|
||||||
if (stats)
|
if (stats)
|
||||||
{
|
{
|
||||||
trainStats['N/A'] = {
|
trainStats[ "N/A" ] = {
|
||||||
score: stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0,
|
score: stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0,
|
||||||
distance: stats?.stats?.find(x => x.name === "DISTANCE_M")?.value ?? 0,
|
distance: stats?.stats?.find(x => x.name === "DISTANCE_M")?.value ?? 0,
|
||||||
time: 0,
|
time: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatcherStats['N/A'] = {
|
dispatcherStats[ "N/A" ] = {
|
||||||
time: (stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0) * 1000 * 60
|
time: (stats?.stats?.find(x => x.name === "DISPATCHER_TIME")?.value ?? 0) * 1000 * 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
trainPoints = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
trainPoints = stats?.stats?.find(x => x.name === "SCORE")?.value ?? 0;
|
||||||
|
@ -65,7 +65,8 @@ export class SimrailClient extends EventEmitter
|
|||||||
public constructor()
|
public constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.setup().then(() => {
|
this.setup().then(() =>
|
||||||
|
{
|
||||||
void this.update(false);
|
void this.update(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,38 +77,48 @@ export class SimrailClient extends EventEmitter
|
|||||||
|
|
||||||
private async setup()
|
private async setup()
|
||||||
{
|
{
|
||||||
if (!await redis.get('last_updated')) {
|
if (!await redis.get("last_updated"))
|
||||||
await redis.json.set('trains_occupied', '$', {});
|
{
|
||||||
await redis.json.set('trains', '$', []);
|
await redis.json.set("trains_occupied", "$", {});
|
||||||
await redis.json.set('stations', '$', []);
|
await redis.json.set("trains", "$", []);
|
||||||
await redis.json.set('stations_occupied', '$', {});
|
await redis.json.set("stations", "$", []);
|
||||||
|
await redis.json.set("stations_occupied", "$", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUpdated = Date.now() - (Number(await redis.get('last_updated')) ?? 0);
|
const lastUpdated = Date.now() - (Number(await redis.get("last_updated")) ?? 0);
|
||||||
|
|
||||||
if (lastUpdated > 300_000) {
|
if (lastUpdated > 300_000)
|
||||||
console.log('REDIS: last updated more than > 5 mins');
|
{
|
||||||
await redis.json.set('trains_occupied', '$', {});
|
console.log("REDIS: last updated more than > 5 mins");
|
||||||
await redis.json.set('trains', '$', []);
|
await redis.json.set("trains_occupied", "$", {});
|
||||||
await redis.json.set('stations', '$', []);
|
await redis.json.set("trains", "$", []);
|
||||||
await redis.json.set('stations_occupied', '$', {});
|
await redis.json.set("stations", "$", []);
|
||||||
|
await redis.json.set("stations_occupied", "$", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await redis.json.get('stations'))
|
if (!await redis.json.get("stations"))
|
||||||
redis.json.set('stations', '$', []);
|
{
|
||||||
if (!await redis.json.get('trains'))
|
redis.json.set("stations", "$", []);
|
||||||
redis.json.set('trains', '$', []);
|
}
|
||||||
if (!await redis.json.get('trains_occupied'))
|
if (!await redis.json.get("trains"))
|
||||||
redis.json.set('trains_occupied', '$', {});
|
{
|
||||||
if (!await redis.json.get('stations_occupied'))
|
redis.json.set("trains", "$", []);
|
||||||
redis.json.set('stations_occupied', '$', {});
|
}
|
||||||
|
if (!await redis.json.get("trains_occupied"))
|
||||||
|
{
|
||||||
|
redis.json.set("trains_occupied", "$", {});
|
||||||
|
}
|
||||||
|
if (!await redis.json.get("stations_occupied"))
|
||||||
|
{
|
||||||
|
redis.json.set("stations_occupied", "$", {});
|
||||||
|
}
|
||||||
|
|
||||||
this.stations = (await redis.json.get('stations') as unknown as SimrailClient['stations']);
|
this.stations = (await redis.json.get("stations") as unknown as SimrailClient["stations"]);
|
||||||
this.stationsOccupied = (await redis.json.get('stations_occupied') as unknown as SimrailClient['stationsOccupied']);
|
this.stationsOccupied = (await redis.json.get("stations_occupied") as unknown as SimrailClient["stationsOccupied"]);
|
||||||
this.trains = (await redis.json.get('trains') as unknown as SimrailClient['trains']);
|
this.trains = (await redis.json.get("trains") as unknown as SimrailClient["trains"]);
|
||||||
this.trainsOccupied = (await redis.json.get('trains_occupied') as unknown as SimrailClient['trainsOccupied']);
|
this.trainsOccupied = (await redis.json.get("trains_occupied") as unknown as SimrailClient["trainsOccupied"]);
|
||||||
|
|
||||||
redis.set('last_updated', Date.now().toString());
|
redis.set("last_updated", Date.now().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processStation(server: Server, stations: ApiResponse<Station>)
|
private async processStation(server: Server, stations: ApiResponse<Station>)
|
||||||
@ -127,7 +138,7 @@ export class SimrailClient extends EventEmitter
|
|||||||
{
|
{
|
||||||
this.stations[ server.ServerCode ] = stations.data;
|
this.stations[ server.ServerCode ] = stations.data;
|
||||||
redis.json.set("stations", "$", this.stations);
|
redis.json.set("stations", "$", this.stations);
|
||||||
redis.set('last_updated', Date.now().toString());
|
redis.set("last_updated", Date.now().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const x of stations.data)
|
for (const x of stations.data)
|
||||||
@ -167,7 +178,7 @@ export class SimrailClient extends EventEmitter
|
|||||||
|
|
||||||
this.stations[ server.ServerCode ] = stations.data;
|
this.stations[ server.ServerCode ] = stations.data;
|
||||||
redis.json.set("stations", "$", this.stations);
|
redis.json.set("stations", "$", this.stations);
|
||||||
redis.set('last_updated', Date.now().toString());
|
redis.set("last_updated", Date.now().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +199,7 @@ export class SimrailClient extends EventEmitter
|
|||||||
{
|
{
|
||||||
this.trains[ server.ServerCode ] = trains.data;
|
this.trains[ server.ServerCode ] = trains.data;
|
||||||
redis.json.set("trains", "$", this.trains);
|
redis.json.set("trains", "$", this.trains);
|
||||||
redis.set('last_updated', Date.now().toString());
|
redis.set("last_updated", Date.now().toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,13 +263,15 @@ export class SimrailClient extends EventEmitter
|
|||||||
let points = oldPoints ? (playerStats?.stats.find(x => x.name === "SCORE")?.value ?? 0) - oldPoints : 0;
|
let points = oldPoints ? (playerStats?.stats.find(x => x.name === "SCORE")?.value ?? 0) - oldPoints : 0;
|
||||||
|
|
||||||
|
|
||||||
if (distance < 0) {
|
if (distance < 0)
|
||||||
console.warn(`Player ${playerId}, Train ${data.TrainNoLocal} - distance < 0`);
|
{
|
||||||
|
console.warn(`Player ${ playerId }, Train ${ data.TrainNoLocal } - distance < 0`);
|
||||||
distance = 0;
|
distance = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (points < 0) {
|
if (points < 0)
|
||||||
console.warn(`Player ${playerId}, Train ${data.TrainNoLocal} - distance < 0`);
|
{
|
||||||
|
console.warn(`Player ${ playerId }, Train ${ data.TrainNoLocal } - distance < 0`);
|
||||||
points = 0;
|
points = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,14 +286,14 @@ export class SimrailClient extends EventEmitter
|
|||||||
this.trains[ server.ServerCode ] = trains.data;
|
this.trains[ server.ServerCode ] = trains.data;
|
||||||
redis.json.set("trains", "$", this.trains);
|
redis.json.set("trains", "$", this.trains);
|
||||||
redis.json.set("trains_occupied", "$", this.trainsOccupied);
|
redis.json.set("trains_occupied", "$", this.trainsOccupied);
|
||||||
redis.set('last_updated', Date.now().toString());
|
redis.set("last_updated", Date.now().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async update(needSetup: boolean = false)
|
private async update(needSetup: boolean = false)
|
||||||
{
|
{
|
||||||
const servers = (await fetch("https://panel.simrail.eu:8084/servers-open").then(x => x.json()).catch(() => ({ data: [], result: false })) as ApiResponse<Server>)
|
const servers = (await fetch("https://panel.simrail.eu:8084/servers-open").then(x => x.json()).catch(() => ({ data: [], result: false })) as ApiResponse<Server>)
|
||||||
.data ?? [] //?.filter(x => x.ServerName.includes("Polski")) ?? []; // TODO: remove this in v3
|
.data ?? []; //?.filter(x => x.ServerName.includes("Polski")) ?? []; // TODO: remove this in v3
|
||||||
|
|
||||||
if (!servers.length)
|
if (!servers.length)
|
||||||
{
|
{
|
||||||
@ -290,7 +303,7 @@ export class SimrailClient extends EventEmitter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needSetup)
|
if (needSetup)
|
||||||
{
|
{
|
||||||
await this.setup();
|
await this.setup();
|
||||||
}
|
}
|
||||||
@ -299,8 +312,8 @@ export class SimrailClient extends EventEmitter
|
|||||||
// TODO: check performance
|
// TODO: check performance
|
||||||
for (const server of servers)
|
for (const server of servers)
|
||||||
{
|
{
|
||||||
const stations = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Station>;
|
const stations = (await fetch("https://panel.simrail.eu:8084/stations-open?serverCode=" + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Station>;
|
||||||
const trains = (await fetch('https://panel.simrail.eu:8084/trains-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Train>;
|
const trains = (await fetch("https://panel.simrail.eu:8084/trains-open?serverCode=" + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Train>;
|
||||||
|
|
||||||
await this.processStation(server, stations);
|
await this.processStation(server, stations);
|
||||||
await this.processTrain(server, trains);
|
await this.processTrain(server, trains);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import wcmatch from 'wildcard-match'
|
import wcmatch from "wildcard-match";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
E186_134 = "Traxx/E186-134",
|
E186_134 = "Traxx/E186-134",
|
||||||
@ -48,85 +48,82 @@ E186_134 = "Traxx/E186-134",
|
|||||||
|
|
||||||
export const trainsList = [
|
export const trainsList = [
|
||||||
{
|
{
|
||||||
train: 'Traxx (E186)',
|
train: "Traxx (E186)",
|
||||||
pattern: [
|
pattern: [
|
||||||
'Traxx/E186-*',
|
"Traxx/E186-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'Dragon2 (E6ACTa, E6ACTadb)',
|
train: "Dragon2 (E6ACTa, E6ACTadb)",
|
||||||
pattern: [
|
pattern: [
|
||||||
'Dragon2/E6ACTa-*',
|
"Dragon2/E6ACTa-*",
|
||||||
'Dragon2/E6ACTadb-*'
|
"Dragon2/E6ACTadb-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'Dragon2 (ET25)',
|
train: "Dragon2 (ET25)",
|
||||||
pattern: [
|
pattern: [
|
||||||
'Dragon2/ET25-*',
|
"Dragon2/ET25-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'Pendolino (ED250)',
|
train: "Pendolino (ED250)",
|
||||||
pattern: [
|
pattern: [
|
||||||
'Pendolino/ED250-*',
|
"Pendolino/ED250-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'EN57',
|
train: "EN57",
|
||||||
pattern: [
|
pattern: [
|
||||||
'EN57/EN57-*',
|
"EN57/EN57-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'EN71',
|
train: "EN71",
|
||||||
pattern: [
|
pattern: [
|
||||||
'EN57/EN71-*',
|
"EN57/EN71-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'EN76',
|
train: "EN76",
|
||||||
pattern: [
|
pattern: [
|
||||||
'Elf/EN76-*',
|
"Elf/EN76-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'EN96',
|
train: "EN96",
|
||||||
pattern: [
|
pattern: [
|
||||||
'Elf/EN96-*',
|
"Elf/EN96-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
train: 'EP07',
|
train: "EP07",
|
||||||
pattern: [
|
pattern: [
|
||||||
'4E/EP07-*',
|
"4E/EP07-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
train: 'EP08',
|
train: "EP08",
|
||||||
pattern: [
|
pattern: [
|
||||||
'4E/EP08-*',
|
"4E/EP08-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
train: 'ET22',
|
train: "ET22",
|
||||||
pattern: [
|
pattern: [
|
||||||
'201E/ET22-*',
|
"201E/ET22-*",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
train: 'EU07',
|
train: "EU07",
|
||||||
pattern: [
|
pattern: [
|
||||||
'4E/EU07-*',
|
"4E/EU07-*",
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
|
|
||||||
export const getVehicle = (name: string) => {
|
export const getVehicle = (name: string) =>
|
||||||
|
{
|
||||||
return trainsList.find(x => wcmatch(x.pattern)(name))?.train;
|
return trainsList.find(x => wcmatch(x.pattern)(name))?.train;
|
||||||
};
|
};
|
@ -26,17 +26,23 @@ export const removeProperties = <T>(data: any, names: string[]) =>
|
|||||||
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
||||||
export const escapeRegexString = (str: string) => {
|
export const escapeRegexString = (str: string) =>
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
{
|
||||||
}
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
};
|
||||||
|
|
||||||
export const isTruthyAndGreaterThanZero = (data: number) => {
|
export const isTruthyAndGreaterThanZero = (data: number) =>
|
||||||
if (!data) return false;
|
{
|
||||||
|
if (!data)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return data > 0;
|
return data > 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const arrayGroupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) =>
|
export const arrayGroupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) =>
|
||||||
Object.values((array.reduce((acc, value, index, array) => {
|
Object.values((array.reduce((acc, value, index, array) =>
|
||||||
(acc[predicate(value, index, array)] ||= []).push(value);
|
{
|
||||||
|
(acc[ predicate(value, index, array) ] ||= []).push(value);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: T[] }))).flat();
|
}, {} as { [ key: string ]: T[] }))).flat();
|
@ -9,25 +9,18 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"apexcharts": "^3.41.0",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"headlessui": "^0.0.0",
|
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"match-sorter": "^6.3.1",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-apexcharts": "^1.4.1",
|
|
||||||
"react-cookie": "^7.2.2",
|
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-hot-toast": "^2.4.1",
|
|
||||||
"react-i18next": "^15.0.2",
|
"react-i18next": "^15.0.2",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-toastify": "^10.0.6",
|
"react-toastify": "^10.0.6",
|
||||||
"sort-by": "^0.0.2",
|
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"use-debounce": "^10.0.4"
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@ import { TrainLeaderboard } from "./pages/leaderboard/TrainLeaderboard.tsx";
|
|||||||
import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx";
|
import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx";
|
||||||
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
|
import { TrainLogs } from "./pages/logs/TrainLogs.tsx";
|
||||||
import { StationLogs } from "./pages/logs/StationLogs.tsx";
|
import { StationLogs } from "./pages/logs/StationLogs.tsx";
|
||||||
import { Profile } from "./pages/profile/Profile.tsx";
|
import { Profile } from "./pages/profiles/Profile.tsx";
|
||||||
import { Log } from "./pages/log/Log.tsx";
|
import { Log } from "./pages/log/Log.tsx";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
@ -36,6 +36,7 @@ import { ActiveStationsPlayers } from "./pages/activePlayers/ActiveStationsPlaye
|
|||||||
import { ActiveTrainPlayers } from "./pages/activePlayers/ActiveTrainPlayers.tsx";
|
import { ActiveTrainPlayers } from "./pages/activePlayers/ActiveTrainPlayers.tsx";
|
||||||
import { AuthProvider } from "./hooks/useAuth.tsx";
|
import { AuthProvider } from "./hooks/useAuth.tsx";
|
||||||
import { NotFoundError } from "./pages/errors/NotFound.tsx";
|
import { NotFoundError } from "./pages/errors/NotFound.tsx";
|
||||||
|
import { Profiles } from "./pages/profiles/Profiles.tsx";
|
||||||
|
|
||||||
function App()
|
function App()
|
||||||
{
|
{
|
||||||
@ -55,135 +56,147 @@ function App()
|
|||||||
|
|
||||||
return <HelmetProvider>
|
return <HelmetProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
{ loading ? (
|
{ loading ? (
|
||||||
<Loader/>
|
<Loader/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="top-center"
|
position="top-center"
|
||||||
autoClose={ 1500 }
|
autoClose={ 1500 }
|
||||||
hideProgressBar={ false }
|
hideProgressBar={ false }
|
||||||
newestOnTop={ false }
|
newestOnTop={ false }
|
||||||
closeOnClick
|
closeOnClick
|
||||||
rtl={ false }
|
rtl={ false }
|
||||||
pauseOnHover
|
pauseOnHover
|
||||||
theme={ theme as "light" | "dark" }
|
theme={ theme as "light" | "dark" }
|
||||||
/>
|
/>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Home"
|
<PageMeta title="simrail.pro | Home"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<Home/>
|
<Home/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/leaderboard/trains"
|
path="/leaderboard/trains"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Train Leaderboard"
|
<PageMeta title="simrail.pro | Train Leaderboard"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<TrainLeaderboard/>
|
<TrainLeaderboard/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/logs/trains"
|
path="/logs/trains"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Trains Logs"
|
<PageMeta title="simrail.pro | Trains Logs"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<TrainLogs/>
|
<TrainLogs/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/logs/stations"
|
path="/logs/stations"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Stations Logs"
|
<PageMeta title="simrail.pro | Stations Logs"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<StationLogs/>
|
<StationLogs/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/leaderboard/stations"
|
path="/leaderboard/stations"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Station Leaderboard"
|
<PageMeta title="simrail.pro | Station Leaderboard"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<StationLeaderboard/>
|
<StationLeaderboard/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/active/trains"
|
path="/active/trains"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Active Trains"
|
<PageMeta title="simrail.pro | Active Trains"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<ActiveTrainPlayers/>
|
<ActiveTrainPlayers/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/active/stations"
|
path="/active/stations"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Active Station"
|
<PageMeta title="simrail.pro | Active Station"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
<ActiveStationsPlayers/>
|
<ActiveStationsPlayers/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/profile/:id"
|
path="/profile/:id"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Profile"
|
<PageMeta title="simrail.pro | Profile"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
{/* page meta is modified in component! */ }
|
{/* page meta is modified in component! */ }
|
||||||
<Profile/>
|
<Profile/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/log/:id"
|
path="/profiles/"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageMeta title="simrail.pro | Log"
|
<PageMeta title="simrail.pro | Profiles"
|
||||||
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
{/* page title is modified after API response */ }
|
{/* page meta is modified in component! */ }
|
||||||
<Log/>
|
<Profiles/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="/log/:id"
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<NotFoundError/>
|
<PageMeta title="simrail.pro | Log"
|
||||||
</>
|
description="Simrail Stats - The best SimRail logs and statistics site!"/>
|
||||||
}
|
{/* page title is modified after API response */ }
|
||||||
/>
|
<Log/>
|
||||||
</Routes>
|
</>
|
||||||
</DefaultLayout>
|
}
|
||||||
</>
|
/>
|
||||||
) }
|
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<NotFoundError/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</DefaultLayout>
|
||||||
|
</>
|
||||||
|
) }
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</HelmetProvider>;
|
</HelmetProvider>;
|
||||||
}
|
}
|
||||||
|
15
packages/frontend/src/components/mini/alerts/Info.tsx
Normal file
15
packages/frontend/src/components/mini/alerts/Info.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const SuccessAlert = ({ title, description }: { title: string, description: string }) =>
|
||||||
|
<div
|
||||||
|
className="flex w-full border-l-6 border-[#34D399] bg-[#34D399] bg-opacity-[15%] dark:bg-[#1B1B24] px-7 py-8 shadow-md dark:bg-opacity-30 md:p-9">
|
||||||
|
<div className="mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#34D399]">
|
||||||
|
<SuccessAlertIcon/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<h5 className="mb-3 text-lg font-semibold text-black dark:text-[#34D399] ">
|
||||||
|
{ title }
|
||||||
|
</h5>
|
||||||
|
<p className="text-base leading-relaxed text-body">
|
||||||
|
{ description }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>;
|
43
packages/frontend/src/components/mini/icons/UserIcons.tsx
Normal file
43
packages/frontend/src/components/mini/icons/UserIcons.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
import { ReactNode, useState } from "react";
|
||||||
|
import { FaUserShield, FaUserSlash, FaUserLock } from "react-icons/fa6";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const UseHover = ({ children, hover }: { children: ReactNode, hover: ReactNode }) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(true);
|
||||||
|
|
||||||
|
|
||||||
|
return <div className={"inline"} onMouseEnter={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(true)}>
|
||||||
|
{isHovered ? children : hover}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserIcons = ({ flags }: { flags: string[] }) =>
|
||||||
|
{
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <> { flags.includes('administrator') &&
|
||||||
|
<UseHover hover={ <button
|
||||||
|
className="inline-flex rounded-full bg-[#DC3545] py-1 px-3 text-sm font-medium text-white hover:bg-opacity-90">{t("icons.admin")}</button> }><FaUserShield
|
||||||
|
className={ "inline text-meta-1 ml-1" }/></UseHover> } { flags.includes("leaderboard_hidden") &&
|
||||||
|
<UseHover hover={ <button
|
||||||
|
className="inline-flex rounded-full bg-[#F9C107] py-1 px-3 text-sm font-medium text-[#212B36] hover:bg-opacity-90">{t("icons.leaderboard_hidden")}</button> }><FaUserLock
|
||||||
|
className={ "inline text-meta-6 ml-1" }/></UseHover> } { flags.includes("hidden") &&
|
||||||
|
<UseHover hover={ <button
|
||||||
|
className="inline-flex rounded-full bg-[#DC3545] py-1 px-3 text-sm font-medium text-white hover:bg-opacity-90">{ t("icons.hidden") }</button> }>
|
||||||
|
<FaUserSlash className={ "inline text-meta-1 ml-1" }/></UseHover> }</>;
|
||||||
|
};
|
@ -1,61 +1,72 @@
|
|||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: { showModal: boolean; setShowModal: Dispatch<SetStateAction<boolean>>; onConfirm: () => void; title: string; description: string; }) => {
|
export const ConfirmModal = ({ showModal, setShowModal, onConfirm, title, description }: {
|
||||||
|
showModal: boolean;
|
||||||
|
setShowModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}) =>
|
||||||
|
{
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showModal ? (
|
{ showModal ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="relative w-auto my-6 mx-auto max-w-3xl">
|
<div className="relative w-auto my-6 mx-auto max-w-3xl">
|
||||||
|
|
||||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-strokedark outline-none focus:outline-none">
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-strokedark outline-none focus:outline-none">
|
||||||
|
|
||||||
<div className="flex items-start justify-between p-5">
|
<div className="flex items-start justify-between p-5">
|
||||||
<h3 className="text-3xl font-semibold text-meta-2">
|
<h3 className="text-3xl font-semibold text-meta-2">
|
||||||
{ title }
|
{ title }
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
className="p-1 ml-auto bg-transparent border-0 text-meta-2 opacity-5 float-right text-3xl leading-none font-semibold outline-none focus:outline-none"
|
className="p-1 ml-auto bg-transparent border-0 text-meta-2 opacity-5 float-right text-3xl leading-none font-semibold outline-none focus:outline-none"
|
||||||
onClick={() => setShowModal(false)}
|
onClick={ () => setShowModal(false) }
|
||||||
>
|
>
|
||||||
<span className="bg-transparent text-meta-2 opacity-5 h-6 w-6 text-2xl block outline-none focus:outline-none">
|
<span className="bg-transparent text-meta-2 opacity-5 h-6 w-6 text-2xl block outline-none focus:outline-none">
|
||||||
×
|
×
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative p-6 flex-auto">
|
<div className="relative p-6 flex-auto">
|
||||||
<p className="my-4 text-blueGray-500 text-lg leading-relaxed text-meta-2">
|
<p className="my-4 text-blueGray-500 text-lg leading-relaxed text-meta-2">
|
||||||
{ description }
|
{ description }
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end p-6 rounded-b">
|
<div className="flex items-center justify-end p-6 rounded-b">
|
||||||
<button
|
<button
|
||||||
className="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
className="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowModal(false)}
|
onClick={ () => setShowModal(false) }
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
className="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setShowModal(false); onConfirm(); }}
|
onClick={ () =>
|
||||||
>
|
{
|
||||||
Confirm
|
setShowModal(false);
|
||||||
</button>
|
onConfirm();
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
</div>
|
</>
|
||||||
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
) : null }
|
||||||
</>
|
</>
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
|
import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx";
|
||||||
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
|
import { ArrowIcon } from "../icons/ArrowIcon.tsx";
|
||||||
import { FaHome, FaClipboardList } from "react-icons/fa";
|
import { FaHome, FaClipboardList } from "react-icons/fa";
|
||||||
import { FaChartSimple, FaTrain, FaBuildingFlag, FaBolt } from "react-icons/fa6";
|
import { FaChartSimple, FaTrain, FaBuildingFlag, FaBolt, FaUsers } from "react-icons/fa6";
|
||||||
import { useAuth } from "../../../hooks/useAuth.tsx";
|
import { useAuth } from "../../../hooks/useAuth.tsx";
|
||||||
|
|
||||||
interface SidebarProps
|
interface SidebarProps
|
||||||
@ -101,8 +101,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={ sidebar }
|
ref={ sidebar }
|
||||||
className={ `absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
className={ `absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${ sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
{/* <!-- SIDEBAR HEADER --> */ }
|
{/* <!-- SIDEBAR HEADER --> */ }
|
||||||
@ -126,9 +125,8 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
<li>
|
<li>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/"
|
to="/"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${ pathname === "/" &&
|
||||||
pathname === "/" &&
|
"bg-graydark dark:bg-meta-4"
|
||||||
"bg-graydark dark:bg-meta-4"
|
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<FaHome/>
|
<FaHome/>
|
||||||
@ -143,7 +141,85 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
{ t("sidebar.info") }
|
{ t("sidebar.info") }
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/profiles"
|
||||||
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${ pathname === "/profiles" &&
|
||||||
|
"bg-graydark dark:bg-meta-4"
|
||||||
|
}` }
|
||||||
|
>
|
||||||
|
<FaUsers/>
|
||||||
|
{ t("sidebar.profiles") }
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
<SidebarLinkGroup
|
<SidebarLinkGroup
|
||||||
|
isOpen={ true }
|
||||||
|
|
||||||
|
activeCondition={
|
||||||
|
pathname === "/leaderboard" || pathname.includes("leaderboard")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ (handleClick, open) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<NavLink
|
||||||
|
to="#"
|
||||||
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${ (pathname === "/leaderboard" ||
|
||||||
|
(pathname.includes("leaderboard/") && !pathname.includes("leaderboard/steam"))) &&
|
||||||
|
"bg-graydark dark:bg-meta-4"
|
||||||
|
}` }
|
||||||
|
onClick={ (e) =>
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
sidebarExpanded
|
||||||
|
? handleClick()
|
||||||
|
: setSidebarExpanded(true);
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<FaChartSimple/>
|
||||||
|
{ t("sidebar.leaderboard") }
|
||||||
|
<ArrowIcon rotated={ open }/>
|
||||||
|
</NavLink>
|
||||||
|
<div
|
||||||
|
className={ `translate transform overflow-hidden ${ !open && "hidden"
|
||||||
|
}` }
|
||||||
|
>
|
||||||
|
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/leaderboard/stations"
|
||||||
|
className={ ({ isActive }) =>
|
||||||
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
|
(isActive && "!text-white")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaBuildingFlag/>
|
||||||
|
{ t("sidebar.stations") }
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/leaderboard/trains"
|
||||||
|
className={ ({ isActive }) =>
|
||||||
|
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
||||||
|
(isActive && "!text-white")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaTrain/>
|
||||||
|
{ t("sidebar.trains") }
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</SidebarLinkGroup>
|
||||||
|
|
||||||
|
<SidebarLinkGroup
|
||||||
|
isOpen={ true }
|
||||||
activeCondition={
|
activeCondition={
|
||||||
pathname === "/logs" || pathname.includes("logs")
|
pathname === "/logs" || pathname.includes("logs")
|
||||||
}
|
}
|
||||||
@ -154,10 +230,9 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="#"
|
to="#"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${ (pathname === "/logs" ||
|
||||||
(pathname === "/logs" ||
|
pathname.includes("logs")) &&
|
||||||
pathname.includes("logs")) &&
|
"bg-graydark dark:bg-meta-4"
|
||||||
"bg-graydark dark:bg-meta-4"
|
|
||||||
}` }
|
}` }
|
||||||
onClick={ (e) =>
|
onClick={ (e) =>
|
||||||
{
|
{
|
||||||
@ -173,8 +248,7 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={ `translate transform overflow-hidden ${
|
className={ `translate transform overflow-hidden ${ !open && "hidden"
|
||||||
!open && "hidden"
|
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||||
@ -210,72 +284,8 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
</SidebarLinkGroup>
|
</SidebarLinkGroup>
|
||||||
|
|
||||||
<SidebarLinkGroup
|
<SidebarLinkGroup
|
||||||
activeCondition={
|
isOpen={ true }
|
||||||
pathname === "/leaderboard" || pathname.includes("leaderboard")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ (handleClick, open) =>
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<NavLink
|
|
||||||
to="#"
|
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
|
||||||
(pathname === "/leaderboard" ||
|
|
||||||
(pathname.includes("leaderboard/") && !pathname.includes('leaderboard/steam'))) &&
|
|
||||||
"bg-graydark dark:bg-meta-4"
|
|
||||||
}` }
|
|
||||||
onClick={ (e) =>
|
|
||||||
{
|
|
||||||
e.preventDefault();
|
|
||||||
sidebarExpanded
|
|
||||||
? handleClick()
|
|
||||||
: setSidebarExpanded(true);
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
<FaChartSimple/>
|
|
||||||
{ t("sidebar.leaderboard") }
|
|
||||||
<ArrowIcon rotated={ open }/>
|
|
||||||
</NavLink>
|
|
||||||
<div
|
|
||||||
className={ `translate transform overflow-hidden ${
|
|
||||||
!open && "hidden"
|
|
||||||
}` }
|
|
||||||
>
|
|
||||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
|
||||||
<li>
|
|
||||||
<NavLink
|
|
||||||
to="/leaderboard/stations"
|
|
||||||
className={ ({ isActive }) =>
|
|
||||||
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
|
||||||
(isActive && "!text-white")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FaBuildingFlag/>
|
|
||||||
{ t("sidebar.stations") }
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NavLink
|
|
||||||
to="/leaderboard/trains"
|
|
||||||
className={ ({ isActive }) =>
|
|
||||||
"group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white " +
|
|
||||||
(isActive && "!text-white")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FaTrain/>
|
|
||||||
{ t("sidebar.trains") }
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
} }
|
|
||||||
</SidebarLinkGroup>
|
|
||||||
|
|
||||||
|
|
||||||
<SidebarLinkGroup
|
|
||||||
activeCondition={
|
activeCondition={
|
||||||
pathname === "/active/trains" || pathname.includes("active/trains")
|
pathname === "/active/trains" || pathname.includes("active/trains")
|
||||||
}
|
}
|
||||||
@ -286,10 +296,9 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="#"
|
to="#"
|
||||||
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
className={ `group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${ (pathname === "/active" ||
|
||||||
(pathname === "/active" ||
|
pathname.includes("/active/")) &&
|
||||||
pathname.includes("/active/")) &&
|
"bg-graydark dark:bg-meta-4"
|
||||||
"bg-graydark dark:bg-meta-4"
|
|
||||||
}` }
|
}` }
|
||||||
onClick={ (e) =>
|
onClick={ (e) =>
|
||||||
{
|
{
|
||||||
@ -299,13 +308,12 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
: setSidebarExpanded(true);
|
: setSidebarExpanded(true);
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<FaBolt />
|
<FaBolt/>
|
||||||
{ t("sidebar.active_players") }
|
{ t("sidebar.active_players") }
|
||||||
<ArrowIcon rotated={ open }/>
|
<ArrowIcon rotated={ open }/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div
|
<div
|
||||||
className={ `translate transform overflow-hidden ${
|
className={ `translate transform overflow-hidden ${ !open && "hidden"
|
||||||
!open && "hidden"
|
|
||||||
}` }
|
}` }
|
||||||
>
|
>
|
||||||
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
<ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">
|
||||||
@ -347,286 +355,19 @@ export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) =>
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<p className="group relative flex items-center rounded-sm py-2 px-4 text-sm text-bodydark1 duration-300 ease-in-out ">{t("sidebar.logged", { username })}</p>
|
<p className="group relative flex items-center rounded-sm py-2 px-4 text-sm text-bodydark1 duration-300 ease-in-out ">{ t("sidebar.logged", { username }) }</p>
|
||||||
</li>
|
</li>
|
||||||
<button onClick={() => {
|
<button onClick={ () =>
|
||||||
window.localStorage.setItem('auth_token', 'undefined');
|
{
|
||||||
|
window.localStorage.setItem("auth_token", "undefined");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}} className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center text-sm text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
} }
|
||||||
>{t("sidebar.logout")}
|
className="inline-flex items-center justify-center rounded-md bg-primary py-2 px-8 text-center text-sm text-white hover:bg-opacity-90 lg:px-8 xl:px-10"
|
||||||
|
>{ t("sidebar.logout") }
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</ul> }
|
</ul> }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TODO: add admin panel with simple auth */ }
|
|
||||||
{/*{false && <div>*/ }
|
|
||||||
{/* <h3 className="mb-4 ml-4 text-sm font-semibold text-bodydark2">*/ }
|
|
||||||
{/* {t('sidebar.admin')}*/ }
|
|
||||||
{/* </h3>*/ }
|
|
||||||
|
|
||||||
{/* <ul className="mb-6 flex flex-col gap-1.5">*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Menu Item Chart --> */ }
|
|
||||||
{/* <li>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="/chart"*/ }
|
|
||||||
{/* className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${*/ }
|
|
||||||
{/* pathname.includes('chart') && 'bg-graydark dark:bg-meta-4'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <svg*/ }
|
|
||||||
{/* className="fill-current"*/ }
|
|
||||||
{/* width="18"*/ }
|
|
||||||
{/* height="19"*/ }
|
|
||||||
{/* viewBox="0 0 18 19"*/ }
|
|
||||||
{/* fill="none"*/ }
|
|
||||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <g clipPath="url(#clip0_130_9801)">*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M10.8563 0.55835C10.5188 0.55835 10.2095 0.8396 10.2095 1.20522V6.83022C10.2095 7.16773 10.4907 7.4771 10.8563 7.4771H16.8751C17.0438 7.4771 17.2126 7.39272 17.3251 7.28022C17.4376 7.1396 17.4938 6.97085 17.4938 6.8021C17.2688 3.28647 14.3438 0.55835 10.8563 0.55835ZM11.4751 6.15522V1.8521C13.8095 2.13335 15.6938 3.8771 16.1438 6.18335H11.4751V6.15522Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M15.3845 8.7427H9.1126V2.69582C9.1126 2.35832 8.83135 2.07707 8.49385 2.07707C8.40947 2.07707 8.3251 2.07707 8.24072 2.07707C3.96572 2.04895 0.506348 5.53645 0.506348 9.81145C0.506348 14.0864 3.99385 17.5739 8.26885 17.5739C12.5438 17.5739 16.0313 14.0864 16.0313 9.81145C16.0313 9.6427 16.0313 9.47395 16.0032 9.33332C16.0032 8.99582 15.722 8.7427 15.3845 8.7427ZM8.26885 16.3083C4.66885 16.3083 1.77197 13.4114 1.77197 9.81145C1.77197 6.3802 4.47197 3.53957 7.8751 3.3427V9.36145C7.8751 9.69895 8.15635 10.0083 8.52197 10.0083H14.7938C14.6813 13.4958 11.7845 16.3083 8.26885 16.3083Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </g>*/ }
|
|
||||||
{/* <defs>*/ }
|
|
||||||
{/* <clipPath id="clip0_130_9801">*/ }
|
|
||||||
{/* <rect*/ }
|
|
||||||
{/* width="18"*/ }
|
|
||||||
{/* height="18"*/ }
|
|
||||||
{/* fill="white"*/ }
|
|
||||||
{/* transform="translate(0 0.052124)"*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </clipPath>*/ }
|
|
||||||
{/* </defs>*/ }
|
|
||||||
{/* </svg>*/ }
|
|
||||||
{/* Chart*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* </li>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Menu Item Chart --> */ }
|
|
||||||
|
|
||||||
{/* */ }{/*{/* <!-- Menu Item Ui Elements --> */ }
|
|
||||||
{/* <SidebarLinkGroup*/ }
|
|
||||||
{/* activeCondition={pathname === '/ui' || pathname.includes('ui')}*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* {(handleClick, open) => {*/ }
|
|
||||||
{/* return (*/ }
|
|
||||||
{/* <React.Fragment>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="#"*/ }
|
|
||||||
{/* className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${*/ }
|
|
||||||
{/* (pathname === '/ui' || pathname.includes('ui')) &&*/ }
|
|
||||||
{/* 'bg-graydark dark:bg-meta-4'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* onClick={(e) => {*/ }
|
|
||||||
{/* e.preventDefault();*/ }
|
|
||||||
{/* sidebarExpanded*/ }
|
|
||||||
{/* ? handleClick()*/ }
|
|
||||||
{/* : setSidebarExpanded(true);*/ }
|
|
||||||
{/* }}*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <svg*/ }
|
|
||||||
{/* className="fill-current"*/ }
|
|
||||||
{/* width="18"*/ }
|
|
||||||
{/* height="19"*/ }
|
|
||||||
{/* viewBox="0 0 18 19"*/ }
|
|
||||||
{/* fill="none"*/ }
|
|
||||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <g clipPath="url(#clip0_130_9807)">*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M15.7501 0.55835H2.2501C1.29385 0.55835 0.506348 1.34585 0.506348 2.3021V7.53335C0.506348 8.4896 1.29385 9.2771 2.2501 9.2771H15.7501C16.7063 9.2771 17.4938 8.4896 17.4938 7.53335V2.3021C17.4938 1.34585 16.7063 0.55835 15.7501 0.55835ZM16.2563 7.53335C16.2563 7.8146 16.0313 8.0396 15.7501 8.0396H2.2501C1.96885 8.0396 1.74385 7.8146 1.74385 7.53335V2.3021C1.74385 2.02085 1.96885 1.79585 2.2501 1.79585H15.7501C16.0313 1.79585 16.2563 2.02085 16.2563 2.3021V7.53335Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M6.13135 10.9646H2.2501C1.29385 10.9646 0.506348 11.7521 0.506348 12.7083V15.8021C0.506348 16.7583 1.29385 17.5458 2.2501 17.5458H6.13135C7.0876 17.5458 7.8751 16.7583 7.8751 15.8021V12.7083C7.90322 11.7521 7.11572 10.9646 6.13135 10.9646ZM6.6376 15.8021C6.6376 16.0833 6.4126 16.3083 6.13135 16.3083H2.2501C1.96885 16.3083 1.74385 16.0833 1.74385 15.8021V12.7083C1.74385 12.4271 1.96885 12.2021 2.2501 12.2021H6.13135C6.4126 12.2021 6.6376 12.4271 6.6376 12.7083V15.8021Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M15.75 10.9646H11.8688C10.9125 10.9646 10.125 11.7521 10.125 12.7083V15.8021C10.125 16.7583 10.9125 17.5458 11.8688 17.5458H15.75C16.7063 17.5458 17.4938 16.7583 17.4938 15.8021V12.7083C17.4938 11.7521 16.7063 10.9646 15.75 10.9646ZM16.2562 15.8021C16.2562 16.0833 16.0312 16.3083 15.75 16.3083H11.8688C11.5875 16.3083 11.3625 16.0833 11.3625 15.8021V12.7083C11.3625 12.4271 11.5875 12.2021 11.8688 12.2021H15.75C16.0312 12.2021 16.2562 12.4271 16.2562 12.7083V15.8021Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </g>*/ }
|
|
||||||
{/* <defs>*/ }
|
|
||||||
{/* <clipPath id="clip0_130_9807">*/ }
|
|
||||||
{/* <rect*/ }
|
|
||||||
{/* width="18"*/ }
|
|
||||||
{/* height="18"*/ }
|
|
||||||
{/* fill="white"*/ }
|
|
||||||
{/* transform="translate(0 0.052124)"*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </clipPath>*/ }
|
|
||||||
{/* </defs>*/ }
|
|
||||||
{/* </svg>*/ }
|
|
||||||
{/* UI Elements*/ }
|
|
||||||
{/* <svg*/ }
|
|
||||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/ }
|
|
||||||
{/* open && 'rotate-180'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* width="20"*/ }
|
|
||||||
{/* height="20"*/ }
|
|
||||||
{/* viewBox="0 0 20 20"*/ }
|
|
||||||
{/* fill="none"*/ }
|
|
||||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* fillRule="evenodd"*/ }
|
|
||||||
{/* clipRule="evenodd"*/ }
|
|
||||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </svg>*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Dropdown Menu Start --> */ }
|
|
||||||
{/* <div*/ }
|
|
||||||
{/* className={`translate transform overflow-hidden ${*/ }
|
|
||||||
{/* !open && 'hidden'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <ul className="mb-5.5 mt-4 flex flex-col gap-2.5 pl-6">*/ }
|
|
||||||
{/* <li>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="/ui/alerts"*/ }
|
|
||||||
{/* className={({ isActive }) =>*/ }
|
|
||||||
{/* 'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +*/ }
|
|
||||||
{/* (isActive && '!text-white')*/ }
|
|
||||||
{/* }*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* Alerts*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* </li>*/ }
|
|
||||||
{/* <li>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="/ui/buttons"*/ }
|
|
||||||
{/* className={({ isActive }) =>*/ }
|
|
||||||
{/* 'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +*/ }
|
|
||||||
{/* (isActive && '!text-white')*/ }
|
|
||||||
{/* }*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* Buttons*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* </li>*/ }
|
|
||||||
{/* </ul>*/ }
|
|
||||||
{/* </div>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Dropdown Menu End --> */ }
|
|
||||||
{/* </React.Fragment>*/ }
|
|
||||||
{/* );*/ }
|
|
||||||
{/* }}*/ }
|
|
||||||
{/* </SidebarLinkGroup>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Menu Item Ui Elements --> */ }
|
|
||||||
|
|
||||||
{/* */ }{/*{/* <!-- Menu Item Auth Pages --> */ }
|
|
||||||
{/* <SidebarLinkGroup*/ }
|
|
||||||
{/* activeCondition={*/ }
|
|
||||||
{/* pathname === '/auth' || pathname.includes('auth')*/ }
|
|
||||||
{/* }*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* {(handleClick, open) => {*/ }
|
|
||||||
{/* return (*/ }
|
|
||||||
{/* <React.Fragment>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="#"*/ }
|
|
||||||
{/* className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${*/ }
|
|
||||||
{/* (pathname === '/auth' || pathname.includes('auth')) &&*/ }
|
|
||||||
{/* 'bg-graydark dark:bg-meta-4'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* onClick={(e) => {*/ }
|
|
||||||
{/* e.preventDefault();*/ }
|
|
||||||
{/* sidebarExpanded*/ }
|
|
||||||
{/* ? handleClick()*/ }
|
|
||||||
{/* : setSidebarExpanded(true);*/ }
|
|
||||||
{/* }}*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <svg*/ }
|
|
||||||
{/* className="fill-current"*/ }
|
|
||||||
{/* width="18"*/ }
|
|
||||||
{/* height="19"*/ }
|
|
||||||
{/* viewBox="0 0 18 19"*/ }
|
|
||||||
{/* fill="none"*/ }
|
|
||||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <g clipPath="url(#clip0_130_9814)">*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M12.7127 0.55835H9.53457C8.80332 0.55835 8.18457 1.1771 8.18457 1.90835V3.84897C8.18457 4.18647 8.46582 4.46772 8.80332 4.46772C9.14082 4.46772 9.45019 4.18647 9.45019 3.84897V1.88022C9.45019 1.82397 9.47832 1.79585 9.53457 1.79585H12.7127C13.3877 1.79585 13.9221 2.33022 13.9221 3.00522V15.0709C13.9221 15.7459 13.3877 16.2802 12.7127 16.2802H9.53457C9.47832 16.2802 9.45019 16.2521 9.45019 16.1959V14.2552C9.45019 13.9177 9.16894 13.6365 8.80332 13.6365C8.43769 13.6365 8.18457 13.9177 8.18457 14.2552V16.1959C8.18457 16.9271 8.80332 17.5459 9.53457 17.5459H12.7127C14.0908 17.5459 15.1877 16.4209 15.1877 15.0709V3.03335C15.1877 1.65522 14.0627 0.55835 12.7127 0.55835Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* d="M10.4346 8.60205L7.62207 5.7333C7.36895 5.48018 6.97519 5.48018 6.72207 5.7333C6.46895 5.98643 6.46895 6.38018 6.72207 6.6333L8.46582 8.40518H3.45957C3.12207 8.40518 2.84082 8.68643 2.84082 9.02393C2.84082 9.36143 3.12207 9.64268 3.45957 9.64268H8.49395L6.72207 11.4427C6.46895 11.6958 6.46895 12.0896 6.72207 12.3427C6.83457 12.4552 7.00332 12.5114 7.17207 12.5114C7.34082 12.5114 7.50957 12.4552 7.62207 12.3145L10.4346 9.4458C10.6877 9.24893 10.6877 8.85518 10.4346 8.60205Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </g>*/ }
|
|
||||||
{/* <defs>*/ }
|
|
||||||
{/* <clipPath id="clip0_130_9814">*/ }
|
|
||||||
{/* <rect*/ }
|
|
||||||
{/* width="18"*/ }
|
|
||||||
{/* height="18"*/ }
|
|
||||||
{/* fill="white"*/ }
|
|
||||||
{/* transform="translate(0 0.052124)"*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </clipPath>*/ }
|
|
||||||
{/* </defs>*/ }
|
|
||||||
{/* </svg>*/ }
|
|
||||||
{/* Authentication*/ }
|
|
||||||
{/* <svg*/ }
|
|
||||||
{/* className={`absolute right-4 top-1/2 -translate-y-1/2 fill-current ${*/ }
|
|
||||||
{/* open && 'rotate-180'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* width="20"*/ }
|
|
||||||
{/* height="20"*/ }
|
|
||||||
{/* viewBox="0 0 20 20"*/ }
|
|
||||||
{/* fill="none"*/ }
|
|
||||||
{/* xmlns="http://www.w3.org/2000/svg"*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <path*/ }
|
|
||||||
{/* fillRule="evenodd"*/ }
|
|
||||||
{/* clipRule="evenodd"*/ }
|
|
||||||
{/* d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"*/ }
|
|
||||||
{/* fill=""*/ }
|
|
||||||
{/* />*/ }
|
|
||||||
{/* </svg>*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Dropdown Menu Start --> */ }
|
|
||||||
{/* <div*/ }
|
|
||||||
{/* className={`translate transform overflow-hidden ${*/ }
|
|
||||||
{/* !open && 'hidden'*/ }
|
|
||||||
{/* }`}*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* <ul className="mt-4 mb-5.5 flex flex-col gap-2.5 pl-6">*/ }
|
|
||||||
{/* <li>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="/auth/signin"*/ }
|
|
||||||
{/* className={({ isActive }) =>*/ }
|
|
||||||
{/* 'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +*/ }
|
|
||||||
{/* (isActive && '!text-white')*/ }
|
|
||||||
{/* }*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* Sign In*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* </li>*/ }
|
|
||||||
{/* <li>*/ }
|
|
||||||
{/* <NavLink*/ }
|
|
||||||
{/* to="/auth/signup"*/ }
|
|
||||||
{/* className={({ isActive }) =>*/ }
|
|
||||||
{/* 'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +*/ }
|
|
||||||
{/* (isActive && '!text-white')*/ }
|
|
||||||
{/* }*/ }
|
|
||||||
{/* >*/ }
|
|
||||||
{/* Sign Up*/ }
|
|
||||||
{/* </NavLink>*/ }
|
|
||||||
{/* </li>*/ }
|
|
||||||
{/* </ul>*/ }
|
|
||||||
{/* </div>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Dropdown Menu End --> */ }
|
|
||||||
{/* </React.Fragment>*/ }
|
|
||||||
{/* );*/ }
|
|
||||||
{/* }}*/ }
|
|
||||||
{/* </SidebarLinkGroup>*/ }
|
|
||||||
{/* */ }{/*{/* <!-- Menu Item Auth Pages --> */ }
|
|
||||||
{/* </ul>*/ }
|
|
||||||
{/*</div>}*/ }
|
|
||||||
</nav>
|
</nav>
|
||||||
{/* <!-- Sidebar Menu --> */ }
|
{/* <!-- Sidebar Menu --> */ }
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,11 +20,12 @@ interface SidebarLinkGroupProps
|
|||||||
{
|
{
|
||||||
children: (handleClick: () => void, open: boolean) => ReactNode;
|
children: (handleClick: () => void, open: boolean) => ReactNode;
|
||||||
activeCondition: boolean;
|
activeCondition: boolean;
|
||||||
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) =>
|
const SidebarLinkGroup = ({ children, activeCondition, isOpen }: SidebarLinkGroupProps) =>
|
||||||
{
|
{
|
||||||
const [ open, setOpen ] = useState<boolean>(activeCondition);
|
const [ open, setOpen ] = useState<boolean>(isOpen ?? activeCondition);
|
||||||
|
|
||||||
const handleClick = () =>
|
const handleClick = () =>
|
||||||
{
|
{
|
||||||
|
@ -16,23 +16,33 @@
|
|||||||
|
|
||||||
import { ChangeEventHandler } from "react";
|
import { ChangeEventHandler } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FcInfo } from "react-icons/fc";
|
||||||
|
|
||||||
export const Search = ({ searchItem, handleInputChange }: {
|
export const Search = ({ searchItem, handleInputChange }: {
|
||||||
searchItem: string;
|
searchItem: string;
|
||||||
handleInputChange: ChangeEventHandler
|
handleInputChange: ChangeEventHandler,
|
||||||
}) =>
|
}) =>
|
||||||
{
|
{
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return <div
|
return <>
|
||||||
className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
<div
|
||||||
<div className="flex justify-center items-center">
|
className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8">
|
||||||
<input
|
<div className="flex justify-center items-center">
|
||||||
className="w-full rounded border border-stroke bg-gray py-3 pl-5 pr-5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary"
|
<input
|
||||||
type="text"
|
className="w-full rounded border border-stroke bg-gray py-3 pl-5 pr-5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary"
|
||||||
onChange={ handleInputChange }
|
type="text"
|
||||||
value={ searchItem }
|
onChange={ handleInputChange }
|
||||||
placeholder={ t("logs.search") }
|
value={ searchItem }
|
||||||
/>
|
placeholder={ t("search.placeholder") }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex pt-2 gap-1 align-center">
|
||||||
|
<FcInfo/>
|
||||||
|
<p className="text-sm text-black dark:text-white">
|
||||||
|
{ t("search.tip") }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</>;
|
||||||
};
|
};
|
@ -17,7 +17,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TActiveStationPlayersData } from "../../../types/active.ts";
|
import { TActiveStationPlayersData } from "../../../types/active.ts";
|
||||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlayersData[] }) =>
|
export const ActiveStationTable = ({ stations }: { stations: TActiveStationPlayersData[] }) =>
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TActiveTrainPlayersData } from "../../../types/active.ts";
|
import { TActiveTrainPlayersData } from "../../../types/active.ts";
|
||||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
export const ActiveTrainTable = ({ trains }: {
|
export const ActiveTrainTable = ({ trains }: {
|
||||||
trains: TActiveTrainPlayersData[],
|
trains: TActiveTrainPlayersData[],
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
|
export const LeaderboardStationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =>
|
||||||
|
{
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-3">
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.user") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.time") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.actions") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ stations.map((station, key) => (
|
||||||
|
<div
|
||||||
|
className={ `grid grid-cols-2 sm:grid-cols-3 ${ stations.length === (key + 1) // todo: ...
|
||||||
|
? ""
|
||||||
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
|
}` }
|
||||||
|
key={ station.id }
|
||||||
|
>
|
||||||
|
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
<Link to={ "/profile/" + station.id }
|
||||||
|
className="color-orchid">{ station.username }</Link> <UserIcons
|
||||||
|
flags={ station.flags }/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-3">{ formatTime(station.dispatcherTime) }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
|
<Link
|
||||||
|
to={ "/profile/" + station.id }
|
||||||
|
className={ `inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 ${ station.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
|
style={ station.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||||
|
>
|
||||||
|
{ t("leaderboard.profile") }
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||||
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
|
export const LeaderboardTrainTable = ({ trains, setSortBy, sortBy }: {
|
||||||
|
trains: TLeaderboardRecord[],
|
||||||
|
setSortBy: Dispatch<SetStateAction<string>>
|
||||||
|
sortBy: string
|
||||||
|
}) =>
|
||||||
|
{
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.user") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
|
onClick={ () => setSortBy("distance") }>
|
||||||
|
{ t("leaderboard.distance") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortBy === "distance" || !sortBy }/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
|
onClick={ () => setSortBy("points") }>
|
||||||
|
{ t("leaderboard.points") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortBy === "points" }/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||||
|
onClick={ () => setSortBy("time") }>
|
||||||
|
{ t("leaderboard.time") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortBy === "time" }/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("leaderboard.actions") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ trains.map((train, key) => (
|
||||||
|
<div
|
||||||
|
className={ `grid grid-cols-3 sm:grid-cols-5 ${ trains.length === (key + 1)
|
||||||
|
? ""
|
||||||
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
|
}` }
|
||||||
|
key={ train.id }
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
<Link to={ "/profile/" + train.id }
|
||||||
|
className="color-orchid">{ train.username }</Link> <UserIcons
|
||||||
|
flags={ train.flags }/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-6">{ (train.trainDistance / 1000).toFixed(2) }km</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-5">{ train.trainPoints }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-3">{ formatTime(train.trainTime) }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
|
<Link
|
||||||
|
to={ "/profile/" + train.id }
|
||||||
|
className={ `inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 ${ train.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
|
style={ train.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||||
|
>
|
||||||
|
{ t("leaderboard.profile") }
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
;
|
||||||
|
};
|
@ -58,7 +58,8 @@ export const StationTable = ({ stations }: { stations: TLeaderboardRecord[] }) =
|
|||||||
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + station.id }
|
<Link to={ "/profile/" + station.id }
|
||||||
className="color-orchid">{ station.username }</Link> <UserIcons flags={station.flags} />
|
className="color-orchid">{ station.username }</Link> <UserIcons
|
||||||
|
flags={ station.flags }/>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,7 +80,8 @@ export const TrainTable = ({ trains, setSortBy, sortBy }: {
|
|||||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + train.id }
|
<Link to={ "/profile/" + train.id }
|
||||||
className="color-orchid">{ train.username }</Link> <UserIcons flags={train.flags} />
|
className="color-orchid">{ train.username }</Link> <UserIcons
|
||||||
|
flags={ train.flags }/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import { TLogStationData } from "../../../types/log.ts";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
|
|
||||||
export const StationLog = ({ data }: { data: TLogStationData }) =>
|
export const StationLog = ({ data }: { data: TLogStationData }) =>
|
||||||
@ -51,7 +51,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{ data.player.username } <UserIcons flags={data.player.flags} />
|
{ data.player.username } <UserIcons flags={ data.player.flags }/>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +85,7 @@ export const StationLog = ({ data }: { data: TLogStationData }) =>
|
|||||||
{ t("log.buttons.copy") }
|
{ t("log.buttons.copy") }
|
||||||
</a>
|
</a>
|
||||||
<Link
|
<Link
|
||||||
to={"/profile/" + data.player.id}
|
to={ "/profile/" + data.player.id }
|
||||||
className={ `inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10 ${ data.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
className={ `inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10 ${ data.player.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
style={ data.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>
|
style={ data.player.flags.includes("private") ? { pointerEvents: "none" } : undefined }>
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import { TLogTrainData } from "../../../types/log.ts";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
|
|
||||||
export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
||||||
@ -51,7 +51,7 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) =>
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
{ data.player.username } <UserIcons flags={data.player.flags} />
|
{ data.player.username } <UserIcons flags={ data.player.flags }/>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { TStationRecord } from "../../../types/station.ts";
|
import { TStationRecord } from "../../../types/station.ts";
|
||||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const StationTable = ({ stations }: {
|
export const StationTable = ({ stations }: {
|
||||||
@ -65,7 +65,8 @@ export const StationTable = ({ stations }: {
|
|||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
|
<Link to={ "/profile/" + (station.steam ?? station.player.id) }
|
||||||
className="color-orchid">{ station.username ?? station.player.username }</Link> <UserIcons flags={station.player.flags} />
|
className="color-orchid">{ station.username ?? station.player.username }</Link>
|
||||||
|
<UserIcons flags={ station.player.flags }/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { TTrainRecord } from "../../../types/train.ts";
|
import { TTrainRecord } from "../../../types/train.ts";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { UserIcons } from "../../mini/util/UserIcons.tsx";
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
// setSearchItem: Dispatch<SetStateAction<string>>
|
// setSearchItem: Dispatch<SetStateAction<string>>
|
||||||
export const TrainTable = ({ trains }: {
|
export const TrainTable = ({ trains }: {
|
||||||
@ -72,10 +72,12 @@ export const TrainTable = ({ trains }: {
|
|||||||
}` }
|
}` }
|
||||||
key={ train.id }
|
key={ train.id }
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
|
<Link to={ "/profile/" + (train.steam ?? train.player.id) }
|
||||||
className="color-orchid">{ train.username ?? train.player.username }</Link> <UserIcons flags={train.player.flags} />
|
className="color-orchid">{ train.username ?? train.player.username }</Link>
|
||||||
|
<UserIcons flags={ train.player.flags }/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -65,7 +65,8 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return <>
|
return <>
|
||||||
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
|
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
|
||||||
onConfirm={ adminToggleHideLeaderboardPlayerProfile } title={ t("admin.hideLeaderboard.modal.title") }
|
onConfirm={ adminToggleHideLeaderboardPlayerProfile }
|
||||||
|
title={ t("admin.hideLeaderboard.modal.title") }
|
||||||
description={ t("admin.hideLeaderboard.modal.description") }/>
|
description={ t("admin.hideLeaderboard.modal.description") }/>
|
||||||
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
|
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
|
||||||
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
|
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
|
||||||
|
255
packages/frontend/src/components/pages/profiles/Profile.tsx
Normal file
255
packages/frontend/src/components/pages/profiles/Profile.tsx
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TProfileData } from "../../../types/profile.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ArrowIcon, FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||||
|
import { formatTime } from "../../../util/time.ts";
|
||||||
|
import { useAuth } from "../../../hooks/useAuth.tsx";
|
||||||
|
import { ConfirmModal } from "../../mini/modal/ConfirmModal.tsx";
|
||||||
|
import { post } from "../../../util/fetcher.ts";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
|
||||||
|
export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||||
|
{
|
||||||
|
|
||||||
|
const [ showTrains, setShowTrains ] = useState(false);
|
||||||
|
const [ showStations, setShowStations ] = useState(false);
|
||||||
|
const [ sortTrainsBy, setSortTrainsBy ] = useState<"time" | "score" | "distance">("distance");
|
||||||
|
const [ hideLeaderboardStatsModal, setHideLeaderboardStatsModal ] = useState(false);
|
||||||
|
const [ hideProfileModal, setHideProfileModal ] = useState(false);
|
||||||
|
|
||||||
|
const { isAdmin, token } = useAuth();
|
||||||
|
|
||||||
|
const adminToggleHideLeaderboardPlayerProfile = () =>
|
||||||
|
{
|
||||||
|
post(`/admin/profile/${ data.player.id }/${ data.player.flags.includes("leaderboard_hidden") ? "showLeaderboard" : "hideLeaderboard" }`, {}, { "X-Auth-Token": token })
|
||||||
|
.then((response) =>
|
||||||
|
{
|
||||||
|
if (response.code === 200)
|
||||||
|
{
|
||||||
|
toast.success(t("admin.hideLeaderboard.alert"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminHidePlayerProfile = () =>
|
||||||
|
{
|
||||||
|
post(`/admin/profile/${ data.player.id }/hide`, {}, { "X-Auth-Token": token })
|
||||||
|
.then((response) =>
|
||||||
|
{
|
||||||
|
if (response.code === 200)
|
||||||
|
{
|
||||||
|
toast.success(t("admin.hide.alert"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return <>
|
||||||
|
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
|
||||||
|
onConfirm={ adminToggleHideLeaderboardPlayerProfile }
|
||||||
|
title={ t("admin.hideLeaderboard.modal.title") }
|
||||||
|
description={ t("admin.hideLeaderboard.modal.description") }/>
|
||||||
|
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
|
||||||
|
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
|
||||||
|
description={ t("admin.hide.modal.description") }/>
|
||||||
|
<div
|
||||||
|
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
|
<div className="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||||
|
<div
|
||||||
|
className="mx-auto w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-44 sm:p-3">
|
||||||
|
<div className="relative drop-shadow-2">
|
||||||
|
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="mb-1.5 text-2xl font-semibold text-black dark:text-white">
|
||||||
|
{ data.player.username } <UserIcons flags={ data.player.flags }/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="mx-auto mt-4.5 mb-5.5 grid max-w-94 grid-cols-2 rounded-md border border-stroke py-2.5 shadow-1 dark:border-strokedark dark:bg-[#37404F]">
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
||||||
|
<span className="font-semibold text-black dark:text-white">
|
||||||
|
{ Math.floor(data.player.trainDistance / 1000) }km
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-wrap">{ t("profile.stats.distance") }</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
||||||
|
<span className="font-semibold text-black dark:text-white">
|
||||||
|
{ formatTime(data.player.dispatcherTime) }
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-wrap">{ t("profile.stats.time") }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ Object.keys(data.player.trainStats || {}).length > 0 &&
|
||||||
|
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
|
<div className="group relative cursor-pointer" onClick={ () => setShowTrains(val => !val) }>
|
||||||
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.trains.header") }</h1>
|
||||||
|
<ArrowIcon rotated={ showTrains }/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ showTrains &&
|
||||||
|
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
||||||
|
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4">
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profile.trains.train") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
||||||
|
onClick={ () => setSortTrainsBy("distance") }>
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profile.trains.distance") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortTrainsBy === "distance" || !sortTrainsBy }/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
||||||
|
onClick={ () => setSortTrainsBy("score") }>
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profile.trains.points") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortTrainsBy === "score" }/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
||||||
|
onClick={ () => setSortTrainsBy("time") }>
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profile.trains.time") }
|
||||||
|
</h5>
|
||||||
|
<FlexArrowIcon rotated={ sortTrainsBy === "time" }/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ Object.keys(data.player.trainStats).sort((a, b) => data.player.trainStats[ b ][ sortTrainsBy ] - data.player.trainStats[ a ][ sortTrainsBy ]).map(trainName =>
|
||||||
|
{
|
||||||
|
const train = data.player.trainStats[ trainName ];
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={ `grid grid-cols-3 sm:grid-cols-4 border-t border-t-stroke dark:border-t-strokedark` }
|
||||||
|
key={ trainName }
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
{ trainName }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-6 sm:block break-all">{ Math.floor(train.distance / 1000) }km</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-3">{ train.score }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-3">{ formatTime(train.time) }</p>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}) }
|
||||||
|
|
||||||
|
|
||||||
|
</div> }
|
||||||
|
|
||||||
|
</div> }
|
||||||
|
{ Object.keys(data.player.dispatcherStats || {}).length > 0 &&
|
||||||
|
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
|
<div className="group relative cursor-pointer" onClick={ () => setShowStations(val => !val) }>
|
||||||
|
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.stations.header") }</h1>
|
||||||
|
<ArrowIcon rotated={ showStations }/>
|
||||||
|
</div>
|
||||||
|
{ showStations &&
|
||||||
|
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
||||||
|
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profile.stations.station") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profile.stations.time") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ Object.keys(data.player.dispatcherStats).sort((a, b) => data.player.dispatcherStats[ b ].time - data.player.dispatcherStats[ a ].time).map(stationName =>
|
||||||
|
{
|
||||||
|
const station = data.player.dispatcherStats[ stationName ];
|
||||||
|
return <div
|
||||||
|
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
||||||
|
key={ stationName }
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
{ stationName }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||||
|
<p className="text-meta-3">{ formatTime(station.time) }</p>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}) }
|
||||||
|
|
||||||
|
</div> }
|
||||||
|
|
||||||
|
</div> }
|
||||||
|
|
||||||
|
|
||||||
|
{ isAdmin && <>
|
||||||
|
<div className="shadow-default dark:bg-boxdark items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
|
||||||
|
<h1 className="text-xl text-black dark:text-white">{ t("admin.header") }</h1>
|
||||||
|
|
||||||
|
<div className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap">
|
||||||
|
|
||||||
|
{ data.player.flags.includes("leaderboard_hidden") ?
|
||||||
|
<button className={ "inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-success" }
|
||||||
|
onClick={ () => adminToggleHideLeaderboardPlayerProfile() }>
|
||||||
|
{ t("admin.hideLeaderboard.button2") }
|
||||||
|
</button> :
|
||||||
|
<button className={ "inline-flex items-center justify-center rounded-md py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 bg-danger" }
|
||||||
|
onClick={ () => setHideLeaderboardStatsModal(true) }>
|
||||||
|
{ t("admin.hideLeaderboard.button") }
|
||||||
|
</button> }
|
||||||
|
|
||||||
|
<button className="inline-flex items-center justify-center rounded-md bg-danger py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5"
|
||||||
|
onClick={ () => setHideProfileModal(true) }>
|
||||||
|
{ t("admin.hide.button") }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</> }
|
||||||
|
|
||||||
|
<div className="shadow-default dark:bg-boxdark items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
|
||||||
|
<h1 className="text-sm text-black dark:text-white">
|
||||||
|
{ t("profile.info", { date: dayjs(data.player.createdAt).format("DD/MM/YYYY") }) }
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
};
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
||||||
|
import { TProfilePlayer } from "../../../types/profile.ts";
|
||||||
|
|
||||||
|
export const ProfilesTable = ({ profiles }: { profiles: TProfilePlayer[] }) =>
|
||||||
|
{
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
|
||||||
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profiles.user") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||||
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
|
{ t("profiles.actions") }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ profiles.map((profile, key) => (
|
||||||
|
<div
|
||||||
|
className={ `grid grid-cols-2 ${ profiles.length === (key + 1) // todo: ...
|
||||||
|
? ""
|
||||||
|
: "border-b border-stroke dark:border-strokedark"
|
||||||
|
}` }
|
||||||
|
key={ profile.id }
|
||||||
|
>
|
||||||
|
<div className="flex justify-center items-center gap-3 p-5 lg:p-5">
|
||||||
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
|
<Link to={ "/profile/" + profile.id }
|
||||||
|
className="color-orchid">{ profile.username }</Link> <UserIcons
|
||||||
|
flags={ profile.flags }/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||||
|
<Link
|
||||||
|
to={ "/profile/" + profile.id }
|
||||||
|
className={ `inline-flex items-center justify-center rounded-md bg-primary py-2 px-5 text-center font-medium text-white hover:bg-opacity-50 lg:px-4 xl:px-5 ${ profile.flags.includes("private") ? "bg-opacity-50" : "" }` }
|
||||||
|
style={ profile.flags.includes("private") ? { pointerEvents: "none" } : undefined }
|
||||||
|
>
|
||||||
|
{ t("profiles.profile") }
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
@ -5,27 +5,32 @@ import useLocalStorage from "./useLocalStorage.tsx";
|
|||||||
|
|
||||||
export type AdminContext = { isAdmin: boolean; username: string; token: string; };
|
export type AdminContext = { isAdmin: boolean; username: string; token: string; };
|
||||||
|
|
||||||
const defaultValue: AdminContext = { isAdmin: false, username: '', token: '' };
|
const defaultValue: AdminContext = { isAdmin: false, username: "", token: "" };
|
||||||
|
|
||||||
const AuthContext = createContext<AdminContext>(defaultValue);
|
const AuthContext = createContext<AdminContext>(defaultValue);
|
||||||
|
|
||||||
|
|
||||||
// {"code":200,"status":true,"data":{"isAdmin":true,"username":"alekswilc","token":"test"}}
|
// {"code":200,"status":true,"data":{"isAdmin":true,"username":"alekswilc","token":"test"}}
|
||||||
const getUserAuthData = () => {
|
const getUserAuthData = () =>
|
||||||
const [value, _setValue] = useLocalStorage<string|undefined>('auth_token', undefined);
|
{
|
||||||
|
const [ value, _setValue ] = useLocalStorage<string | undefined>("auth_token", undefined);
|
||||||
|
|
||||||
if (!value || value === 'undefined')
|
if (!value || value === "undefined")
|
||||||
return { isAdmin: false, username: '', token: '' };
|
{
|
||||||
|
return { isAdmin: false, username: "", token: "" };
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = useSWR(`/admin/auth/?token=${value}`, get);
|
const { data } = useSWR(`/admin/auth/?token=${ value }`, get);
|
||||||
|
|
||||||
return data ? { isAdmin: data.data.isAdmin, username: data.data.username, token: value } : { isAdmin: false, username: '', token: '' };
|
return data ? { isAdmin: data.data.isAdmin, username: data.data.username, token: value } : { isAdmin: false, username: "", token: "" };
|
||||||
}
|
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
||||||
return <AuthContext.Provider value={getUserAuthData()}>{children}</AuthContext.Provider>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const AuthProvider = ({ children }: { children: ReactNode }) =>
|
||||||
|
{
|
||||||
|
return <AuthContext.Provider value={ getUserAuthData() }>{ children }</AuthContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () =>
|
||||||
|
{
|
||||||
return useContext(AuthContext);
|
return useContext(AuthContext);
|
||||||
};
|
};
|
||||||
|
@ -28,11 +28,13 @@ function useLocalStorage<T>(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
const item = window.localStorage.getItem(key);
|
const item = window.localStorage.getItem(key);
|
||||||
if (item) {
|
if (item)
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
return item ? JSON.parse(item) : initialValue;
|
return item ? JSON.parse(item) : initialValue;
|
||||||
}
|
} catch
|
||||||
catch {
|
{
|
||||||
return item ? item : initialValue;
|
return item ? item : initialValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
"title": "Preview version!",
|
"title": "Preview version!",
|
||||||
"description": "The site is in version V3-PREVIEW, may contain errors. I will be grateful for reporting all errors on the project page (git.alekswilc.dev) or in a private message on discord - alekswilc. Stay tuned!"
|
"description": "The site is in version V3-PREVIEW, may contain errors. I will be grateful for reporting all errors on the project page (git.alekswilc.dev) or in a private message on discord - alekswilc. Stay tuned!"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Type to search.",
|
||||||
|
"tip": "You can search for multiple data using a comma, ex. pl2,alekswilc (server name, username)"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"stats": {
|
"stats": {
|
||||||
"trains": "Trains",
|
"trains": "Trains",
|
||||||
@ -33,6 +37,11 @@
|
|||||||
"description": "It seems you're lost.",
|
"description": "It seems you're lost.",
|
||||||
"button": "Return to homepage."
|
"button": "Return to homepage."
|
||||||
},
|
},
|
||||||
|
"profiles": {
|
||||||
|
"user": "Player",
|
||||||
|
"profile": "Profile",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
"leaderboard": {
|
"leaderboard": {
|
||||||
"user": "Player",
|
"user": "Player",
|
||||||
"time": "Time",
|
"time": "Time",
|
||||||
@ -58,7 +67,6 @@
|
|||||||
"record": "Record",
|
"record": "Record",
|
||||||
"train": "Train",
|
"train": "Train",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"search": "Type to search",
|
|
||||||
"station": "Station"
|
"station": "Station"
|
||||||
},
|
},
|
||||||
"content_loader": {
|
"content_loader": {
|
||||||
@ -151,10 +159,16 @@
|
|||||||
"info": "INFO",
|
"info": "INFO",
|
||||||
"admin": "ADMIN",
|
"admin": "ADMIN",
|
||||||
"logged": "Logged as {{username}}",
|
"logged": "Logged as {{username}}",
|
||||||
"logout": "Log out"
|
"logout": "Log out",
|
||||||
|
"profiles": "Profiles"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"admin": "Administrator",
|
||||||
|
"leaderboard_hidden": "Leaderboard hidden",
|
||||||
|
"hidden": "Profile hidden"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"header": "Akcje moderacyjne",
|
"header": "Moderator actions",
|
||||||
"hideLeaderboard": {
|
"hideLeaderboard": {
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "Are you sure?",
|
"title": "Are you sure?",
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
"title": "Wersja preview!",
|
"title": "Wersja preview!",
|
||||||
"description": "Strona znajduje się w wersji V3-PREVIEW, może zawierać błędy. Będe wdzieczny za zgłaszanie wszystkich błędów na stronie projektu (git.alekswilc.dev) lub w wiadomości prywatnej na discordzie - alekswilc. Stay tuned!"
|
"description": "Strona znajduje się w wersji V3-PREVIEW, może zawierać błędy. Będe wdzieczny za zgłaszanie wszystkich błędów na stronie projektu (git.alekswilc.dev) lub w wiadomości prywatnej na discordzie - alekswilc. Stay tuned!"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Wpisz, aby wyszukać",
|
||||||
|
"tip": "Możesz wyszukiwać wiele danych używając przecinka, np pl2,alekswilc (nazwa serwera, nazwa użytkownika)"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"stats": {
|
"stats": {
|
||||||
"trains": "Pociągów",
|
"trains": "Pociągów",
|
||||||
@ -41,6 +45,11 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"actions": "Akcje"
|
"actions": "Akcje"
|
||||||
},
|
},
|
||||||
|
"profiles": {
|
||||||
|
"user": "Gracz",
|
||||||
|
"profile": "Profil",
|
||||||
|
"actions": "Akcje"
|
||||||
|
},
|
||||||
"active": {
|
"active": {
|
||||||
"server": "Serwer",
|
"server": "Serwer",
|
||||||
"user": "Gracz",
|
"user": "Gracz",
|
||||||
@ -58,7 +67,6 @@
|
|||||||
"record": "Więcej",
|
"record": "Więcej",
|
||||||
"train": "Pociąg",
|
"train": "Pociąg",
|
||||||
"actions": "Akcje",
|
"actions": "Akcje",
|
||||||
"search": "Wpisz, aby wyszukać",
|
|
||||||
"station": "Stacja"
|
"station": "Stacja"
|
||||||
},
|
},
|
||||||
"content_loader": {
|
"content_loader": {
|
||||||
@ -141,6 +149,11 @@
|
|||||||
"profile": "Profil"
|
"profile": "Profil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"icons": {
|
||||||
|
"admin": "Administrator",
|
||||||
|
"leaderboard_hidden": "Tablica wyników ukryta",
|
||||||
|
"hidden": "Profil ukryty"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"home": "Strona główna",
|
"home": "Strona główna",
|
||||||
"logs": "Logi",
|
"logs": "Logi",
|
||||||
@ -151,10 +164,11 @@
|
|||||||
"info": "INFO",
|
"info": "INFO",
|
||||||
"admin": "ADMIN",
|
"admin": "ADMIN",
|
||||||
"logged": "Zalogowano jako {{username}}",
|
"logged": "Zalogowano jako {{username}}",
|
||||||
"logout": "Wyloguj"
|
"logout": "Wyloguj",
|
||||||
|
"profiles": "Profile"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"header": "Moderator actions",
|
"header": "Akcje moderacyjne",
|
||||||
"hideLeaderboard": {
|
"hideLeaderboard": {
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "Czy jesteś pewien?",
|
"title": "Czy jesteś pewien?",
|
||||||
|
@ -19,7 +19,7 @@ import { Link, useSearchParams } from "react-router-dom";
|
|||||||
import { TStatsResponse } from "../types/stats.ts";
|
import { TStatsResponse } from "../types/stats.ts";
|
||||||
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
|
import { CardDataStats } from "../components/mini/util/CardDataStats.tsx";
|
||||||
import { get } from "../util/fetcher.ts";
|
import { get } from "../util/fetcher.ts";
|
||||||
import useSWR from 'swr';
|
import useSWR from "swr";
|
||||||
import { LoadError } from "../components/mini/loaders/ContentLoader.tsx";
|
import { LoadError } from "../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
|
||||||
export const Home = () =>
|
export const Home = () =>
|
||||||
@ -28,10 +28,11 @@ export const Home = () =>
|
|||||||
|
|
||||||
const { data, error } = useSWR<TStatsResponse>("/stats/", get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
const { data, error } = useSWR<TStatsResponse>("/stats/", get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
|
||||||
if (searchParams.get('admin_token')) {
|
if (searchParams.get("admin_token"))
|
||||||
window.localStorage.setItem('auth_token', searchParams.get('admin_token')!);
|
{
|
||||||
|
window.localStorage.setItem("auth_token", searchParams.get("admin_token")!);
|
||||||
setSearchParams(new URLSearchParams());
|
setSearchParams(new URLSearchParams());
|
||||||
setTimeout(() => window.location.reload(), 1000);
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
}
|
}
|
||||||
@ -39,13 +40,14 @@ export const Home = () =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex pb-5">
|
<div className="flex pb-5">
|
||||||
{ error && <LoadError /> }
|
{ error && <LoadError/> }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 xl:grid-cols-3 2xl:gap-7.5">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 xl:grid-cols-3 2xl:gap-7.5">
|
||||||
<CardDataStats title={ t("home.stats.trains") } total={ data?.data?.stats?.trains ?? "-" }/>
|
<CardDataStats title={ t("home.stats.trains") } total={ data?.data?.stats?.trains ?? "-" }/>
|
||||||
<CardDataStats title={ t("home.stats.dispatchers") } total={ data?.data?.stats?.dispatchers ?? "-" }/>
|
<CardDataStats title={ t("home.stats.dispatchers") }
|
||||||
|
total={ data?.data?.stats?.dispatchers ?? "-" }/>
|
||||||
<CardDataStats title={ t("home.stats.profiles") } total={ data?.data?.stats?.profiles ?? "-" }/>
|
<CardDataStats title={ t("home.stats.profiles") } total={ data?.data?.stats?.profiles ?? "-" }/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -105,7 +107,7 @@ export const Home = () =>
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>{ data?.data?.git?.version && <Link className="color-orchid"
|
<p>{ data?.data?.git?.version && <Link className="color-orchid"
|
||||||
to={ `https://git.alekswilc.dev/simrail/simrail.pro/releases/tag/${ data?.data?.git?.version }` }>{ data?.data?.git?.version }</Link> }{ data?.data?.git?.version && data?.data?.git?.commit && " | " }{ data?.data?.git?.commit &&
|
to={ `https://git.alekswilc.dev/simrail/simrail.pro/releases/tag/${ data?.data?.git?.version }` }>{ data?.data?.git?.version }</Link> }{ data?.data?.git?.version && data?.data?.git?.commit && " | " }{ data?.data?.git?.commit &&
|
||||||
<Link className="color-orchid"
|
<Link className="color-orchid"
|
||||||
to={ `https://git.alekswilc.dev/simrail/simrail.pro/commit/${ data?.data?.git?.commit }` }>{ data?.data?.git?.commit }</Link> }</p>
|
to={ `https://git.alekswilc.dev/simrail/simrail.pro/commit/${ data?.data?.git?.commit }` }>{ data?.data?.git?.commit }</Link> }</p>
|
||||||
|
|
||||||
|
@ -70,11 +70,13 @@ export const ActiveTrainPlayers = () =>
|
|||||||
|
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
{ (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
{ (data && data.code === 404) || (data && !data?.data?.records?.length) &&
|
||||||
description={ t("content_loader.notfound.description") }/>
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ data && data.code === 200 && !!data?.data?.records?.length && <ActiveTrainTable trains={ data?.data?.records } /> }
|
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||||
|
<ActiveTrainTable trains={ data?.data?.records }/> }
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -24,9 +24,7 @@ export const NotFoundError = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
className="overflow-hidden rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
<div className="px-4 pb-6 text-center">
|
<div className="px-4 pb-6 text-center">
|
||||||
|
@ -16,12 +16,12 @@
|
|||||||
|
|
||||||
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx";
|
import { LeaderboardStationTable } from "../../components/pages/leaderboard/LeaderboardStationTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { get } from "../../util/fetcher.ts";
|
import { get } from "../../util/fetcher.ts";
|
||||||
import useSWR from 'swr';
|
import useSWR from "swr";
|
||||||
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -30,7 +30,7 @@ export const StationLeaderboard = () =>
|
|||||||
{
|
{
|
||||||
const [ params, setParams ] = useState(new URLSearchParams());
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
|
||||||
const { data, error, isLoading } = useSWR(`/leaderboard/station/?${params.toString()}`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
const { data, error, isLoading } = useSWR(`/leaderboard/station/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
@ -66,14 +66,16 @@ export const StationLeaderboard = () =>
|
|||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
<>
|
<>
|
||||||
{ error && <LoadError /> }
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
{ data && (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
{ data && (data && data.code === 404) || (data && !data?.data?.records?.length) &&
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
{ data && data.code === 200 && data.data && !!data?.data?.records?.length && <StationTable stations={ data.data.records } /> }
|
{ data && data.code === 200 && data.data && !!data?.data?.records?.length &&
|
||||||
|
<LeaderboardStationTable stations={ data.data.records }/> }
|
||||||
</>
|
</>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx";
|
import { LeaderboardTrainTable } from "../../components/pages/leaderboard/LeaderboardTrainTable.tsx";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
@ -73,11 +73,14 @@ export const TrainLeaderboard = () =>
|
|||||||
|
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
{ (data && data.code === 404) || (data && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
{ (data && data.code === 404) || (data && !data?.data?.records?.length) &&
|
||||||
description={ t("content_loader.notfound.description") }/>
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ data && data.code === 200 && !!data?.data?.records?.length && <TrainTable trains={ data?.data?.records } setSortBy={ setSortBy } sortBy={ sortBy }/> }
|
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||||
|
<LeaderboardTrainTable trains={ data?.data?.records } setSortBy={ setSortBy }
|
||||||
|
sortBy={ sortBy }/> }
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -33,8 +33,8 @@ export const Log = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ERROR */}
|
{/* ERROR */ }
|
||||||
{ error && <LoadError /> }
|
{ error && <LoadError/> }
|
||||||
{/* LOADING */ }
|
{/* LOADING */ }
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */ }
|
||||||
|
@ -19,16 +19,16 @@ import { StationTable } from "../../components/pages/logs/StationTable.tsx";
|
|||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Search } from "../../components/mini/util/Search.tsx";
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import useSWR from 'swr';
|
import useSWR from "swr";
|
||||||
import { get } from "../../util/fetcher.ts";
|
import { get } from "../../util/fetcher.ts";
|
||||||
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const StationLogs = () =>
|
export const StationLogs = () =>
|
||||||
{
|
{
|
||||||
const [params, setParams] = useState(new URLSearchParams());
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
const { data, error, isLoading } = useSWR(`/stations/?${params.toString()}`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
const { data, error, isLoading } = useSWR(`/stations/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
@ -40,7 +40,7 @@ export const StationLogs = () =>
|
|||||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
searchValue && params.set('q', searchValue);
|
searchValue && params.set("q", searchValue);
|
||||||
|
|
||||||
setSearchParams(params.toString());
|
setSearchParams(params.toString());
|
||||||
setParams(params);
|
setParams(params);
|
||||||
@ -63,14 +63,16 @@ export const StationLogs = () =>
|
|||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
<>
|
<>
|
||||||
{ error && <LoadError /> }
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
{isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) &&
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
{data && data.code === 200 && !!data?.data?.records?.length && <StationTable stations={data.data.records} /> }
|
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||||
|
<StationTable stations={ data.data.records }/> }
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -67,10 +67,12 @@ export const TrainLogs = () =>
|
|||||||
|
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) && <WarningAlert title={ t("content_loader.notfound.header") }
|
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) &&
|
||||||
description={ t("content_loader.notfound.description") }/> }
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
{ data && data.code === 200 && !!data?.data?.records?.length && <TrainTable trains={ data.data.records }/> }
|
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||||
|
<TrainTable trains={ data.data.records }/> }
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -23,7 +23,7 @@ import { ProfileCard } from "../../components/pages/profile/Profile.tsx";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||||
import { formatTime } from "../../util/time.ts";
|
import { formatTime } from "../../util/time.ts";
|
||||||
import useSWR from 'swr';
|
import useSWR from "swr";
|
||||||
import { get } from "../../util/fetcher.ts";
|
import { get } from "../../util/fetcher.ts";
|
||||||
|
|
||||||
|
|
||||||
@ -38,8 +38,8 @@ export const Profile = () =>
|
|||||||
<>
|
<>
|
||||||
{/* LOADING */ }
|
{/* LOADING */ }
|
||||||
{ isLoading && <ContentLoader/> }
|
{ isLoading && <ContentLoader/> }
|
||||||
{/* ERROR */}
|
{/* ERROR */ }
|
||||||
{ error && <LoadError /> }
|
{ error && <LoadError/> }
|
||||||
{/* BLACKLISTED */ }
|
{/* BLACKLISTED */ }
|
||||||
{ data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
|
{ data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
|
||||||
description="The player's profile could not be displayed due to active moderator actions."/> }
|
description="The player's profile could not be displayed due to active moderator actions."/> }
|
||||||
@ -47,14 +47,14 @@ export const Profile = () =>
|
|||||||
description={ t("profile.errors.blacklist.description") }/> }
|
description={ t("profile.errors.blacklist.description") }/> }
|
||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */ }
|
||||||
{ data && data.code === 404 && <PageMeta title="simrail.pro | Profile not found"
|
{ data && data.code === 404 && <PageMeta title="simrail.pro | Profile not found"
|
||||||
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
||||||
{ data && data.code === 404 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
{ data && data.code === 404 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
||||||
description={ t("profile.errors.notfound.description") }/> }
|
description={ t("profile.errors.notfound.description") }/> }
|
||||||
|
|
||||||
{/* SUCCESS */ }
|
{/* SUCCESS */ }
|
||||||
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
|
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
|
||||||
title={ `simrail.pro | ${ data.data.player.username }'s profile` }
|
title={ `simrail.pro | ${ data.data.player.username }'s profile` }
|
||||||
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
||||||
${ data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime) } dispatcher experience` }/> }
|
${ data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime) } dispatcher experience` }/> }
|
||||||
{ data && data.code === 200 && <ProfileCard data={ data.data }/> }
|
{ data && data.code === 200 && <ProfileCard data={ data.data }/> }
|
||||||
</>
|
</>
|
||||||
|
63
packages/frontend/src/pages/profiles/Profile.tsx
Normal file
63
packages/frontend/src/pages/profiles/Profile.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ProfileCard } from "../../components/pages/profiles/Profile.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { PageMeta } from "../../components/mini/util/PageMeta.tsx";
|
||||||
|
import { formatTime } from "../../util/time.ts";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { get } from "../../util/fetcher.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export const Profile = () =>
|
||||||
|
{
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data, error, isLoading } = useSWR(`/profiles/${ id }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* LOADING */ }
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
{/* ERROR */ }
|
||||||
|
{ error && <LoadError/> }
|
||||||
|
{/* BLACKLISTED */ }
|
||||||
|
{ data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
|
||||||
|
description="The player's profile could not be displayed due to active moderator actions."/> }
|
||||||
|
{ data && data.code === 403 && <WarningAlert title={ t("profile.errors.blacklist.title") }
|
||||||
|
description={ t("profile.errors.blacklist.description") }/> }
|
||||||
|
{/* NOT FOUND */ }
|
||||||
|
{ data && data.code === 404 && <PageMeta title="simrail.pro | Profile not found"
|
||||||
|
description="Player's profile could not be found or the player has a private Steam profile."/> }
|
||||||
|
{ data && data.code === 404 && <WarningAlert title={ t("profile.errors.notfound.title") }
|
||||||
|
description={ t("profile.errors.notfound.description") }/> }
|
||||||
|
|
||||||
|
{/* SUCCESS */ }
|
||||||
|
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
|
||||||
|
title={ `simrail.pro | ${ data.data.player.username }'s profile` }
|
||||||
|
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
||||||
|
${ data.data.player.dispatcherTime ? 0 : formatTime(data.data.player.dispatcherTime) } dispatcher experience` }/> }
|
||||||
|
{ data && data.code === 200 && <ProfileCard data={ data.data }/> }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
80
packages/frontend/src/pages/profiles/Profiles.tsx
Normal file
80
packages/frontend/src/pages/profiles/Profiles.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* See LICENSE for more.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { Search } from "../../components/mini/util/Search.tsx";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { WarningAlert } from "../../components/mini/alerts/Warning.tsx";
|
||||||
|
import { ContentLoader, LoadError } from "../../components/mini/loaders/ContentLoader.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { get } from "../../util/fetcher.ts";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { ProfilesTable } from "../../components/pages/profiles/ProfilesTable.tsx";
|
||||||
|
|
||||||
|
export const Profiles = () =>
|
||||||
|
{
|
||||||
|
const [ params, setParams ] = useState(new URLSearchParams());
|
||||||
|
const { data, error, isLoading } = useSWR(`/profiles/?${ params.toString() }`, get, { refreshInterval: 10_000, errorRetryCount: 5 });
|
||||||
|
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||||
|
|
||||||
|
const [ searchValue ] = useDebounce(searchItem, 500);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
searchValue && params.set("q", searchValue);
|
||||||
|
|
||||||
|
setSearchParams(params.toString());
|
||||||
|
setParams(params);
|
||||||
|
}, [ searchValue ]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setSearchItem(searchParams.get("q") ?? "");
|
||||||
|
}, [ searchParams ]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
{
|
||||||
|
setSearchItem(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||||
|
<>
|
||||||
|
{ error && <LoadError/> }
|
||||||
|
|
||||||
|
{ isLoading && <ContentLoader/> }
|
||||||
|
|
||||||
|
{ (data && data.code === 404) || (data && data.code === 200 && !data?.data?.records?.length) &&
|
||||||
|
<WarningAlert title={ t("content_loader.notfound.header") }
|
||||||
|
description={ t("content_loader.notfound.description") }/> }
|
||||||
|
|
||||||
|
{ data && data.code === 200 && !!data?.data?.records?.length &&
|
||||||
|
<ProfilesTable profiles={ data.data.records }/> }
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -24,7 +24,8 @@ export interface TActiveTrainPlayersResponse
|
|||||||
code: number;
|
code: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TActiveTrainPlayersData {
|
export interface TActiveTrainPlayersData
|
||||||
|
{
|
||||||
"server": string,
|
"server": string,
|
||||||
"player": TProfilePlayer,
|
"player": TProfilePlayer,
|
||||||
"trainNumber": string,
|
"trainNumber": string,
|
||||||
@ -40,7 +41,8 @@ export interface TActiveStationPlayersResponse
|
|||||||
code: number;
|
code: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TActiveStationPlayersData {
|
export interface TActiveStationPlayersData
|
||||||
|
{
|
||||||
"server": string,
|
"server": string,
|
||||||
"player": TProfilePlayer,
|
"player": TProfilePlayer,
|
||||||
"stationName": string,
|
"stationName": string,
|
||||||
|
@ -14,6 +14,6 @@
|
|||||||
* See LICENSE for more.
|
* See LICENSE for more.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const get = (url: string) => fetch(`${ import.meta.env.VITE_API_URL }${url}`, { signal: AbortSignal.timeout(2500) }).then((res) => res.json());
|
export const get = (url: string) => fetch(`${ import.meta.env.VITE_API_URL }${ url }`, { signal: AbortSignal.timeout(2500) }).then((res) => res.json());
|
||||||
|
|
||||||
export const post = (url: string, body?: any, headers: Record<string, string> = {}) => fetch(`${ import.meta.env.VITE_API_URL }${url}`, { signal: AbortSignal.timeout(2500), method:'POST',body: JSON.stringify(body), headers: Object.assign(headers, { 'Content-Type': "application/json", }) }).then((res) => res.json());
|
export const post = (url: string, body?: any, headers: Record<string, string> = {}) => fetch(`${ import.meta.env.VITE_API_URL }${ url }`, { signal: AbortSignal.timeout(2500), method: "POST", body: JSON.stringify(body), headers: Object.assign(headers, { "Content-Type": "application/json" }) }).then((res) => res.json());
|
@ -23,8 +23,8 @@ export const formatTime = (time: number) =>
|
|||||||
|
|
||||||
if (Math.floor(time / 60000) > 0)
|
if (Math.floor(time / 60000) > 0)
|
||||||
{
|
{
|
||||||
return `${ Math.floor(time / 60000) }m`
|
return `${ Math.floor(time / 60000) }m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return '0h';
|
return "0h";
|
||||||
}
|
};
|
Loading…
x
Reference in New Issue
Block a user