feat: initialize Blazor web project and update application configuration

This commit is contained in:
Dmitrii
2026-04-16 18:47:35 +03:00
parent 6ef1091a3a
commit 1faec68785
83 changed files with 61324 additions and 7 deletions
@@ -0,0 +1,43 @@
package git.yawaflua.tech.command;
import git.yawaflua.tech.database.DatabaseManager;
import git.yawaflua.tech.messages.Messages;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class PtCommand implements BasicCommand {
private final DatabaseManager databaseManager;
public PtCommand(DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
}
@Override
public void execute(@NotNull CommandSourceStack stack, @NotNull String[] args) {
CommandSender sender = stack.getSender();
if (!(sender instanceof Player player)) {
sender.sendMessage(Messages.parse(Messages.ONLY_PLAYERS));
return;
}
System.out.println(String.join(",", args));
if (args.length == 3 && args[0].equalsIgnoreCase("web") && args[1].equalsIgnoreCase("auth")) {
String code = args[2];
System.out.println(code);
boolean success = databaseManager.resolveWebAuth(code, player.getUniqueId());
System.out.println(success);
if (success) {
player.sendMessage(Messages.parse(Messages.WEB_AUTH_SUCCESS));
} else {
player.sendMessage(Messages.parse(Messages.WEB_AUTH_FAILED));
}
return;
}
player.sendMessage(Messages.parse(Messages.WEB_AUTH_USAGE));
}
}
@@ -6,6 +6,8 @@ import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters; import com.mongodb.client.model.Filters;
import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.Updates;
import com.mongodb.client.result.UpdateResult;
import git.yawaflua.tech.model.PlayerData; import git.yawaflua.tech.model.PlayerData;
import org.bson.Document; import org.bson.Document;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
@@ -20,6 +22,7 @@ public class DatabaseManager {
private MongoDatabase database; private MongoDatabase database;
private MongoCollection<Document> playersCollection; private MongoCollection<Document> playersCollection;
private MongoCollection<Document> reportsCollection; private MongoCollection<Document> reportsCollection;
private MongoCollection<Document> webAuthCollection;
public DatabaseManager(Logger logger, FileConfiguration config) { public DatabaseManager(Logger logger, FileConfiguration config) {
this.logger = logger; this.logger = logger;
@@ -44,6 +47,7 @@ public class DatabaseManager {
database = mongoClient.getDatabase(dbName); database = mongoClient.getDatabase(dbName);
playersCollection = database.getCollection("players"); playersCollection = database.getCollection("players");
reportsCollection = database.getCollection("reports"); reportsCollection = database.getCollection("reports");
webAuthCollection = database.getCollection("auth_requests");
logger.info("Connected to MongoDB -> " + dbName); logger.info("Connected to MongoDB -> " + dbName);
} }
@@ -69,8 +73,7 @@ public class DatabaseManager {
doc.getInteger("age", 0), doc.getInteger("age", 0),
doc.getDouble("points"), doc.getDouble("points"),
doc.getBoolean("registered", false), doc.getBoolean("registered", false),
doc.getLong("firstJoin") doc.getLong("firstJoin"));
);
} }
public void savePlayer(PlayerData player) { public void savePlayer(PlayerData player) {
@@ -86,8 +89,7 @@ public class DatabaseManager {
playersCollection.replaceOne( playersCollection.replaceOne(
Filters.eq("uuid", player.getUuid().toString()), Filters.eq("uuid", player.getUuid().toString()),
doc, doc,
new ReplaceOptions().upsert(true) new ReplaceOptions().upsert(true));
);
} }
public void logReport(UUID reporter, UUID target, String reason) { public void logReport(UUID reporter, UUID target, String reason) {
@@ -98,4 +100,15 @@ public class DatabaseManager {
.append("resolved", false); .append("resolved", false);
reportsCollection.insertOne(report); reportsCollection.insertOne(report);
} }
public boolean resolveWebAuth(String code, UUID playerUuid) {
UpdateResult result = webAuthCollection.updateOne(
Filters.and(
Filters.eq("code", code),
Filters.eq("resolved", false)),
Updates.combine(
Updates.set("uuid", playerUuid.toString()),
Updates.set("resolved", true)));
return result.getModifiedCount() > 0;
}
} }
@@ -0,0 +1,112 @@
package git.yawaflua.tech.listener;
import git.yawaflua.tech.messages.Messages;
import git.yawaflua.tech.score.ScoreManager;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityPickupItemEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ItemShareListener implements Listener {
private final ScoreManager scoreManager;
private final Set<Material> valuableItems = new HashSet<>();
private final double pointsPerShare;
private final long cooldownMs;
// Key: "dropper_uuid:picker_uuid", Value: last reward timestamp
private final Map<String, Long> cooldowns = new ConcurrentHashMap<>();
// Key: Item entity UUID -> dropper player UUID
private final Map<UUID, UUID> droppedByMap = new ConcurrentHashMap<>();
public ItemShareListener(ScoreManager scoreManager, FileConfiguration config) {
this.scoreManager = scoreManager;
this.pointsPerShare = config.getDouble("sharing.points-per-share", 3.0);
this.cooldownMs = config.getLong("sharing.cooldown-seconds", 300) * 1000L;
List<String> items = config.getStringList("sharing.valuable-items");
for (String itemName : items) {
try {
valuableItems.add(Material.valueOf(itemName.toUpperCase()));
} catch (IllegalArgumentException ignored) {
// Invalid material name in config, skip
}
}
}
/**
* Track when a player drops an item so we know who the original dropper is.
*/
@EventHandler
public void onPlayerDropItem(org.bukkit.event.player.PlayerDropItemEvent event) {
Player dropper = event.getPlayer();
Item itemEntity = event.getItemDrop();
Material mat = itemEntity.getItemStack().getType();
if (valuableItems.contains(mat)) {
droppedByMap.put(itemEntity.getUniqueId(), dropper.getUniqueId());
}
}
/**
* When a player picks up an item dropped by another player, reward the dropper with points.
*/
@EventHandler
public void onEntityPickupItem(EntityPickupItemEvent event) {
Entity entity = event.getEntity();
if (!(entity instanceof Player picker)) {
return;
}
Item itemEntity = event.getItem();
Material mat = itemEntity.getItemStack().getType();
if (!valuableItems.contains(mat)) {
return;
}
UUID dropperUuid = droppedByMap.remove(itemEntity.getUniqueId());
if (dropperUuid == null) {
return; // Not tracked (e.g. world-generated drop)
}
// Don't reward picking up your own items
if (dropperUuid.equals(picker.getUniqueId())) {
return;
}
// Check cooldown between this pair
String cooldownKey = dropperUuid.toString() + ":" + picker.getUniqueId().toString();
long now = System.currentTimeMillis();
long lastReward = cooldowns.getOrDefault(cooldownKey, 0L);
if (now - lastReward < cooldownMs) {
return;
}
cooldowns.put(cooldownKey, now);
scoreManager.addPoints(dropperUuid, pointsPerShare);
// Notify the dropper if they're online
Player dropper = org.bukkit.Bukkit.getPlayer(dropperUuid);
if (dropper != null && dropper.isOnline()) {
String msg = Messages.ITEM_SHARE_REWARD
.replace("<amount>", String.valueOf(pointsPerShare))
.replace("<item>", formatMaterialName(mat))
.replace("<player>", picker.getName());
dropper.sendMessage(Messages.parse(msg));
}
}
private String formatMaterialName(Material mat) {
return mat.name().replace('_', ' ').toLowerCase();
}
}
@@ -43,4 +43,10 @@ public class Messages {
public static final String REPORT_COOLDOWN = PREFIX public static final String REPORT_COOLDOWN = PREFIX
+ "<red>Wait <time> sec. before sending another report.</red>"; + "<red>Wait <time> sec. before sending another report.</red>";
public static final String REPORT_NOTIFICATION_OP = "<red>[REPORT]</red> <yellow><reporter></yellow> reported <yellow><target></yellow>. Reason: <gray><reason></gray>"; public static final String REPORT_NOTIFICATION_OP = "<red>[REPORT]</red> <yellow><reporter></yellow> reported <yellow><target></yellow>. Reason: <gray><reason></gray>";
public static final String WEB_AUTH_SUCCESS = PREFIX + "<green>Website authentication successful!</green>";
public static final String WEB_AUTH_FAILED = PREFIX + "<red>Invalid or already used authentication code.</red>";
public static final String WEB_AUTH_USAGE = PREFIX + "<yellow>Usage: /pt web auth <code></yellow>";
public static final String ITEM_SHARE_REWARD = PREFIX + "<green>+<amount> points for sharing <item> with <player>!</green>";
} }
@@ -1,9 +1,11 @@
package git.yawaflua.tech; package git.yawaflua.tech;
import git.yawaflua.tech.command.PtCommand;
import git.yawaflua.tech.command.ReportCommand; import git.yawaflua.tech.command.ReportCommand;
import git.yawaflua.tech.database.DatabaseManager; import git.yawaflua.tech.database.DatabaseManager;
import git.yawaflua.tech.filter.ProfanityFilter; import git.yawaflua.tech.filter.ProfanityFilter;
import git.yawaflua.tech.listener.ChatListener; import git.yawaflua.tech.listener.ChatListener;
import git.yawaflua.tech.listener.ItemShareListener;
import git.yawaflua.tech.listener.PlayerJoinListener; import git.yawaflua.tech.listener.PlayerJoinListener;
import git.yawaflua.tech.listener.PlayerQuitListener; import git.yawaflua.tech.listener.PlayerQuitListener;
import git.yawaflua.tech.listener.PvPListener; import git.yawaflua.tech.listener.PvPListener;
@@ -52,9 +54,11 @@ public final class pixeltalk extends JavaPlugin {
getServer().getPluginManager().registerEvents(new PlayerJoinListener(scoreManager, questionnaireManager, tabManager), this); getServer().getPluginManager().registerEvents(new PlayerJoinListener(scoreManager, questionnaireManager, tabManager), this);
getServer().getPluginManager().registerEvents(new PlayerQuitListener(scoreManager), this); getServer().getPluginManager().registerEvents(new PlayerQuitListener(scoreManager), this);
getServer().getPluginManager().registerEvents(new PvPListener(scoreManager, getConfig()), this); getServer().getPluginManager().registerEvents(new PvPListener(scoreManager, getConfig()), this);
getServer().getPluginManager().registerEvents(new ItemShareListener(scoreManager, getConfig()), this);
this.getLifecycleManager().registerEventHandler(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, event -> { this.getLifecycleManager().registerEventHandler(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, event -> {
event.registrar().register("report", new ReportCommand(databaseManager, getConfig(), voiceIntegration)); event.registrar().register("report", new ReportCommand(databaseManager, getConfig(), voiceIntegration));
event.registrar().register("pt", new PtCommand(databaseManager));
}); });
getLogger().info("PixelTalk enabled successfully!"); getLogger().info("PixelTalk enabled successfully!");
@@ -41,7 +41,8 @@ public class QuestionnaireManager {
public void handleAnswer(Player player, String answer, PlayerData playerData) { public void handleAnswer(Player player, String answer, PlayerData playerData) {
QuestionnaireState state = activeQuestionnaires.get(player.getUniqueId()); QuestionnaireState state = activeQuestionnaires.get(player.getUniqueId());
if (state == null) return; if (state == null)
return;
switch (state.currentState) { switch (state.currentState) {
case LANGUAGE: case LANGUAGE:
@@ -61,6 +62,7 @@ public class QuestionnaireManager {
playerData.setAge(age); playerData.setAge(age);
playerData.setRegistered(true); playerData.setRegistered(true);
activeQuestionnaires.remove(player.getUniqueId()); activeQuestionnaires.remove(player.getUniqueId());
playerData.setName(player.getName());
databaseManager.savePlayer(playerData); databaseManager.savePlayer(playerData);
player.sendMessage(Messages.parse(Messages.REGISTRATION_COMPLETE)); player.sendMessage(Messages.parse(Messages.REGISTRATION_COMPLETE));
} else { } else {
@@ -30,7 +30,6 @@ public class VoiceIntegration {
private final Plugin plugin; private final Plugin plugin;
private final Logger logger; private final Logger logger;
private final Map<UUID, Integer> speakingTime = new HashMap<>();
public VoiceIntegration(Plugin plugin, ScoreManager scoreManager, FileConfiguration config) { public VoiceIntegration(Plugin plugin, ScoreManager scoreManager, FileConfiguration config) {
this.plugin = plugin; this.plugin = plugin;
+23
View File
@@ -99,3 +99,26 @@ profanity:
- "يخرب بيتك" - "يخرب بيتك"
- "كس أمك" - "كس أمك"
- "عيري فيك" - "عيري فيك"
# Sharing valuable resources
sharing:
# Points awarded for sharing valuable items with other players
points-per-share: 3.0
# Cooldown in seconds between share rewards (per player pair)
cooldown-seconds: 300
# List of valuable items (Minecraft material names)
valuable-items:
- DIAMOND
- DIAMOND_BLOCK
- EMERALD
- EMERALD_BLOCK
- NETHERITE_INGOT
- NETHERITE_BLOCK
- GOLDEN_APPLE
- ENCHANTED_GOLDEN_APPLE
- ELYTRA
- TOTEM_OF_UNDYING
- BEACON
- NETHER_STAR
- TRIDENT
- SHULKER_BOX
+5
View File
@@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
+16
View File
@@ -0,0 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "web.PixelTalk", "web.PixelTalk\web.PixelTalk.csproj", "{DD48A3C3-8690-442D-9D44-ED1E9271A313}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DD48A3C3-8690-442D-9D44-ED1E9271A313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD48A3C3-8690-442D-9D44-ED1E9271A313}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD48A3C3-8690-442D-9D44-ED1E9271A313}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD48A3C3-8690-442D-9D44-ED1E9271A313}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIMongoCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff92749f892fb46959621e0c6a2d97fc5286400_003F40_003Fdca007d1_003FIMongoCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]"/>
<link rel="stylesheet" href="@Assets["app.css"]"/>
<link rel="stylesheet" href="@Assets["web.PixelTalk.styles.css"]"/>
<ImportMap/>
<link rel="icon" type="image/png" href="favicon.png"/>
<HeadOutlet/>
</head>
<body>
<Routes/>
<ReconnectModal/>
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>
@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -0,0 +1,98 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -0,0 +1,40 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">web.PixelTalk</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/>
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<AuthorizeView Roles="op">
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="admin/reports">
<span class="bi bi-exclamation-triangle-fill" aria-hidden="true"></span> Manage Reports
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="admin/store">
<span class="bi bi-shop" aria-hidden="true"></span> Manage Store
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="admin/purchases">
<span class="bi bi-receipt" aria-hidden="true"></span> Purchase Log
</NavLink>
</div>
</Authorized>
</AuthorizeView>
</nav>
</div>
@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
@@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br/>Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br/>Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>
@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}
@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
@@ -0,0 +1,100 @@
@page "/admin/purchases"
@attribute [Authorize(Roles = "op")]
@using Microsoft.AspNetCore.Authorization
@using MongoDB.Driver
@using web.PixelTalk.Models
@using web.PixelTalk.Services
@inject MongoService MongoService
<PageTitle>Manage Purchases</PageTitle>
<div class="container mt-4">
<h2>Player Purchase History</h2>
<div class="table-responsive mt-3 shadow-sm rounded">
<table class="table table-hover table-striped align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Date</th>
<th>Player UUID</th>
<th>Item Redeemed</th>
<th>Points Cost</th>
</tr>
</thead>
<tbody>
@foreach (var p in logs)
{
<tr>
<td class="text-muted small">@(DateTimeOffset.FromUnixTimeSeconds(p.Timestamp).ToString("g"))</td>
<td><code>@p.Uuid</code></td>
<td class="fw-semibold text-primary">@p.ItemName</td>
<td><span class="badge bg-danger rounded-pill">-@p.Cost.ToString("N0")</span></td>
</tr>
}
@if (!logs.Any())
{
<tr>
<td colspan="4" class="text-center py-5 text-muted fst-italic">No purchase records found.</td>
</tr>
}
</tbody>
</table>
</div>
@if (totalPages > 1)
{
<div class="d-flex justify-content-between align-items-center mt-4">
<button class="btn btn-outline-primary px-4" @onclick="PreviousPage" disabled="@(currentPage == 1)">
&laquo; Previous
</button>
<span class="text-muted fw-bold">Page <span class="text-dark">@currentPage</span> of <span class="text-dark">@totalPages</span></span>
<button class="btn btn-outline-primary px-4" @onclick="NextPage" disabled="@(currentPage == totalPages)">
Next &raquo;
</button>
</div>
}
</div>
@code {
private List<PurchaseLog> logs = new();
private int currentPage = 1;
private int pageSize = 15;
private long totalItems;
private int totalPages => (int)Math.Max(1, Math.Ceiling((double)totalItems / pageSize));
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
var filter = Builders<PurchaseLog>.Filter.Empty;
totalItems = await MongoService.PurchaseLogs.CountDocumentsAsync(filter);
logs = await MongoService.PurchaseLogs.Find(filter)
.SortByDescending(r => r.Timestamp)
.Skip((currentPage - 1) * pageSize)
.Limit(pageSize)
.ToListAsync();
}
private async Task NextPage()
{
if (currentPage < totalPages)
{
currentPage++;
await LoadData();
}
}
private async Task PreviousPage()
{
if (currentPage > 1)
{
currentPage--;
await LoadData();
}
}
}
@@ -0,0 +1,112 @@
@page "/admin/reports"
@attribute [Authorize(Roles = "op")]
@using Microsoft.AspNetCore.Authorization
@using MongoDB.Driver
@using web.PixelTalk.Models
@using web.PixelTalk.Services
@inject MongoService MongoService
<PageTitle>Manage Reports</PageTitle>
<div class="container mt-4">
<h2>Player Reports</h2>
<div class="table-responsive mt-3">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Reporter</th>
<th>Target</th>
<th>Reason</th>
<th>Date</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var report in reports)
{
<tr>
<td><code>@report.ReporterUuid</code></td>
<td><code>@report.TargetUuid</code></td>
<td>@report.Reason</td>
<td>@(DateTimeOffset.FromUnixTimeMilliseconds(report.Timestamp).ToString("g"))</td>
<td>
@if (report.Resolved)
{
<span class="badge bg-success">Resolved</span>
}
else
{
<span class="badge bg-warning text-dark">Pending</span>
}
</td>
<td>
@if (!report.Resolved)
{
<button class="btn btn-sm btn-primary" @onclick="() => RunSolve(report)">Resolve</button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<button class="btn btn-outline-secondary" @onclick="PreviousPage" disabled="@(currentPage == 1)">Previous</button>
<span>Page @currentPage of @totalPages</span>
<button class="btn btn-outline-secondary" @onclick="NextPage" disabled="@(currentPage == totalPages)">Next</button>
</div>
</div>
@code {
private List<Report> reports = new();
private int currentPage = 1;
private int pageSize = 10;
private long totalItems;
private int totalPages => (int)Math.Max(1, Math.Ceiling((double)totalItems / pageSize));
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
var filter = Builders<Report>.Filter.Empty;
totalItems = await MongoService.Reports.CountDocumentsAsync(filter);
reports = await MongoService.Reports.Find(filter)
.SortByDescending(r => r.Timestamp)
.Skip((currentPage - 1) * pageSize)
.Limit(pageSize)
.ToListAsync();
}
private async Task RunSolve(Report report)
{
var filter = Builders<Report>.Filter.Eq(r => r.Id, report.Id);
var update = Builders<Report>.Update.Set(r => r.Resolved, true);
await MongoService.Reports.UpdateOneAsync(filter, update);
report.Resolved = true;
}
private async Task NextPage()
{
if (currentPage < totalPages)
{
currentPage++;
await LoadData();
}
}
private async Task PreviousPage()
{
if (currentPage > 1)
{
currentPage--;
await LoadData();
}
}
}
@@ -0,0 +1,107 @@
@page "/admin/store"
@attribute [Authorize(Roles = "op")]
@using Microsoft.AspNetCore.Authorization
@using MongoDB.Driver
@using web.PixelTalk.Models
@using web.PixelTalk.Services
@inject MongoService MongoService
<PageTitle>Manage Store</PageTitle>
<div class="container mt-4">
<h2>Store Inventory</h2>
<div class="row mt-4">
<!-- New Item Form -->
<div class="col-md-5 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">Add New Item</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" @bind="newItem.Name" />
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" rows="3" @bind="newItem.Description"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Cost (Points)</label>
<input type="number" class="form-control" @bind="newItem.Cost" />
</div>
<button class="btn btn-success w-100" @onclick="AddItem">Add to Store</button>
@if (errorMessage != null)
{
<div class="alert alert-danger mt-3">@errorMessage</div>
}
</div>
</div>
</div>
<!-- Current Items List -->
<div class="col-md-7">
<div class="list-group shadow-sm">
@foreach (var item in items)
{
<div class="list-group-item list-group-item-action d-flex justify-content-between align-items-start py-3">
<div class="ms-2 me-auto">
<div class="fw-bold">@item.Name</div>
<small class="text-muted d-block mt-1">@item.Description</small>
<span class="badge bg-secondary mt-2">@item.Cost.ToString("N0") pts</span>
</div>
<button class="btn btn-sm btn-outline-danger ms-3 mt-1" @onclick="() => DeleteItem(item)">
Delete
</button>
</div>
}
@if (!items.Any())
{
<div class="list-group-item text-center py-4 text-muted">
No items found in the store. Add some using the form!
</div>
}
</div>
</div>
</div>
</div>
@code {
private List<StoreItem> items = new();
private StoreItem newItem = new();
private string? errorMessage;
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
items = await MongoService.StoreItems.Find(_ => true).ToListAsync();
}
private async Task AddItem()
{
errorMessage = null;
if (string.IsNullOrWhiteSpace(newItem.Name) || newItem.Cost < 0)
{
errorMessage = "Please verify item details. Name is required and Cost cannot be negative.";
return;
}
await MongoService.StoreItems.InsertOneAsync(newItem);
items.Add(newItem);
newItem = new StoreItem();
}
private async Task DeleteItem(StoreItem item)
{
var filter = Builders<StoreItem>.Filter.Eq(i => i.Id, item.Id);
await MongoService.StoreItems.DeleteOneAsync(filter);
items.Remove(item);
}
}
@@ -0,0 +1,81 @@
@page "/auth"
@inject NavigationManager NavManager
@inject web.PixelTalk.Services.MongoService MongoService
@using System.Timers
@using MongoDB.Driver
@implements IDisposable
<PageTitle>PixelTalk Authentication</PageTitle>
<div class="row min-vh-100 justify-content-center align-items-center">
<div class="col-md-6 text-center">
@if (isLoading)
{
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Generating authorization code...</p>
}
else
{
<div class="card shadow-lg p-5">
<h2 class="mb-4">Link Your Minecraft Account</h2>
<p>Run the following command in-game on the server to log in:</p>
<h3 class="font-monospace user-select-all bg-light p-3 border rounded text-primary">/pt web auth @authCode</h3>
<p class="text-muted mt-3">Waiting for authentication... This page will redirect automatically.</p>
<button class="btn btn-outline-secondary mt-3" @onclick="CheckAuthStatus">Check Now</button>
<div class="spinner-grow text-primary mt-2" role="status">
<span class="visually-hidden">Refreshing...</span>
</div>
</div>
}
</div>
</div>
@code {
private string authCode = string.Empty;
private bool isLoading = true;
private Timer? timer;
protected override async Task OnInitializedAsync()
{
authCode = GenerateCode();
var req = new web.PixelTalk.Models.AuthRequest
{
Code = authCode,
Resolved = false,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
await MongoService.AuthRequests.InsertOneAsync(req);
isLoading = false;
timer = new Timer(1500);
timer.Elapsed += async (sender, e) => await CheckAuthStatus();
timer.Start();
}
private async Task CheckAuthStatus()
{
var req = await MongoService.AuthRequests.Find(r => r.Code == authCode).FirstOrDefaultAsync();
Console.WriteLine(req == null);
if (req != null && req.Resolved)
{
timer?.Stop();
NavManager.NavigateTo($"/login_callback?code={authCode}", true);
}
}
private string GenerateCode()
{
var random = new Random();
return random.Next(100000, 999999).ToString();
}
public void Dispose()
{
timer?.Dispose();
}
}
@@ -0,0 +1,20 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter] private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
@@ -0,0 +1,109 @@
@page "/"
@using Microsoft.AspNetCore.Components.Authorization
@using web.PixelTalk.Models
@using web.PixelTalk.Services
@using MongoDB.Driver
@inject AuthenticationStateProvider AuthStateProvider
@inject MongoService MongoService
<PageTitle>PixelTalk Dashboard</PageTitle>
<AuthorizeView>
<Authorized>
<div class="container mt-4">
<h1 class="mb-4">Welcome, @player?.Nickname!</h1>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card shadow-sm text-center">
<div class="card-body">
<h5 class="card-title text-muted">Current Points</h5>
<h2 class="display-4 text-success">@player?.Points.ToString("N0")</h2>
</div>
</div>
</div>
</div>
<h3 class="mt-5 mb-3">Rewards Store</h3>
@if (message != null)
{
<div class="alert alert-info" role="alert">@message</div>
}
<div class="row">
@foreach (var item in storeItems)
{
<div class="col-md-4 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body d-flex flex-column">
<h5 class="card-title">@item.Name</h5>
<p class="card-text flex-grow-1">@item.Description</p>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="badge bg-primary fs-6">@item.Cost.ToString("N0") pts</span>
<button class="btn btn-sm btn-outline-success" @onclick="() => BuyItem(item)" disabled="@(player?.Points < item.Cost)">
Purchase
</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
</Authorized>
<NotAuthorized>
<div class="container text-center mt-5">
<h1 class="display-5">Welcome to PixelTalk</h1>
<p class="lead mt-3 text-muted">You are not authenticated. Please log in utilizing our in-game system to view your dashboard.</p>
<a href="/auth" class="btn btn-primary btn-lg mt-4 shadow-sm">Authenticate via Game</a>
</div>
</NotAuthorized>
</AuthorizeView>
@code {
private PlayerData? player;
private List<StoreItem> storeItems = new();
private string? message;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated ?? false)
{
var uuid = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (uuid != null)
{
player = await MongoService.Players.Find(p => p.Uuid == uuid).FirstOrDefaultAsync();
storeItems = await MongoService.StoreItems.Find(_ => true).ToListAsync();
}
}
}
private async Task BuyItem(StoreItem item)
{
message = null;
if (player == null || player.Points < item.Cost) return;
bool success = await MongoService.DeductPointsAsync(player.Uuid, item.Cost);
if (success)
{
player.Points -= item.Cost;
var log = new PurchaseLog
{
Uuid = player.Uuid,
ItemId = item.Id,
ItemName = item.Name,
Cost = item.Cost,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
await MongoService.PurchaseLogs.InsertOneAsync(log);
message = $"Successfully purchased {item.Name} for {item.Cost} points!";
}
else
{
message = "Transaction failed. Insufficient points or an error occurred!";
}
StateHasChanged();
}
}
@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
@@ -0,0 +1,67 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
</Found>
</Router>
@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Authorization
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp1
@using BlazorApp1.Components
@using BlazorApp1.Components.Layout
@@ -0,0 +1,84 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace web.PixelTalk.Models;
public class PlayerData
{
[BsonId]
[BsonElement("_id")]
public ObjectId Id { get; set; }
[BsonElement("uuid")]
public string Uuid { get; set; } = string.Empty;
[BsonElement("language")]
public string Language { get; set; } = string.Empty;
[BsonElement("age")] public int Age { get; set; } = 6;
[BsonElement("registered")] public bool Registred { get; set; }
[BsonElement("firstJoin")] public ulong FirstJoinTimestamp { get; set; }
[BsonElement("interests")] public string Interests { get; set; }
[BsonElement("name")]
public string Nickname { get; set; } = string.Empty;
[BsonElement("points")]
public double Points { get; set; }
[BsonElement("role")]
public string Role { get; set; } = "player";
}
public class Report
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("reporter")]
public string ReporterUuid { get; set; } = string.Empty;
[BsonElement("target")]
public string TargetUuid { get; set; } = string.Empty;
[BsonElement("reason")]
public string Reason { get; set; } = string.Empty;
[BsonElement("timestamp")]
public long Timestamp { get; set; }
[BsonElement("resolved")]
public bool Resolved { get; set; }
}
public class AuthRequest
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("code")]
public string Code { get; set; } = string.Empty;
[BsonElement("resolved")]
public bool Resolved { get; set; }
[BsonElement("uuid")]
public string? Uuid { get; set; }
[BsonElement("timestamp")]
public long Timestamp { get; set; }
}
public class StoreItem
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("name")]
public string Name { get; set; } = string.Empty;
[BsonElement("description")]
public string Description { get; set; } = string.Empty;
[BsonElement("cost")]
public int Cost { get; set; }
}
public class PurchaseLog
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("uuid")]
public string Uuid { get; set; } = string.Empty;
[BsonElement("itemId")]
public ObjectId ItemId { get; set; }
[BsonElement("itemName")]
public string ItemName { get; set; } = string.Empty;
[BsonElement("cost")]
public int Cost { get; set; }
[BsonElement("timestamp")]
public long Timestamp { get; set; }
}
@@ -0,0 +1,75 @@
using BlazorApp1.Components;
using Microsoft.AspNetCore.Authentication.Cookies;
using MongoDB.Driver;
using web.PixelTalk.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/auth";
options.Cookie.Name = "PixelTalkAuth";
});
builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IMongoClient>(sp =>
{
var connectionString = builder.Configuration.GetConnectionString("MongoDb");
return new MongoClient(connectionString ?? "mongodb://witteringgray:27017");
});
builder.Services.AddSingleton<MongoService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapGet("/login_callback", async (string code, web.PixelTalk.Services.MongoService mongoService, HttpContext context) =>
{
var req = await mongoService.AuthRequests.Find(r => r.Code == code && r.Resolved == true).FirstOrDefaultAsync();
if (req != null && !string.IsNullOrEmpty(req.Uuid))
{
Console.WriteLine(req.Uuid);
var player = await mongoService.Players.Find(p => p.Uuid == req.Uuid).FirstOrDefaultAsync();
var claims = new List<System.Security.Claims.Claim>
{
new(System.Security.Claims.ClaimTypes.NameIdentifier, req.Uuid),
new(System.Security.Claims.ClaimTypes.Name, player?.Nickname ?? req.Uuid)
};
if (player?.Role == "op")
{
claims.Add(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, "op"));
}
var identity = new System.Security.Claims.ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new System.Security.Claims.ClaimsPrincipal(identity);
await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.SignInAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, principal);
return Results.Redirect("/");
}
return Results.Redirect("/auth");
});
app.Run();
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5182",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7135;http://localhost:5182",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,34 @@
using MongoDB.Bson;
using MongoDB.Driver;
using web.PixelTalk.Models;
namespace web.PixelTalk.Services;
public class MongoService
{
private readonly IMongoDatabase _database;
public IMongoCollection<PlayerData> Players => _database.GetCollection<PlayerData>("players");
public IMongoCollection<Report> Reports => _database.GetCollection<Report>("reports");
public IMongoCollection<AuthRequest> AuthRequests => _database.GetCollection<AuthRequest>("auth_requests");
public IMongoCollection<StoreItem> StoreItems => _database.GetCollection<StoreItem>("store_items");
public IMongoCollection<PurchaseLog> PurchaseLogs => _database.GetCollection<PurchaseLog>("purchases_log");
public MongoService(IMongoClient client, IConfiguration config)
{
_database = client.GetDatabase(config.GetValue<string>("MongoDatabaseName") ?? "pixeltalk");
}
public async Task<bool> DeductPointsAsync(string uuid, int amount)
{
var filter = Builders<PlayerData>.Filter.And(
Builders<PlayerData>.Filter.Eq(x => x.Uuid, uuid),
Builders<PlayerData>.Filter.Gte(x => x.Points, amount)
);
var update = Builders<PlayerData>.Update.Inc(x => x.Points, -amount);
var result = await Players.UpdateOneAsync(filter, update);
return result.ModifiedCount > 0;
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<RootNamespace>BlazorApp1</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.6" />
<PackageReference Include="MongoDB.Driver" Version="3.7.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,60 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long