diff --git a/[core]/es_extended/server/classes/player.lua b/[core]/es_extended/server/classes/player.lua index 367c98381..67a7d7456 100644 --- a/[core]/es_extended/server/classes/player.lua +++ b/[core]/es_extended/server/classes/player.lua @@ -433,27 +433,34 @@ function CreateExtendedPlayer(playerId, identifier, ssn, group, accounts, invent reason = reason or "Unknown" if not tonumber(money) then error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) - return + return false + end + if money <= 0 then + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) + return false + end + local account = self.getAccount(accountName) + if not account then + error(("Tried To Set Add To Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) + return false + end + money = account.round and ESX.Math.Round(money) or money + if self.accounts[account.index].money - money > self.accounts[account.index].money then + error(("Tried To Underflow Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) + return false end - if money > 0 then - local account = self.getAccount(accountName) - - if account then - money = account.round and ESX.Math.Round(money) or money - if self.accounts[account.index].money - money > self.accounts[account.index].money then - error(("Tried To Underflow Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) - return - end - self.accounts[account.index].money = self.accounts[account.index].money - money - self.triggerEvent("esx:setAccountMoney", account) - TriggerEvent("esx:removeAccountMoney", self.source, accountName, money, reason) - else - error(("Tried To Set Add To Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) + if Config.CustomInventory == "ox" and Inventory and Inventory.accounts and Inventory.accounts[accountName] then + local removed = Inventory.RemoveItem(self.source, accountName, money) + if not removed then + return false, 0 end - else - error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) end + + self.accounts[account.index].money = self.accounts[account.index].money - money + self.triggerEvent("esx:setAccountMoney", account) + TriggerEvent("esx:removeAccountMoney", self.source, accountName, money, reason) + return true, money end function self.getInventoryItem(itemName) diff --git a/[core]/esx_whitelist/.gitignore b/[core]/esx_whitelist/.gitignore new file mode 100644 index 000000000..f706d8927 --- /dev/null +++ b/[core]/esx_whitelist/.gitignore @@ -0,0 +1,11 @@ +# Node modules +web/node_modules/ + +# Logs +*.log + +# Config de entorno +.env +web/dist +web/.next/ +whitelist_config.json diff --git a/[core]/esx_whitelist/README.md b/[core]/esx_whitelist/README.md new file mode 100644 index 000000000..2eee60338 --- /dev/null +++ b/[core]/esx_whitelist/README.md @@ -0,0 +1,74 @@ +

ESX Whitelist System

+

Discord - Website - Documentation

+ +

Dynamic whitelist with automated rules and Discord integration

+ +
+ +## ✨ Features + +- 🎯 Dynamic whitelist control without restarts +- ⏱️ Grace period system before kick +- 🎨 Modern tablet-style UI panel +- 🔍 Auto-detect identifiers (License, Steam, Discord, XBL, FiveM) +- 🤖 Automated rules (player count, admin presence, scheduled) +- 🔔 Discord webhook logs + role verification +- ⚡ Optimized performance (6x faster queries) +- 🔐 Secure with rate limiting & validation + +--- + +## 📦 Installation + +1. Extract to `resources/esx_whitelist` +2. Add to `server.cfg`: `ensure esx_whitelist` +3. Configure Discord token in `server/cfg_discord.lua` (optional) +4. Restart server + +**Requirements:** ESX Legacy, oxmysql, ox_lib + +--- + +## 🎮 Commands + +**In-Game (Admin)** +``` +/whitelist - Open panel +/wl_add [id] - Add player +/wl_check [id] - Check status +``` + +**Console** +``` +wl_add [identifier] - Add by identifier +wl_remove [identifier] - Remove player +wl_on / wl_off - Toggle whitelist +``` + +--- + +## ⚙️ Configuration + +Edit `config.lua`: +```lua +Config.Locale = 'en' +Config.UICommand = 'whitelist' +Config.AdminGroups = { 'admin', 'mod' } +``` + +Discord token in `server/cfg_discord.lua`: +```lua +Config.DiscordBotToken = "YOUR_BOT_TOKEN" +``` + +--- + +## 💬 Support + +- [ESX Discord](https://discord.esx-framework.org/) +- [ESX Documentation](https://docs.esx-framework.org/en) + +--- + +

Developed by ESX TEAM - Alx

+

Made with ❤️ for the ESX Community

\ No newline at end of file diff --git a/[core]/esx_whitelist/client/main.lua b/[core]/esx_whitelist/client/main.lua new file mode 100644 index 000000000..55728de28 --- /dev/null +++ b/[core]/esx_whitelist/client/main.lua @@ -0,0 +1,143 @@ +local isWhitelistEnabled = false +local isGracePeriodActive = false +local gracePeriodEndTime = 0 +local isUIVisible = false +local translations = {} + +local function loadLocaleFile() + local localeFile = LoadResourceFile(GetCurrentResourceName(), 'locales/' .. Config.Locale .. '.json') + if localeFile then + translations = json.decode(localeFile) + end +end + +local function T(key, ...) + local str = translations[key] or key + if not str then return key end + if ... then + local success, result = pcall(string.format, str, ...) + return success and result or str + end + return str +end + +RegisterNetEvent('esx_whitelist:stateChanged', function(enabled) + isWhitelistEnabled = enabled +end) + +RegisterNetEvent('esx_whitelist:startGracePeriod', function(seconds) + isGracePeriodActive = true + gracePeriodEndTime = GetGameTimer() + (seconds * 1000) + + CreateThread(function() + while isGracePeriodActive and GetGameTimer() < gracePeriodEndTime do + Wait(1000) + local remainingSeconds = math.ceil((gracePeriodEndTime - GetGameTimer()) / 1000) + if remainingSeconds > 0 then + ESX.ShowNotification(string.format('~r~%s~s~\n%s', T('whitelist_active'), T('remaining_time', remainingSeconds))) + end + end + isGracePeriodActive = false + end) +end) + +RegisterNetEvent('esx_whitelist:cancelGracePeriod', function() + isGracePeriodActive = false + gracePeriodEndTime = 0 + ESX.ShowNotification('~g~' .. T('grace_cancelled')) +end) + +local function toggleUI(visible) + isUIVisible = visible + SetNuiFocus(visible, visible) + + if not visible then + SendNUIMessage({ + action = 'closeUI' + }) + end +end + +RegisterCommand(Config.UICommand, function() + if isUIVisible then return end + + ESX.TriggerServerCallback('esx_whitelist:getConfig', function(serverConfig) + if not serverConfig then + ESX.ShowNotification('~r~' .. T('no_permission')) + return + end + + toggleUI(true) + SendNUIMessage({ + action = 'openUI', + data = serverConfig + }) + end) +end) + +RegisterNUICallback('closeUI', function(data, cb) + toggleUI(false) + cb('ok') +end) + +RegisterNUICallback('updateConfig', function(configData, cb) + ESX.TriggerServerCallback('esx_whitelist:updateConfig', function(success) + cb(success) + end, configData) +end) + +RegisterNUICallback('testWebhook', function(data, cb) + ESX.TriggerServerCallback('esx_whitelist:testWebhook', function(success) + if success then + ESX.ShowNotification('~g~' .. T('webhook_sent')) + end + cb(success) + end) +end) + +RegisterNUICallback('getWhitelistEntries', function(data, cb) + ESX.TriggerServerCallback('esx_whitelist:getWhitelistEntries', function(entries) + cb(entries or {}) + end) +end) + +RegisterNUICallback('getConfig', function(data, cb) + ESX.TriggerServerCallback('esx_whitelist:getConfig', function(serverConfig) + cb(serverConfig or {}) + end) +end) + +RegisterNUICallback('managePlayer', function(data, cb) + ESX.TriggerServerCallback('esx_whitelist:managePlayer', function(success, message) + cb({success = success, message = message}) + end, data) +end) + +RegisterNUICallback('toggleWhitelistStatus', function(data, cb) + ESX.TriggerServerCallback('esx_whitelist:toggleWhitelistStatus', function(success) + cb(success) + end, data) +end) + +local function DisableControls() + while isUIVisible do + Wait(0) + DisableControlAction(0, 1, true) + DisableControlAction(0, 2, true) + DisableControlAction(0, 142, true) + DisableControlAction(0, 18, true) + DisableControlAction(0, 322, true) + DisableControlAction(0, 106, true) + end +end + +CreateThread(function() + loadLocaleFile() + + while true do + Wait(100) + if isUIVisible then + DisableControls() + end + end +end) \ No newline at end of file diff --git a/[core]/esx_whitelist/config.lua b/[core]/esx_whitelist/config.lua new file mode 100644 index 000000000..e4415dfe8 --- /dev/null +++ b/[core]/esx_whitelist/config.lua @@ -0,0 +1,14 @@ +Config = {} + +Config.Locale = 'en' +Config.Debug = false + +Config.UICommand = 'whitelist' + +Config.AdminGroups = { + 'admin', + 'mod' +} + +Config.ConsoleCommands = true +Config.InGameCommands = true \ No newline at end of file diff --git a/[core]/esx_whitelist/fxmanifest.lua b/[core]/esx_whitelist/fxmanifest.lua new file mode 100644 index 000000000..36711d1c9 --- /dev/null +++ b/[core]/esx_whitelist/fxmanifest.lua @@ -0,0 +1,35 @@ +fx_version 'cerulean' + +game 'gta5' + +description 'ESX Dynamic Whitelist System' + +version '1.0.0' + +lua54 'yes' + +shared_scripts { + '@es_extended/imports.lua', + '@ox_lib/init.lua', + 'config.lua' +} + +server_scripts { + '@oxmysql/lib/MySQL.lua', + '@es_extended/locale.lua', + 'server/main.lua', + 'server/cfg_discord.lua', + 'server/commands.lua' +} + +client_scripts { + '@es_extended/locale.lua', + 'client/main.lua' +} + +ui_page 'web/dist/index.html' + +files { + 'web/dist/**/*', + 'locales/*.json' +} \ No newline at end of file diff --git a/[core]/esx_whitelist/install.sql b/[core]/esx_whitelist/install.sql new file mode 100644 index 000000000..197716749 --- /dev/null +++ b/[core]/esx_whitelist/install.sql @@ -0,0 +1,25 @@ +-- ESX Whitelist System Database Tables +-- Note: These tables are created automatically by the script +-- This file is provided for manual setup or debugging purposes only + +CREATE TABLE IF NOT EXISTS `whitelist` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `player_name` VARCHAR(255) COLLATE utf8mb4_unicode_ci, + `whitelisted` TINYINT(1) NOT NULL DEFAULT 0, + `added_by` VARCHAR(255) COLLATE utf8mb4_unicode_ci, + `added_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `whitelist_identifiers` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `whitelist_id` INT NOT NULL, + `type` ENUM('steam', 'license', 'discord', 'xbl', 'live', 'fivem') NOT NULL, + `identifier` VARCHAR(255) NOT NULL COLLATE utf8mb4_bin, + FOREIGN KEY (`whitelist_id`) REFERENCES `whitelist`(`id`) ON DELETE CASCADE, + UNIQUE KEY `unique_identifier` (`type`, `identifier`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Indexes for better performance +CREATE INDEX `idx_whitelisted` ON `whitelist`(`whitelisted`); +CREATE INDEX `idx_whitelist_id` ON `whitelist_identifiers`(`whitelist_id`); +CREATE INDEX `idx_identifier` ON `whitelist_identifiers`(`identifier`); \ No newline at end of file diff --git a/[core]/esx_whitelist/locales/de.json b/[core]/esx_whitelist/locales/de.json new file mode 100644 index 000000000..a2681dfa1 --- /dev/null +++ b/[core]/esx_whitelist/locales/de.json @@ -0,0 +1,101 @@ +{ + "ui_title": "Whitelist Panel", + "ui_subtitle": "Server-Zugriffsverwaltungssystem", + "current_status": "Aktueller Status", + "whitelist_enabled": "Whitelist Aktiviert", + "whitelist_disabled": "Whitelist Deaktiviert", + "grace_period": "Schonfrist", + "grace_period_desc": "Zeit vor dem Ausschluss nicht autorisierter Spieler", + "grace_period_seconds": "Schonfrist Sekunden", + "kick_connected_label": "Verbundene kicken", + "discord_notifications": "Discord-Benachrichtigungen", + "discord_notifications_desc": "Ereignisse auf Discord protokollieren", + "webhook_url": "Webhook-URL", + "webhook_configured": "Webhook bereits konfiguriert", + "test_webhook": "Webhook Testen", + "discord_verification": "Discord-Verifizierung", + "discord_verification_desc": "Discord-Rollen bei Verbindung überprüfen", + "discord_token_status": "Bot-Token Status", + "discord_token_configured": "Token korrekt konfiguriert", + "discord_token_missing": "Token nicht konfiguriert", + "discord_token_warning": "Ein gültiger Bot-Token ist in cfg_discord.lua für die Discord-Verifizierung erforderlich", + "guild_id": "Server-ID", + "role_id": "Rollen-ID", + "manage_whitelist": "Manuelle Verwaltung", + "manage_whitelist_desc": "Spieler manuell hinzufügen oder entfernen", + "view_list": "Liste Anzeigen", + "action": "Aktion", + "add_to_whitelist": "Hinzufügen", + "remove_from_whitelist": "Entfernen", + "identifier_type": "Typ", + "identifier_value": "Identifikator", + "apply": "Anwenden", + "whitelist_rules": "Automatische Regeln", + "whitelist_rules_desc": "Whitelist automatisch aktivieren/deaktivieren", + "add_admin": "Admin", + "add_players": "Spieler", + "add_schedule": "Zeitplan", + "admin_presence": "Admin-Anwesenheit", + "player_count": "Spielerzähler", + "scheduled_time": "Geplante Zeit", + "priority": "Priorität", + "condition": "Bedingung", + "less_than": "Kleiner als (<)", + "greater_than": "Größer als (>)", + "greater_equal": "Größer oder gleich (>=)", + "equal_to": "Gleich (==)", + "admin_count": "Anzahl Admins", + "player_count_label": "Anzahl Spieler", + "enable_whitelist": "Aktivieren", + "disable_whitelist": "Deaktivieren", + "start_time": "Startzeit", + "end_time": "Endzeit", + "save_configuration": "Speichern", + "reset": "Zurücksetzen", + "whitelist_list": "Whitelist-Liste", + "search_players": "Spieler suchen...", + "total_entries": "Gesamteinträge", + "no_results": "Keine Ergebnisse gefunden", + "no_entries": "Keine Einträge", + "view_identifiers": "Identifikatoren anzeigen", + "hide_identifiers": "Identifikatoren verbergen", + "whitelisted": "Auf Whitelist", + "not_whitelisted": "Nicht auf Whitelist", + "checking_whitelist": "Whitelist wird überprüft...", + "kick_message": "Sie sind nicht auf der Whitelist", + "no_perms": "Sie haben keine Berechtigung", + "usage_wl_add": "Verwendung: /wl_add [Spieler-ID]", + "player_not_found": "Spieler nicht gefunden", + "player_already_wl": "Spieler ist bereits auf der Whitelist", + "player_added": "Spieler zur Whitelist hinzugefügt", + "added_to_whitelist": "Sie wurden zur Whitelist hinzugefügt", + "usage_wl_check": "Verwendung: /wl_check [Spieler-ID]", + "player_has_wl": "%s ist auf der Whitelist", + "player_no_wl": "%s ist NICHT auf der Whitelist", + "player_is_admin": "Dieser Spieler ist Administrator", + "config_saved": "Konfiguration gespeichert", + "webhook_invalid": "Ungültige Webhook-URL", + "webhook_updated": "Webhook aktualisiert", + "webhook_test": "Webhook-Test - Angefordert von %s", + "discord_no_token": "Discord aktiviert, aber kein Token konfiguriert", + "discord_missing_config": "Guild-ID oder Rollen-ID fehlt", + "whitelist_enabled_manual": "Whitelist manuell aktiviert von %s", + "whitelist_disabled_manual": "Whitelist manuell deaktiviert von %s", + "whitelist_enabled_auto": "Whitelist automatisch durch Regeln aktiviert", + "whitelist_disabled_auto": "Whitelist automatisch durch Regeln deaktiviert", + "discord_player_added": "✅ %s hat hinzugefügt: %s", + "discord_player_removed": "❌ %s hat entfernt: %s", + "invalid_identifier": "Ungültiger Identifikator", + "player_added_ui": "Spieler erfolgreich hinzugefügt", + "player_removed_ui": "Spieler erfolgreich entfernt", + "player_not_in_wl": "Spieler ist nicht auf der Whitelist", + "discord_title": "🛡️ ESX Whitelist", + "webhook_sent": "Test-Webhook gesendet", + "whitelist_active": "Whitelist Aktiv", + "remaining_time": "Verbleibende Zeit: %d Sekunden", + "grace_cancelled": "Schonfrist abgebrochen", + "no_permission": "Sie haben keine Berechtigung für diesen Befehl", + "invalid_identifier_format": "Ungültiges Identifikatorformat", + "ip_not_allowed": "IP-Identifikatoren sind nicht erlaubt", + "auto_detected": "Automatisch erkannt" +} \ No newline at end of file diff --git a/[core]/esx_whitelist/locales/en.json b/[core]/esx_whitelist/locales/en.json new file mode 100644 index 000000000..ea2d0efe3 --- /dev/null +++ b/[core]/esx_whitelist/locales/en.json @@ -0,0 +1,101 @@ +{ + "checking_whitelist": "Checking whitelist...", + "no_permission": "You do not have permission to access the whitelist panel", + "config_saved": "Configuration saved", + "webhook_sent": "Webhook sent", + "kick_message": "The server is whitelisted. You do not have access.", + "whitelist_active": "Whitelist active", + "remaining_time": "You have %s seconds to be added", + "whitelist_enabled_auto": "Whitelist automatically enabled", + "whitelist_disabled_auto": "Whitelist automatically disabled", + "whitelist_enabled_manual": "Whitelist manually enabled by %s", + "whitelist_disabled_manual": "Whitelist manually disabled by %s", + "webhook_test": "Webhook test performed by %s", + "discord_title": "Whitelist System", + "ui_title": "Whitelist Control", + "ui_subtitle": "Manage your server's dynamic whitelist system", + "current_status": "Current Status", + "whitelist_enabled": "Whitelist Enabled", + "whitelist_disabled": "Whitelist Disabled", + "grace_period": "Grace Period", + "grace_period_desc": "Time before kicking players", + "grace_period_seconds": "Grace Period (seconds)", + "kick_connected_label": "Kick Connected Players", + "discord_notifications": "Discord Notifications", + "discord_notifications_desc": "Receive status change notifications", + "webhook_url": "Webhook URL", + "test_webhook": "Test Webhook", + "discord_verification": "Discord Verification", + "discord_verification_desc": "Verify players using Discord roles", + "guild_id": "Guild ID", + "role_id": "Role ID", + "bot_token": "Bot Token", + "bot_token_configured": "Token configured. Enter a new one to change it.", + "manage_whitelist": "Manual Whitelist Management", + "manage_whitelist_desc": "Add or remove players directly by identifier", + "add_to_whitelist": "Add to Whitelist", + "remove_from_whitelist": "Remove from Whitelist", + "identifier_type": "Identifier Type", + "identifier_value": "Identifier Value", + "apply": "Apply", + "view_list": "View List", + "whitelist_list": "Whitelist Entries", + "total_entries": "Total Entries", + "no_entries": "No entries in the whitelist", + "no_results": "No results found", + "search_players": "Search players...", + "whitelisted": "Whitelisted", + "not_whitelisted": "Not Whitelisted", + "view_identifiers": "View identifiers", + "hide_identifiers": "Hide", + "whitelist_rules": "Whitelist Rules", + "whitelist_rules_desc": "Configure dynamic whitelist conditions", + "add_admin": "Admin", + "add_players": "Players", + "add_schedule": "Schedule", + "admin_presence": "Admin Presence", + "player_count": "Player Count", + "scheduled_time": "Scheduled Time", + "priority": "Priority", + "condition": "Condition", + "less_than": "Less than", + "greater_than": "Greater than", + "greater_equal": "Greater or equal", + "equal_to": "Equal to", + "admin_count": "Admin Count", + "player_count_label": "Player Count", + "action": "Action", + "enable_whitelist": "Enable Whitelist", + "disable_whitelist": "Disable Whitelist", + "start_time": "Start Time", + "end_time": "End Time", + "reset": "Reset", + "save_configuration": "Save Configuration", + "webhook_configured": "Webhook configured. Enter a new one to change it.", + "no_perms": "No permissions", + "player_added": "Player added to whitelist", + "player_removed": "Player removed from whitelist", + "player_not_found": "Player not found", + "player_has_wl": "%s HAS whitelist", + "player_no_wl": "%s does NOT have whitelist", + "player_is_admin": "Is administrator", + "usage_wl_add": "Usage: /wl_add [id]", + "usage_wl_check": "Usage: /wl_check [id]", + "webhook_updated": "Webhook updated", + "webhook_invalid": "Invalid webhook URL", + "grace_cancelled": "Grace period cancelled", + "added_to_whitelist": "You have been added to the whitelist", + "invalid_identifier": "Invalid identifier", + "player_added_ui": "Player added to whitelist", + "player_removed_ui": "Player removed from whitelist", + "player_already_wl": "Player already has whitelist", + "player_not_in_wl": "Player not in whitelist", + "discord_player_added": "%s added %s to whitelist", + "discord_player_removed": "%s removed %s from whitelist", + "discord_no_token": "Discord Bot Token is not configured or has invalid format", + "discord_missing_config": "Discord configuration incomplete. Guild ID and Role ID are required.", + "discord_token_status": "Bot Token Status", + "discord_token_configured": "Valid Token Configured", + "discord_token_missing": "No Valid Token", + "discord_token_warning": "Discord verification requires a valid Bot Token configured in server/cfg_discord.lua" +} \ No newline at end of file diff --git a/[core]/esx_whitelist/locales/es.json b/[core]/esx_whitelist/locales/es.json new file mode 100644 index 000000000..89db7f8ba --- /dev/null +++ b/[core]/esx_whitelist/locales/es.json @@ -0,0 +1,101 @@ +{ + "ui_title": "Panel de Whitelist", + "ui_subtitle": "Sistema de gestión de acceso al servidor", + "current_status": "Estado Actual", + "whitelist_enabled": "Whitelist Activada", + "whitelist_disabled": "Whitelist Desactivada", + "grace_period": "Período de Gracia", + "grace_period_desc": "Tiempo antes de expulsar a jugadores no autorizados", + "grace_period_seconds": "Segundos de gracia", + "kick_connected_label": "Expulsar conectados", + "discord_notifications": "Notificaciones Discord", + "discord_notifications_desc": "Registra eventos en Discord", + "webhook_url": "URL del Webhook", + "webhook_configured": "Webhook ya configurado", + "test_webhook": "Probar Webhook", + "discord_verification": "Verificación Discord", + "discord_verification_desc": "Verificar roles de Discord al conectar", + "discord_token_status": "Estado del Bot Token", + "discord_token_configured": "Token configurado correctamente", + "discord_token_missing": "Token no configurado", + "discord_token_warning": "Se requiere un bot token válido en cfg_discord.lua para la verificación de Discord", + "guild_id": "ID del Servidor", + "role_id": "ID del Rol", + "manage_whitelist": "Gestión Manual", + "manage_whitelist_desc": "Añadir o eliminar jugadores manualmente", + "view_list": "Ver Lista", + "action": "Acción", + "add_to_whitelist": "Añadir", + "remove_from_whitelist": "Eliminar", + "identifier_type": "Tipo", + "identifier_value": "Identificador", + "apply": "Aplicar", + "whitelist_rules": "Reglas Automáticas", + "whitelist_rules_desc": "Activa/desactiva la whitelist automáticamente", + "add_admin": "Admin", + "add_players": "Jugadores", + "add_schedule": "Horario", + "admin_presence": "Presencia Admin", + "player_count": "Contador Jugadores", + "scheduled_time": "Horario Programado", + "priority": "Prioridad", + "condition": "Condición", + "less_than": "Menor que (<)", + "greater_than": "Mayor que (>)", + "greater_equal": "Mayor o igual (>=)", + "equal_to": "Igual a (==)", + "admin_count": "Cantidad de admins", + "player_count_label": "Cantidad de jugadores", + "enable_whitelist": "Activar", + "disable_whitelist": "Desactivar", + "start_time": "Hora inicio", + "end_time": "Hora fin", + "save_configuration": "Guardar", + "reset": "Reiniciar", + "whitelist_list": "Lista de Whitelist", + "search_players": "Buscar jugadores...", + "total_entries": "Entradas totales", + "no_results": "No se encontraron resultados", + "no_entries": "No hay entradas", + "view_identifiers": "Ver identificadores", + "hide_identifiers": "Ocultar identificadores", + "whitelisted": "En whitelist", + "not_whitelisted": "No en whitelist", + "checking_whitelist": "Verificando whitelist...", + "kick_message": "No estás en la whitelist", + "no_perms": "No tienes permisos", + "usage_wl_add": "Uso: /wl_add [ID del jugador]", + "player_not_found": "Jugador no encontrado", + "player_already_wl": "El jugador ya está en la whitelist", + "player_added": "Jugador añadido a la whitelist", + "added_to_whitelist": "Has sido añadido a la whitelist", + "usage_wl_check": "Uso: /wl_check [ID del jugador]", + "player_has_wl": "%s está en la whitelist", + "player_no_wl": "%s NO está en la whitelist", + "player_is_admin": "Este jugador es administrador", + "config_saved": "Configuración guardada", + "webhook_invalid": "URL del webhook inválida", + "webhook_updated": "Webhook actualizado", + "webhook_test": "Test de webhook - Solicitado por %s", + "discord_no_token": "Discord activado pero no hay token configurado", + "discord_missing_config": "Falta configurar Guild ID o Role ID", + "whitelist_enabled_manual": "Whitelist activada manualmente por %s", + "whitelist_disabled_manual": "Whitelist desactivada manualmente por %s", + "whitelist_enabled_auto": "Whitelist activada automáticamente por reglas", + "whitelist_disabled_auto": "Whitelist desactivada automáticamente por reglas", + "discord_player_added": "✅ %s añadió a: %s", + "discord_player_removed": "❌ %s eliminó a: %s", + "invalid_identifier": "Identificador inválido", + "player_added_ui": "Jugador añadido correctamente", + "player_removed_ui": "Jugador eliminado correctamente", + "player_not_in_wl": "El jugador no está en la whitelist", + "discord_title": "🛡️ ESX Whitelist", + "webhook_sent": "Webhook de prueba enviado", + "whitelist_active": "Whitelist Activa", + "remaining_time": "Tiempo restante: %d segundos", + "grace_cancelled": "Período de gracia cancelado", + "no_permission": "No tienes permiso para usar este comando", + "invalid_identifier_format": "Formato de identificador inválido", + "ip_not_allowed": "No se permiten identificadores IP", + "auto_detected": "Detectado automáticamente" +} \ No newline at end of file diff --git a/[core]/esx_whitelist/locales/fr.json b/[core]/esx_whitelist/locales/fr.json new file mode 100644 index 000000000..ad24b6129 --- /dev/null +++ b/[core]/esx_whitelist/locales/fr.json @@ -0,0 +1,101 @@ +{ + "ui_title": "Panneau Whitelist", + "ui_subtitle": "Système de gestion d'accès au serveur", + "current_status": "État Actuel", + "whitelist_enabled": "Whitelist Activée", + "whitelist_disabled": "Whitelist Désactivée", + "grace_period": "Période de Grâce", + "grace_period_desc": "Temps avant d'expulser les joueurs non autorisés", + "grace_period_seconds": "Secondes de grâce", + "kick_connected_label": "Expulser les connectés", + "discord_notifications": "Notifications Discord", + "discord_notifications_desc": "Enregistrer les événements sur Discord", + "webhook_url": "URL du Webhook", + "webhook_configured": "Webhook déjà configuré", + "test_webhook": "Tester le Webhook", + "discord_verification": "Vérification Discord", + "discord_verification_desc": "Vérifier les rôles Discord à la connexion", + "discord_token_status": "État du Token Bot", + "discord_token_configured": "Token configuré correctement", + "discord_token_missing": "Token non configuré", + "discord_token_warning": "Un token bot valide est requis dans cfg_discord.lua pour la vérification Discord", + "guild_id": "ID du Serveur", + "role_id": "ID du Rôle", + "manage_whitelist": "Gestion Manuelle", + "manage_whitelist_desc": "Ajouter ou retirer des joueurs manuellement", + "view_list": "Voir la Liste", + "action": "Action", + "add_to_whitelist": "Ajouter", + "remove_from_whitelist": "Retirer", + "identifier_type": "Type", + "identifier_value": "Identifiant", + "apply": "Appliquer", + "whitelist_rules": "Règles Automatiques", + "whitelist_rules_desc": "Active/désactive automatiquement la whitelist", + "add_admin": "Admin", + "add_players": "Joueurs", + "add_schedule": "Horaire", + "admin_presence": "Présence Admin", + "player_count": "Compteur Joueurs", + "scheduled_time": "Horaire Programmé", + "priority": "Priorité", + "condition": "Condition", + "less_than": "Inférieur à (<)", + "greater_than": "Supérieur à (>)", + "greater_equal": "Supérieur ou égal (>=)", + "equal_to": "Égal à (==)", + "admin_count": "Nombre d'admins", + "player_count_label": "Nombre de joueurs", + "enable_whitelist": "Activer", + "disable_whitelist": "Désactiver", + "start_time": "Heure de début", + "end_time": "Heure de fin", + "save_configuration": "Enregistrer", + "reset": "Réinitialiser", + "whitelist_list": "Liste Whitelist", + "search_players": "Rechercher des joueurs...", + "total_entries": "Entrées totales", + "no_results": "Aucun résultat trouvé", + "no_entries": "Aucune entrée", + "view_identifiers": "Voir les identifiants", + "hide_identifiers": "Masquer les identifiants", + "whitelisted": "En whitelist", + "not_whitelisted": "Pas en whitelist", + "checking_whitelist": "Vérification de la whitelist...", + "kick_message": "Vous n'êtes pas sur la whitelist", + "no_perms": "Vous n'avez pas les permissions", + "usage_wl_add": "Usage: /wl_add [ID du joueur]", + "player_not_found": "Joueur introuvable", + "player_already_wl": "Le joueur est déjà sur la whitelist", + "player_added": "Joueur ajouté à la whitelist", + "added_to_whitelist": "Vous avez été ajouté à la whitelist", + "usage_wl_check": "Usage: /wl_check [ID du joueur]", + "player_has_wl": "%s est sur la whitelist", + "player_no_wl": "%s n'est PAS sur la whitelist", + "player_is_admin": "Ce joueur est administrateur", + "config_saved": "Configuration enregistrée", + "webhook_invalid": "URL du webhook invalide", + "webhook_updated": "Webhook mis à jour", + "webhook_test": "Test du webhook - Demandé par %s", + "discord_no_token": "Discord activé mais aucun token configuré", + "discord_missing_config": "Guild ID ou Role ID manquant", + "whitelist_enabled_manual": "Whitelist activée manuellement par %s", + "whitelist_disabled_manual": "Whitelist désactivée manuellement par %s", + "whitelist_enabled_auto": "Whitelist activée automatiquement par les règles", + "whitelist_disabled_auto": "Whitelist désactivée automatiquement par les règles", + "discord_player_added": "✅ %s a ajouté: %s", + "discord_player_removed": "❌ %s a retiré: %s", + "invalid_identifier": "Identifiant invalide", + "player_added_ui": "Joueur ajouté avec succès", + "player_removed_ui": "Joueur retiré avec succès", + "player_not_in_wl": "Le joueur n'est pas sur la whitelist", + "discord_title": "🛡️ ESX Whitelist", + "webhook_sent": "Webhook de test envoyé", + "whitelist_active": "Whitelist Active", + "remaining_time": "Temps restant: %d secondes", + "grace_cancelled": "Période de grâce annulée", + "no_permission": "Vous n'avez pas la permission d'utiliser cette commande", + "invalid_identifier_format": "Format d'identifiant invalide", + "ip_not_allowed": "Les identifiants IP ne sont pas autorisés", + "auto_detected": "Détecté automatiquement" +} \ No newline at end of file diff --git a/[core]/esx_whitelist/locales/it.json b/[core]/esx_whitelist/locales/it.json new file mode 100644 index 000000000..d4ccd2a60 --- /dev/null +++ b/[core]/esx_whitelist/locales/it.json @@ -0,0 +1,101 @@ +{ + "ui_title": "Pannello Whitelist", + "ui_subtitle": "Sistema di gestione accesso al server", + "current_status": "Stato Attuale", + "whitelist_enabled": "Whitelist Attivata", + "whitelist_disabled": "Whitelist Disattivata", + "grace_period": "Periodo di Grazia", + "grace_period_desc": "Tempo prima di espellere i giocatori non autorizzati", + "grace_period_seconds": "Secondi di grazia", + "kick_connected_label": "Espelli connessi", + "discord_notifications": "Notifiche Discord", + "discord_notifications_desc": "Registra eventi su Discord", + "webhook_url": "URL del Webhook", + "webhook_configured": "Webhook già configurato", + "test_webhook": "Testa Webhook", + "discord_verification": "Verifica Discord", + "discord_verification_desc": "Verifica i ruoli Discord alla connessione", + "discord_token_status": "Stato Token Bot", + "discord_token_configured": "Token configurato correttamente", + "discord_token_missing": "Token non configurato", + "discord_token_warning": "È richiesto un token bot valido in cfg_discord.lua per la verifica Discord", + "guild_id": "ID del Server", + "role_id": "ID del Ruolo", + "manage_whitelist": "Gestione Manuale", + "manage_whitelist_desc": "Aggiungi o rimuovi giocatori manualmente", + "view_list": "Vedi Lista", + "action": "Azione", + "add_to_whitelist": "Aggiungi", + "remove_from_whitelist": "Rimuovi", + "identifier_type": "Tipo", + "identifier_value": "Identificatore", + "apply": "Applica", + "whitelist_rules": "Regole Automatiche", + "whitelist_rules_desc": "Attiva/disattiva automaticamente la whitelist", + "add_admin": "Admin", + "add_players": "Giocatori", + "add_schedule": "Orario", + "admin_presence": "Presenza Admin", + "player_count": "Contatore Giocatori", + "scheduled_time": "Orario Programmato", + "priority": "Priorità", + "condition": "Condizione", + "less_than": "Minore di (<)", + "greater_than": "Maggiore di (>)", + "greater_equal": "Maggiore o uguale (>=)", + "equal_to": "Uguale a (==)", + "admin_count": "Numero di admin", + "player_count_label": "Numero di giocatori", + "enable_whitelist": "Attiva", + "disable_whitelist": "Disattiva", + "start_time": "Ora inizio", + "end_time": "Ora fine", + "save_configuration": "Salva", + "reset": "Reimposta", + "whitelist_list": "Lista Whitelist", + "search_players": "Cerca giocatori...", + "total_entries": "Voci totali", + "no_results": "Nessun risultato trovato", + "no_entries": "Nessuna voce", + "view_identifiers": "Vedi identificatori", + "hide_identifiers": "Nascondi identificatori", + "whitelisted": "In whitelist", + "not_whitelisted": "Non in whitelist", + "checking_whitelist": "Verifica whitelist...", + "kick_message": "Non sei nella whitelist", + "no_perms": "Non hai i permessi", + "usage_wl_add": "Uso: /wl_add [ID giocatore]", + "player_not_found": "Giocatore non trovato", + "player_already_wl": "Il giocatore è già nella whitelist", + "player_added": "Giocatore aggiunto alla whitelist", + "added_to_whitelist": "Sei stato aggiunto alla whitelist", + "usage_wl_check": "Uso: /wl_check [ID giocatore]", + "player_has_wl": "%s è nella whitelist", + "player_no_wl": "%s NON è nella whitelist", + "player_is_admin": "Questo giocatore è amministratore", + "config_saved": "Configurazione salvata", + "webhook_invalid": "URL webhook non valido", + "webhook_updated": "Webhook aggiornato", + "webhook_test": "Test webhook - Richiesto da %s", + "discord_no_token": "Discord attivato ma nessun token configurato", + "discord_missing_config": "Manca la configurazione Guild ID o Role ID", + "whitelist_enabled_manual": "Whitelist attivata manualmente da %s", + "whitelist_disabled_manual": "Whitelist disattivata manualmente da %s", + "whitelist_enabled_auto": "Whitelist attivata automaticamente dalle regole", + "whitelist_disabled_auto": "Whitelist disattivata automaticamente dalle regole", + "discord_player_added": "✅ %s ha aggiunto: %s", + "discord_player_removed": "❌ %s ha rimosso: %s", + "invalid_identifier": "Identificatore non valido", + "player_added_ui": "Giocatore aggiunto con successo", + "player_removed_ui": "Giocatore rimosso con successo", + "player_not_in_wl": "Il giocatore non è nella whitelist", + "discord_title": "🛡️ ESX Whitelist", + "webhook_sent": "Webhook di test inviato", + "whitelist_active": "Whitelist Attiva", + "remaining_time": "Tempo rimanente: %d secondi", + "grace_cancelled": "Periodo di grazia annullato", + "no_permission": "Non hai il permesso per usare questo comando", + "invalid_identifier_format": "Formato identificatore non valido", + "ip_not_allowed": "Gli identificatori IP non sono consentiti", + "auto_detected": "Rilevato automaticamente" +} \ No newline at end of file diff --git a/[core]/esx_whitelist/locales/pt.json b/[core]/esx_whitelist/locales/pt.json new file mode 100644 index 000000000..9151ed9be --- /dev/null +++ b/[core]/esx_whitelist/locales/pt.json @@ -0,0 +1,101 @@ +{ + "ui_title": "Painel Whitelist", + "ui_subtitle": "Sistema de gestão de acesso ao servidor", + "current_status": "Status Atual", + "whitelist_enabled": "Whitelist Ativada", + "whitelist_disabled": "Whitelist Desativada", + "grace_period": "Período de Carência", + "grace_period_desc": "Tempo antes de expulsar jogadores não autorizados", + "grace_period_seconds": "Segundos de carência", + "kick_connected_label": "Expulsar conectados", + "discord_notifications": "Notificações Discord", + "discord_notifications_desc": "Registrar eventos no Discord", + "webhook_url": "URL do Webhook", + "webhook_configured": "Webhook já configurado", + "test_webhook": "Testar Webhook", + "discord_verification": "Verificação Discord", + "discord_verification_desc": "Verificar roles do Discord ao conectar", + "discord_token_status": "Status do Token Bot", + "discord_token_configured": "Token configurado corretamente", + "discord_token_missing": "Token não configurado", + "discord_token_warning": "É necessário um token bot válido em cfg_discord.lua para verificação do Discord", + "guild_id": "ID do Servidor", + "role_id": "ID do Cargo", + "manage_whitelist": "Gestão Manual", + "manage_whitelist_desc": "Adicionar ou remover jogadores manualmente", + "view_list": "Ver Lista", + "action": "Ação", + "add_to_whitelist": "Adicionar", + "remove_from_whitelist": "Remover", + "identifier_type": "Tipo", + "identifier_value": "Identificador", + "apply": "Aplicar", + "whitelist_rules": "Regras Automáticas", + "whitelist_rules_desc": "Ativa/desativa a whitelist automaticamente", + "add_admin": "Admin", + "add_players": "Jogadores", + "add_schedule": "Horário", + "admin_presence": "Presença Admin", + "player_count": "Contador Jogadores", + "scheduled_time": "Horário Programado", + "priority": "Prioridade", + "condition": "Condição", + "less_than": "Menor que (<)", + "greater_than": "Maior que (>)", + "greater_equal": "Maior ou igual (>=)", + "equal_to": "Igual a (==)", + "admin_count": "Quantidade de admins", + "player_count_label": "Quantidade de jogadores", + "enable_whitelist": "Ativar", + "disable_whitelist": "Desativar", + "start_time": "Hora início", + "end_time": "Hora fim", + "save_configuration": "Guardar", + "reset": "Reiniciar", + "whitelist_list": "Lista Whitelist", + "search_players": "Pesquisar jogadores...", + "total_entries": "Entradas totais", + "no_results": "Nenhum resultado encontrado", + "no_entries": "Sem entradas", + "view_identifiers": "Ver identificadores", + "hide_identifiers": "Ocultar identificadores", + "whitelisted": "Na whitelist", + "not_whitelisted": "Não na whitelist", + "checking_whitelist": "Verificando whitelist...", + "kick_message": "Você não está na whitelist", + "no_perms": "Você não tem permissões", + "usage_wl_add": "Uso: /wl_add [ID do jogador]", + "player_not_found": "Jogador não encontrado", + "player_already_wl": "O jogador já está na whitelist", + "player_added": "Jogador adicionado à whitelist", + "added_to_whitelist": "Você foi adicionado à whitelist", + "usage_wl_check": "Uso: /wl_check [ID do jogador]", + "player_has_wl": "%s está na whitelist", + "player_no_wl": "%s NÃO está na whitelist", + "player_is_admin": "Este jogador é administrador", + "config_saved": "Configuração guardada", + "webhook_invalid": "URL do webhook inválida", + "webhook_updated": "Webhook atualizado", + "webhook_test": "Teste de webhook - Solicitado por %s", + "discord_no_token": "Discord ativado mas sem token configurado", + "discord_missing_config": "Falta configurar Guild ID ou Role ID", + "whitelist_enabled_manual": "Whitelist ativada manualmente por %s", + "whitelist_disabled_manual": "Whitelist desativada manualmente por %s", + "whitelist_enabled_auto": "Whitelist ativada automaticamente pelas regras", + "whitelist_disabled_auto": "Whitelist desativada automaticamente pelas regras", + "discord_player_added": "✅ %s adicionou: %s", + "discord_player_removed": "❌ %s removeu: %s", + "invalid_identifier": "Identificador inválido", + "player_added_ui": "Jogador adicionado com sucesso", + "player_removed_ui": "Jogador removido com sucesso", + "player_not_in_wl": "O jogador não está na whitelist", + "discord_title": "🛡️ ESX Whitelist", + "webhook_sent": "Webhook de teste enviado", + "whitelist_active": "Whitelist Ativa", + "remaining_time": "Tempo restante: %d segundos", + "grace_cancelled": "Período de carência cancelado", + "no_permission": "Você não tem permissão para usar este comando", + "invalid_identifier_format": "Formato de identificador inválido", + "ip_not_allowed": "Identificadores IP não são permitidos", + "auto_detected": "Detectado automaticamente" +} \ No newline at end of file diff --git a/[core]/esx_whitelist/server/cfg_discord.lua b/[core]/esx_whitelist/server/cfg_discord.lua new file mode 100644 index 000000000..fc570c286 --- /dev/null +++ b/[core]/esx_whitelist/server/cfg_discord.lua @@ -0,0 +1,55 @@ +Config.DiscordBotToken = "YOUR_DISCORD_BOT_TOKEN_HERE" + +local function IsValidBotToken(token) + if not token or token == '' or #token < 50 or not string.match(token, '%.') then + return false + end + + local invalid = { + 'TU_TOKEN_AQUI', + 'YOUR_TOKEN_HERE', + 'YOUR_BOT_TOKEN', + 'DISCORD_BOT_TOKEN', + 'YOUR_DISCORD_BOT_TOKEN_HERE' + } + + for i = 1, #invalid do + if token == invalid[i] then + return false + end + end + + return true +end + +local function GetPlayerDiscord(src) + local discordId = GetPlayerIdentifierByType(src, 'discord') + + if not discordId then + return 'Unknown' + end + + discordId = discordId:gsub('discord:', '') + + local promise = promise.new() + local name = nil + + PerformHttpRequest("https://discordapp.com/api/users/" .. discordId, function(statusCode, data) + if statusCode == 200 then + local decoded = json.decode(data or "{}") + if decoded and decoded.global_name then + name = decoded.global_name + end + end + promise:resolve() + end, "GET", "", { + Authorization = "Bot " .. Config.DiscordBotToken + }) + + Citizen.Await(promise) + + return name or 'Unknown' +end + +_G.IsValidBotToken = IsValidBotToken +_G.GetPlayerDiscord = GetPlayerDiscord \ No newline at end of file diff --git a/[core]/esx_whitelist/server/commands.lua b/[core]/esx_whitelist/server/commands.lua new file mode 100644 index 000000000..71f02f222 --- /dev/null +++ b/[core]/esx_whitelist/server/commands.lua @@ -0,0 +1,288 @@ +---@diagnostic disable: undefined-global, need-check-nil, param-type-mismatch + +if Config.InGameCommands then + ESX.RegisterCommand('wl_add', 'admin', function(xPlayer, args) + if not args.id then + xPlayer.showNotification('~r~Usage: /wl_add [player id]') + return + end + + local targetId = tonumber(args.id) + local targetPlayer = ESX.Player(targetId) + + if not targetPlayer then + xPlayer.showNotification('~r~Player not found') + return + end + + local targetIdentifiers = GetPlayerIdentifiersFiltered(targetId) + + if #targetIdentifiers == 0 then + xPlayer.showNotification('~r~Player not found') + return + end + + local conditions, params = BuildIdentifierQuery(targetIdentifiers) + + MySQL.query('SELECT w.id, w.whitelisted FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE ' .. conditions, params, function(result) + if result and #result > 0 then + if result[1].whitelisted == 1 then + xPlayer.showNotification('~r~Player already whitelisted') + return + end + + local adminIdentifier = xPlayer.getIdentifier() + + MySQL.update('UPDATE whitelist SET whitelisted = 1, added_by = ? WHERE id = ?', { + adminIdentifier, result[1].id + }, function() + for i = 1, #targetIdentifiers do + Whitelist.Cache[targetIdentifiers[i]] = result[1].id + end + + xPlayer.showNotification('~g~Player added to whitelist') + targetPlayer.showNotification('~g~You have been added to the whitelist') + + if Whitelist.GracePeriodPlayers[targetId] then + Whitelist.GracePeriodPlayers[targetId] = nil + TriggerClientEvent('esx_whitelist:cancelGracePeriod', targetId) + end + end) + else + xPlayer.showNotification('~r~Player not found') + end + end) + end, false, {help = 'Add player to whitelist', validate = true, arguments = { + {name = 'id', help = 'Player ID', type = 'number'} + }}) + + ESX.RegisterCommand('wl_check', 'admin', function(xPlayer, args) + if not args.id then + xPlayer.showNotification('~r~Usage: /wl_check [player id]') + return + end + + local targetId = tonumber(args.id) + local targetPlayer = ESX.Player(targetId) + + if not targetPlayer then + xPlayer.showNotification('~r~Player not found') + return + end + + local targetIdentifiers = GetPlayerIdentifiersFiltered(targetId) + + if #targetIdentifiers == 0 then + xPlayer.showNotification('~r~Player not found') + return + end + + local conditions, params = BuildIdentifierQuery(targetIdentifiers) + + MySQL.query('SELECT w.whitelisted FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE ' .. conditions, params, function(result) + local hasWhitelist = result and #result > 0 and result[1].whitelisted == 1 + local targetName = targetPlayer.getName() + + if hasWhitelist then + xPlayer.showNotification('~g~' .. targetName .. ' is whitelisted') + else + xPlayer.showNotification('~r~' .. targetName .. ' is not whitelisted') + end + + if IsPlayerAdmin(targetId) then + xPlayer.showNotification('~b~This player is an admin') + end + end) + end, false, {help = 'Check whitelist status', validate = true, arguments = { + {name = 'id', help = 'Player ID', type = 'number'} + }}) +end + +if Config.ConsoleCommands then + RegisterCommand('wl_add', function(source, args) + if source ~= 0 then return end + + if not args[1] then + print('Usage: wl_add [identifier]') + return + end + + local rawIdentifier = args[1] + local identifierType, identifierValue = NormalizeIdentifier(rawIdentifier) + + if not identifierType or not identifierValue then + print('Invalid identifier format') + return + end + + if identifierType == 'ip' then + print('IP identifiers are not allowed') + return + end + + local fullIdentifier = identifierType .. ':' .. identifierValue + + GetAllPlayerIdentifiers(fullIdentifier, function(allIdentifiers, existingPlayerName, isOnline) + local playerName = existingPlayerName or 'Pending...' + + MySQL.query('SELECT w.id, w.whitelisted FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE wi.identifier = ?', {fullIdentifier}, function(result) + if result and #result > 0 then + if result[1].whitelisted == 1 then + print('Player is already whitelisted') + return + end + + MySQL.update('UPDATE whitelist SET whitelisted = 1, added_by = ?, player_name = ? WHERE id = ?', { + 'console', playerName, result[1].id + }, function() + local queries = {} + for i = 1, #allIdentifiers do + local idType = allIdentifiers[i]:match("^(%w+):") + if idType then + table.insert(queries, { + query = 'INSERT INTO whitelist_identifiers (whitelist_id, type, identifier) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE whitelist_id = VALUES(whitelist_id)', + values = {result[1].id, idType, allIdentifiers[i]} + }) + end + end + + if #queries > 0 then + MySQL.transaction(queries, function(success) + if success then + for i = 1, #allIdentifiers do + Whitelist.Cache[allIdentifiers[i]] = result[1].id + end + end + end) + end + + print('Added to whitelist: ' .. playerName .. ' (' .. identifierType .. ')' .. (isOnline and ' [ONLINE]' or '')) + + if isOnline then + NotifyOnlinePlayer(fullIdentifier) + end + end) + else + MySQL.insert('INSERT INTO whitelist (player_name, whitelisted, added_by) VALUES (?, ?, ?)', { + playerName, 1, 'console' + }, function(insertId) + if insertId then + local queries = {} + for i = 1, #allIdentifiers do + local idType = allIdentifiers[i]:match("^(%w+):") + if idType then + table.insert(queries, { + query = 'INSERT INTO whitelist_identifiers (whitelist_id, type, identifier) VALUES (?, ?, ?)', + values = {insertId, idType, allIdentifiers[i]} + }) + end + end + + if #queries > 0 then + MySQL.transaction(queries, function(success) + if success then + for i = 1, #allIdentifiers do + Whitelist.Cache[allIdentifiers[i]] = insertId + end + end + end) + end + + print('Added to whitelist: ' .. playerName .. ' (' .. identifierType .. ')' .. (isOnline and ' [ONLINE]' or '')) + + if isOnline then + NotifyOnlinePlayer(fullIdentifier) + end + end + end) + end + end) + end) + end, true) + + RegisterCommand('wl_on', function(source) + if source ~= 0 then return end + + if Whitelist.Enabled then + print('Whitelist is already enabled') + return + end + + Whitelist.ManualOverride = false + Whitelist.Enabled = true + SaveConfig() + + print('Whitelist has been enabled') + SendDiscordLog('Whitelist enabled from console', 15158332) + + Wait(2000) + KickNonWhitelistedPlayers() + + TriggerClientEvent('esx_whitelist:stateChanged', -1, Whitelist.Enabled) + end, true) + + RegisterCommand('wl_off', function(source) + if source ~= 0 then return end + + if not Whitelist.Enabled then + print('Whitelist is already disabled') + return + end + + Whitelist.ManualOverride = true + Whitelist.Enabled = false + SaveConfig() + + print('Whitelist has been disabled') + SendDiscordLog('Whitelist disabled from console', 3066993) + + for playerSource in pairs(Whitelist.GracePeriodPlayers) do + Whitelist.GracePeriodPlayers[playerSource] = nil + TriggerClientEvent('esx_whitelist:cancelGracePeriod', playerSource) + end + + TriggerClientEvent('esx_whitelist:stateChanged', -1, Whitelist.Enabled) + end, true) + + RegisterCommand('wl_remove', function(source, args) + if source ~= 0 then return end + + if not args[1] then + print('Usage: wl_remove [identifier]') + return + end + + local rawIdentifier = args[1] + local identifierType, identifierValue = NormalizeIdentifier(rawIdentifier) + + if not identifierType or not identifierValue then + print('Invalid identifier format') + return + end + + if identifierType == 'ip' then + print('IP identifiers are not allowed') + return + end + + local fullIdentifier = identifierType .. ':' .. identifierValue + + MySQL.query('SELECT w.id, w.whitelisted, w.player_name FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE wi.identifier = ?', {fullIdentifier}, function(result) + if not result or #result == 0 or result[1].whitelisted == 0 then + print('Player is not in whitelist') + return + end + + MySQL.update('UPDATE whitelist SET whitelisted = 0 WHERE id = ?', {result[1].id}, function() + MySQL.query('SELECT identifier FROM whitelist_identifiers WHERE whitelist_id = ?', {result[1].id}, function(identifiers) + for i = 1, #identifiers do + Whitelist.Cache[identifiers[i].identifier] = nil + end + + local playerName = result[1].player_name or 'Unknown' + print('Removed from whitelist: ' .. playerName .. ' (' .. identifierType .. ')') + end) + end) + end) + end, true) +end \ No newline at end of file diff --git a/[core]/esx_whitelist/server/main.lua b/[core]/esx_whitelist/server/main.lua new file mode 100644 index 000000000..2403c46b3 --- /dev/null +++ b/[core]/esx_whitelist/server/main.lua @@ -0,0 +1,975 @@ +Whitelist = { + Enabled = false, + GracePeriod = 60, + KickConnected = false, + DiscordWebhook = '', + DiscordEnabled = false, + DiscordGuildId = '', + DiscordRoleId = '', + DiscordBotToken = Config.DiscordBotToken or '', + Rules = {}, + GracePeriodPlayers = {}, + ManualOverride = false, + Cache = {} +} + +local ruleChangeDebounce = false +local configFile = 'whitelist_config.json' +local webhookCooldown = {} + +local DefaultConfig = { + whitelistEnabled = false, + gracePeriod = 60, + kickConnected = false, + discordWebhook = '', + discordEnabled = false, + discordGuildId = '', + discordRoleId = '', + rules = { + { + id = '1', + type = 'admin-presence', + enabled = false, + priority = 1, + operator = '<', + value = 1, + action = 'enable' + }, + { + id = '2', + type = 'player-count', + enabled = false, + priority = 2, + operator = '>', + value = 32, + action = 'enable' + }, + { + id = '3', + type = 'scheduled', + enabled = false, + priority = 3, + startTime = '03:00', + endTime = '08:00' + } + } +} + +local function IsPlayerAdmin(source) + if not source or source == 0 then return true end + + local xPlayer = ESX.Player(source) + if not xPlayer then return false end + + local group = xPlayer.getGroup() + for i = 1, #Config.AdminGroups do + if group == Config.AdminGroups[i] then + return true + end + end + + return false +end + +function GetPlayerIdentifiersFiltered(source) + local identifiers = {} + local license = GetPlayerIdentifierByType(source, 'license') + local license2 = GetPlayerIdentifierByType(source, 'license2') + local steam = GetPlayerIdentifierByType(source, 'steam') + local discord = GetPlayerIdentifierByType(source, 'discord') + local xbl = GetPlayerIdentifierByType(source, 'xbl') + local fivem = GetPlayerIdentifierByType(source, 'fivem') + + if license then table.insert(identifiers, 'license:' .. license) end + if license2 then table.insert(identifiers, 'license2:' .. license2) end + if steam then table.insert(identifiers, 'steam:' .. steam) end + if discord then table.insert(identifiers, 'discord:' .. discord) end + if xbl then table.insert(identifiers, 'xbl:' .. xbl) end + if fivem then table.insert(identifiers, 'fivem:' .. fivem) end + + return identifiers +end + +function BuildIdentifierQuery(identifiers) + local conditions, params = {}, {} + for i = 1, #identifiers do + table.insert(conditions, 'wi.identifier = ?') + table.insert(params, identifiers[i]) + end + return table.concat(conditions, ' OR '), params +end + +local function SendDiscordLog(message, color) + if not Whitelist.DiscordWebhook or Whitelist.DiscordWebhook == '' or Whitelist.DiscordWebhook == '***CONFIGURED***' then return end + if not message or message == '' then return end + + local currentTime = GetGameTimer() + if webhookCooldown[message] and currentTime - webhookCooldown[message] < 5000 then return end + webhookCooldown[message] = currentTime + + PerformHttpRequest(Whitelist.DiscordWebhook, function() end, 'POST', json.encode({ + embeds = {{ + title = _U('discord_title'), + description = message, + color = color or 3447003, + timestamp = os.date('!%Y-%m-%dT%H:%M:%S') + }} + }), {['Content-Type'] = 'application/json'}) +end + +local function UpdateWhitelistCache() + MySQL.query('SELECT DISTINCT w.id, wi.identifier FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE CAST(w.whitelisted AS UNSIGNED) = 1', {}, function(results) + Whitelist.Cache = {} + for i = 1, #results do + Whitelist.Cache[results[i].identifier] = results[i].id + end + end) +end + +local function StartGracePeriod(source, playerName) + if not source or source == 0 then return end + if Whitelist.GracePeriod <= 0 then + DropPlayer(source, _U('kick_message')) + return + end + + Whitelist.GracePeriodPlayers[source] = { + endTime = os.time() + Whitelist.GracePeriod, + playerName = playerName + } + + TriggerClientEvent('esx_whitelist:startGracePeriod', source, Whitelist.GracePeriod) + + CreateThread(function() + Wait(Whitelist.GracePeriod * 1000) + if Whitelist.GracePeriodPlayers[source] then + if IsPlayerAdmin(source) then + Whitelist.GracePeriodPlayers[source] = nil + return + end + DropPlayer(source, _U('kick_message')) + Whitelist.GracePeriodPlayers[source] = nil + end + end) +end + +function KickNonWhitelistedPlayers() + local players = ESX.GetExtendedPlayers() + for i = 1, #players do + local xPlayer = players[i] + if xPlayer and xPlayer.source and not IsPlayerAdmin(xPlayer.source) then + local hasWhitelist = false + local identifiers = GetPlayerIdentifiersFiltered(xPlayer.source) + + for j = 1, #identifiers do + if Whitelist.Cache[identifiers[j]] then + hasWhitelist = true + break + end + end + + if not hasWhitelist then + if Whitelist.KickConnected then + DropPlayer(xPlayer.source, _U('kick_message')) + else + StartGracePeriod(xPlayer.source, xPlayer.getName()) + end + end + end + end +end + +function SaveConfig() + local data = { + whitelistEnabled = Whitelist.Enabled, + gracePeriod = Whitelist.GracePeriod, + kickConnected = Whitelist.KickConnected, + discordWebhook = Whitelist.DiscordWebhook, + discordEnabled = Whitelist.DiscordEnabled, + discordGuildId = Whitelist.DiscordGuildId, + discordRoleId = Whitelist.DiscordRoleId, + rules = Whitelist.Rules + } + SaveResourceFile(GetCurrentResourceName(), configFile, json.encode(data, {indent = true}), -1) +end + +local function LoadConfig() + local configData = LoadResourceFile(GetCurrentResourceName(), configFile) + + if not configData then + Whitelist.Enabled = DefaultConfig.whitelistEnabled + Whitelist.GracePeriod = DefaultConfig.gracePeriod + Whitelist.KickConnected = DefaultConfig.kickConnected + Whitelist.DiscordWebhook = DefaultConfig.discordWebhook + Whitelist.DiscordEnabled = DefaultConfig.discordEnabled + Whitelist.DiscordGuildId = DefaultConfig.discordGuildId + Whitelist.DiscordRoleId = DefaultConfig.discordRoleId + Whitelist.Rules = DefaultConfig.rules + SaveConfig() + return true + end + + local success, data = pcall(json.decode, configData) + + if not success or not data then + if Config.Debug then + print('Whitelist config file corrupted, regenerating with defaults') + end + Whitelist.Enabled = DefaultConfig.whitelistEnabled + Whitelist.GracePeriod = DefaultConfig.gracePeriod + Whitelist.KickConnected = DefaultConfig.kickConnected + Whitelist.DiscordWebhook = DefaultConfig.discordWebhook + Whitelist.DiscordEnabled = DefaultConfig.discordEnabled + Whitelist.DiscordGuildId = DefaultConfig.discordGuildId + Whitelist.DiscordRoleId = DefaultConfig.discordRoleId + Whitelist.Rules = DefaultConfig.rules + SaveConfig() + return true + end + + Whitelist.Enabled = data.whitelistEnabled or DefaultConfig.whitelistEnabled + Whitelist.GracePeriod = data.gracePeriod or DefaultConfig.gracePeriod + Whitelist.KickConnected = data.kickConnected or DefaultConfig.kickConnected + Whitelist.DiscordWebhook = data.discordWebhook or DefaultConfig.discordWebhook + Whitelist.DiscordEnabled = data.discordEnabled or DefaultConfig.discordEnabled + Whitelist.DiscordGuildId = data.discordGuildId or DefaultConfig.discordGuildId + Whitelist.DiscordRoleId = data.discordRoleId or DefaultConfig.discordRoleId + Whitelist.Rules = data.rules or DefaultConfig.rules + + return true +end + +local function evaluateWhitelistRules() + local activeRules = {} + for i = 1, #Whitelist.Rules do + if Whitelist.Rules[i].enabled then + table.insert(activeRules, Whitelist.Rules[i]) + end + end + + table.sort(activeRules, function(a, b) return a.priority < b.priority end) + + for i = 1, #activeRules do + local rule = activeRules[i] + + if rule.type == 'admin-presence' then + local adminCount = 0 + local players = ESX.GetExtendedPlayers() + for j = 1, #players do + if players[j] and players[j].source and IsPlayerAdmin(players[j].source) then + adminCount = adminCount + 1 + end + end + + local conditionMet = false + if rule.operator == '<' then conditionMet = adminCount < rule.value + elseif rule.operator == '>' then conditionMet = adminCount > rule.value + elseif rule.operator == '>=' then conditionMet = adminCount >= rule.value + elseif rule.operator == '==' then conditionMet = adminCount == rule.value + end + + if conditionMet then return rule.action == 'enable' end + + elseif rule.type == 'player-count' then + local playerCount = #ESX.GetExtendedPlayers() + + local conditionMet = false + if rule.operator == '<' then conditionMet = playerCount < rule.value + elseif rule.operator == '>' then conditionMet = playerCount > rule.value + elseif rule.operator == '>=' then conditionMet = playerCount >= rule.value + elseif rule.operator == '==' then conditionMet = playerCount == rule.value + end + + if conditionMet then return rule.action == 'enable' end + + elseif rule.type == 'scheduled' then + local currentMinutes = tonumber(os.date('%H')) * 60 + tonumber(os.date('%M')) + local startHour, startMin = rule.startTime:match('(%d+):(%d+)') + local startMinutes = tonumber(startHour) * 60 + tonumber(startMin) + local endHour, endMin = rule.endTime:match('(%d+):(%d+)') + local endMinutes = tonumber(endHour) * 60 + tonumber(endMin) + + local inRange + if startMinutes <= endMinutes then + inRange = currentMinutes >= startMinutes and currentMinutes <= endMinutes + else + inRange = currentMinutes >= startMinutes or currentMinutes <= endMinutes + end + + return inRange + end + end + + return Whitelist.Enabled +end + +local function handleRuleChange() + if Whitelist.ManualOverride or ruleChangeDebounce then return end + ruleChangeDebounce = true + + local newState = evaluateWhitelistRules() + if newState ~= Whitelist.Enabled then + Whitelist.Enabled = newState + + if Whitelist.Enabled then + SendDiscordLog(_U('whitelist_enabled_auto'), 15158332) + KickNonWhitelistedPlayers() + else + SendDiscordLog(_U('whitelist_disabled_auto'), 3066993) + for playerSource in pairs(Whitelist.GracePeriodPlayers) do + if playerSource and playerSource ~= 0 then + Whitelist.GracePeriodPlayers[playerSource] = nil + TriggerClientEvent('esx_whitelist:cancelGracePeriod', playerSource) + end + end + end + + TriggerClientEvent('esx_whitelist:stateChanged', -1, Whitelist.Enabled) + SaveConfig() + end + + SetTimeout(5000, function() + ruleChangeDebounce = false + end) +end + +local function checkDiscordRole(discordId, callback) + if not Whitelist.DiscordEnabled or Whitelist.DiscordGuildId == '' or Whitelist.DiscordRoleId == '' or not IsValidBotToken(Whitelist.DiscordBotToken) then + callback(false) + return + end + + PerformHttpRequest(string.format('https://discord.com/api/v10/guilds/%s/members/%s', Whitelist.DiscordGuildId, discordId), + function(statusCode, data) + if statusCode == 200 then + local member = json.decode(data) + if member and member.roles then + for i = 1, #member.roles do + if member.roles[i] == Whitelist.DiscordRoleId then + callback(true) + return + end + end + end + end + callback(false) + end, 'GET', '', { + ['Authorization'] = 'Bot ' .. Whitelist.DiscordBotToken, + ['Content-Type'] = 'application/json' + }) +end + +local function DetectIdentifierType(value) + if not value or value == '' then return nil end + + value = value:gsub("%s+", "") + value = value:gsub("^%w+:", "") + + local len = #value + + if value:find("^[0-9a-fA-F]+%-[0-9a-fA-F]+%-[0-9a-fA-F]+%-[0-9a-fA-F]+%-[0-9a-fA-F]+$") and len == 36 then + return "license2" + end + + if value:find("^[0-9a-fA-F]+$") and len == 40 then + return "license" + end + + if value:find("^%d+$") then + if len >= 17 and len <= 19 then + return "discord" + elseif len == 16 then + return "xbl" + elseif len >= 6 and len <= 8 then + return "fivem" + end + end + + if value:find("^[0-9a-fA-F]+$") and len >= 15 and len <= 17 then + return "steam" + end + + return nil +end + +function NormalizeIdentifier(rawValue) + if not rawValue or rawValue == '' then return nil, nil end + + local cleanValue = rawValue:gsub("%s+", "") + local providedPrefix, providedValue = cleanValue:match("^(%w+):(.+)$") + local valueToDetect = providedValue or cleanValue + local detectedType = DetectIdentifierType(valueToDetect) + + if not detectedType then return nil, nil end + + return detectedType, valueToDetect +end + +function GetAllPlayerIdentifiers(singleIdentifier, callback) + local idType, idValue = NormalizeIdentifier(singleIdentifier) + + if not idType or not idValue then + callback({}, nil, false) + return + end + + local fullIdentifier = idType .. ':' .. idValue + + local players = ESX.GetExtendedPlayers() + for i = 1, #players do + if players[i] and players[i].source then + local identifiers = GetPlayerIdentifiersFiltered(players[i].source) + for j = 1, #identifiers do + if identifiers[j] == fullIdentifier then + local playerName = GetPlayerName(players[i].source) or players[i].getName() + callback(identifiers, playerName, true) + return + end + end + end + end + + MySQL.query([[ + SELECT wi.identifier, w.player_name + FROM whitelist_identifiers wi + JOIN whitelist w ON w.id = wi.whitelist_id + WHERE wi.whitelist_id = ( + SELECT whitelist_id FROM whitelist_identifiers WHERE identifier = ? LIMIT 1 + ) + ]], {fullIdentifier}, function(results) + if results and #results > 0 then + local allIdentifiers = {} + for i = 1, #results do + table.insert(allIdentifiers, results[i].identifier) + end + callback(allIdentifiers, results[1].player_name, false) + else + callback({fullIdentifier}, nil, false) + end + end) +end + +function NotifyOnlinePlayer(fullIdentifier) + local players = ESX.GetExtendedPlayers() + for i = 1, #players do + if players[i] and players[i].source then + local onlineIdentifiers = GetPlayerIdentifiersFiltered(players[i].source) + for j = 1, #onlineIdentifiers do + if onlineIdentifiers[j] == fullIdentifier then + players[i].showNotification('~g~' .. _U('added_to_whitelist')) + if Whitelist.GracePeriodPlayers[players[i].source] then + Whitelist.GracePeriodPlayers[players[i].source] = nil + TriggerClientEvent('esx_whitelist:cancelGracePeriod', players[i].source) + end + return + end + end + end + end +end + +AddEventHandler('playerConnecting', function(playerName, setKickReason, deferrals) + local source = source + if not source or source == 0 then return end + + deferrals.defer() + Wait(0) + + deferrals.update(_U('checking_whitelist')) + + local realPlayerName = GetPlayerName(source) or playerName + local identifiers = GetPlayerIdentifiersFiltered(source) + + if #identifiers == 0 then + deferrals.done(_U('kick_message')) + return + end + + Wait(500) + + local conditions, params = BuildIdentifierQuery(identifiers) + local query = 'SELECT DISTINCT w.id, w.player_name, CAST(w.whitelisted AS UNSIGNED) as whitelisted FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE ' .. conditions + + MySQL.query(query, params, function(result) + local whitelistId + local existsInDB = result and #result > 0 + + if existsInDB then + whitelistId = result[1].id + + local queries = {} + for i = 1, #identifiers do + local idType = identifiers[i]:match("^(%w+):") + if idType then + table.insert(queries, { + query = 'INSERT INTO whitelist_identifiers (whitelist_id, type, identifier) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE whitelist_id = VALUES(whitelist_id)', + values = {whitelistId, idType, identifiers[i]} + }) + end + end + + if #queries > 0 then + MySQL.transaction(queries) + end + + MySQL.update('UPDATE whitelist SET player_name = ? WHERE id = ?', {realPlayerName, whitelistId}) + else + MySQL.insert('INSERT INTO whitelist (player_name, whitelisted) VALUES (?, ?)', {realPlayerName, 0}, function(insertId) + whitelistId = insertId + if whitelistId then + local queries = {} + for i = 1, #identifiers do + local idType = identifiers[i]:match("^(%w+):") + if idType then + table.insert(queries, { + query = 'INSERT INTO whitelist_identifiers (whitelist_id, type, identifier) VALUES (?, ?, ?)', + values = {whitelistId, idType, identifiers[i]} + }) + end + end + + if #queries > 0 then + MySQL.transaction(queries) + end + end + end) + Wait(500) + end + + if not Whitelist.Enabled then + deferrals.done() + return + end + + if Whitelist.DiscordEnabled then + local discordId = GetPlayerIdentifierByType(source, 'discord') + + if not discordId then + deferrals.done(_U('kick_message')) + return + end + + discordId = discordId:gsub('discord:', '') + + checkDiscordRole(discordId, function(hasRole) + if hasRole then + MySQL.update('UPDATE whitelist SET whitelisted = 1 WHERE id = ?', {whitelistId}, function() + for i = 1, #identifiers do + Whitelist.Cache[identifiers[i]] = whitelistId + end + end) + deferrals.done() + else + MySQL.update('UPDATE whitelist SET whitelisted = 0 WHERE id = ?', {whitelistId}, function() + for i = 1, #identifiers do + Whitelist.Cache[identifiers[i]] = nil + end + end) + deferrals.done(_U('kick_message')) + end + end) + else + if existsInDB and tonumber(result[1].whitelisted) == 1 then + for i = 1, #identifiers do + Whitelist.Cache[identifiers[i]] = whitelistId + end + deferrals.done() + else + deferrals.done(_U('kick_message')) + end + end + end) +end) + +AddEventHandler('esx:playerLoaded', function(playerId, xPlayer, isNew) + handleRuleChange() +end) + +AddEventHandler('esx:playerDropped', function(playerId, reason) + if Whitelist.GracePeriodPlayers[playerId] then + Whitelist.GracePeriodPlayers[playerId] = nil + end + Wait(1000) + handleRuleChange() +end) + +AddEventHandler('onResourceStart', function(resourceName) + if GetCurrentResourceName() ~= resourceName then return end + + LoadConfig() + + MySQL.query([[ + CREATE TABLE IF NOT EXISTS whitelist ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_name VARCHAR(255) COLLATE utf8mb4_unicode_ci, + whitelisted TINYINT(1) NOT NULL DEFAULT 0, + added_by VARCHAR(255) COLLATE utf8mb4_unicode_ci, + added_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_whitelisted (whitelisted) + ) + ]]) + + MySQL.query([[ + CREATE TABLE IF NOT EXISTS whitelist_identifiers ( + id INT AUTO_INCREMENT PRIMARY KEY, + whitelist_id INT NOT NULL, + type ENUM('steam', 'license', 'license2', 'discord', 'xbl', 'fivem', 'ip') NOT NULL, + identifier VARCHAR(255) NOT NULL COLLATE utf8mb4_bin, + FOREIGN KEY (whitelist_id) REFERENCES whitelist(id) ON DELETE CASCADE, + UNIQUE (type, identifier), + INDEX idx_identifier (identifier) + ) + ]]) + + Wait(1000) + UpdateWhitelistCache() + + local status = Whitelist.Enabled and 'enabled' or 'disabled' + print('Whitelist system started - Status: ' .. status .. ' - Grace period: ' .. Whitelist.GracePeriod .. 's') + + if Whitelist.DiscordEnabled and IsValidBotToken(Whitelist.DiscordBotToken) then + print('Discord role verification is active') + end + + CreateThread(function() + while true do + Wait(30000) + handleRuleChange() + end + end) + + if Whitelist.Enabled then + Wait(5000) + KickNonWhitelistedPlayers() + end +end) + +ESX.RegisterServerCallback('esx_whitelist:getConfig', function(source, cb) + if not source or source == 0 or not IsPlayerAdmin(source) then + cb(nil) + return + end + + local file = LoadResourceFile(GetCurrentResourceName(), 'locales/' .. Config.Locale .. '.json') + local localeTranslations = file and json.decode(file) or {} + + cb({ + whitelistEnabled = Whitelist.Enabled, + gracePeriod = Whitelist.GracePeriod, + kickConnected = Whitelist.KickConnected, + discordWebhook = Whitelist.DiscordWebhook ~= '' and '***CONFIGURED***' or '', + discordEnabled = Whitelist.DiscordEnabled, + discordGuildId = Whitelist.DiscordGuildId, + discordRoleId = Whitelist.DiscordRoleId, + rules = Whitelist.Rules, + locale = Config.Locale, + translations = localeTranslations, + hasBotToken = Whitelist.DiscordEnabled and IsValidBotToken(Whitelist.DiscordBotToken) + }) +end) + +ESX.RegisterServerCallback('esx_whitelist:getWhitelistEntries', function(source, cb) + if not source or source == 0 or not IsPlayerAdmin(source) then + cb({}) + return + end + + MySQL.query([[ + SELECT w.id, w.player_name, CAST(w.whitelisted AS UNSIGNED) as whitelisted, + GROUP_CONCAT(wi.identifier SEPARATOR '|') as identifiers + FROM whitelist w + LEFT JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id + GROUP BY w.id + ORDER BY w.whitelisted DESC, w.id ASC + ]], {}, function(results) + if not results then + cb({}) + return + end + + local entries = {} + for i = 1, #results do + local row = results[i] + local identifiers = {} + if row.identifiers then + for identifier in string.gmatch(row.identifiers, '[^|]+') do + table.insert(identifiers, identifier) + end + end + + table.insert(entries, { + id = row.id, + identifiers = identifiers, + playerName = row.player_name or 'Unknown', + whitelisted = row.whitelisted + }) + end + + cb(entries) + end) +end) + +ESX.RegisterServerCallback('esx_whitelist:updateConfig', function(source, cb, config) + local playerSource = source + if not playerSource or playerSource == 0 or not IsPlayerAdmin(playerSource) then + cb(false) + return + end + + if config.discordEnabled and not IsValidBotToken(Whitelist.DiscordBotToken) then + TriggerClientEvent('esx:showNotification', playerSource, '~r~Discord bot token not configured') + cb(false) + return + end + + if config.discordEnabled and (not config.discordGuildId or config.discordGuildId == '' or not config.discordRoleId or config.discordRoleId == '') then + TriggerClientEvent('esx:showNotification', playerSource, '~r~Discord configuration incomplete') + cb(false) + return + end + + local oldState = Whitelist.Enabled + local xPlayer = ESX.Player(playerSource) + local playerName = xPlayer and xPlayer.getName() or 'Unknown' + + if config.discordWebhook and config.discordWebhook ~= '' and config.discordWebhook ~= '***CONFIGURED***' then + if string.match(config.discordWebhook, '^https://discord.com/api/webhooks/') or + string.match(config.discordWebhook, '^https://discordapp.com/api/webhooks/') then + Whitelist.DiscordWebhook = config.discordWebhook + end + end + + Whitelist.Enabled = config.whitelistEnabled + Whitelist.GracePeriod = config.gracePeriod + Whitelist.KickConnected = config.kickConnected + Whitelist.DiscordEnabled = config.discordEnabled + Whitelist.DiscordGuildId = config.discordGuildId + Whitelist.DiscordRoleId = config.discordRoleId + Whitelist.Rules = config.rules + Whitelist.ManualOverride = false + + SaveConfig() + + if Whitelist.Enabled and not oldState then + SendDiscordLog(string.format('Whitelist enabled by %s', playerName), 15158332) + Wait(2000) + KickNonWhitelistedPlayers() + elseif not Whitelist.Enabled and oldState then + SendDiscordLog(string.format('Whitelist disabled by %s', playerName), 3066993) + for src in pairs(Whitelist.GracePeriodPlayers) do + if src and src ~= 0 then + Whitelist.GracePeriodPlayers[src] = nil + TriggerClientEvent('esx_whitelist:cancelGracePeriod', src) + end + end + end + + TriggerClientEvent('esx_whitelist:stateChanged', -1, Whitelist.Enabled) + TriggerClientEvent('esx:showNotification', playerSource, '~g~Configuration saved') + cb(true) +end) + +ESX.RegisterServerCallback('esx_whitelist:testWebhook', function(source, cb) + local playerSource = source + if not playerSource or playerSource == 0 or not IsPlayerAdmin(playerSource) then + cb(false) + return + end + + local xPlayer = ESX.Player(playerSource) + local playerName = xPlayer and xPlayer.getName() or 'Unknown' + SendDiscordLog(string.format('Webhook test by %s', playerName), 3447003) + cb(true) +end) + +ESX.RegisterServerCallback('esx_whitelist:managePlayer', function(source, cb, data) + local playerSource = source + if not playerSource or playerSource == 0 then + cb(false, 'No permission') + return + end + + if not IsPlayerAdmin(playerSource) then + cb(false, 'No permission') + return + end + + local xPlayer = ESX.Player(playerSource) + if not xPlayer then + cb(false, 'Error getting player data') + return + end + + local adminIdentifier = xPlayer.getIdentifier() + if not adminIdentifier then + cb(false, 'Error getting admin identifier') + return + end + + local rawIdentifier = data.identifier + local action = data.action + + if not rawIdentifier or rawIdentifier == '' then + cb(false, 'Invalid identifier') + return + end + + local identifierType, identifierValue = NormalizeIdentifier(rawIdentifier) + + if not identifierType or not identifierValue then + cb(false, 'Invalid identifier format') + return + end + + if identifierType == 'ip' then + cb(false, 'IP identifiers not allowed') + return + end + + local fullIdentifier = identifierType .. ':' .. identifierValue + + if action == 'add' then + GetAllPlayerIdentifiers(fullIdentifier, function(allIdentifiers, existingPlayerName, isOnline) + local playerName = existingPlayerName or 'Pending...' + + MySQL.query('SELECT w.id, w.whitelisted FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE wi.identifier = ?', {fullIdentifier}, function(result) + if result and #result > 0 then + if result[1].whitelisted == 1 then + cb(false, 'Player already whitelisted') + return + end + + MySQL.update('UPDATE whitelist SET whitelisted = 1, added_by = ?, player_name = ? WHERE id = ?', { + adminIdentifier, playerName, result[1].id + }, function(affectedRows) + local queries = {} + for i = 1, #allIdentifiers do + local idType = allIdentifiers[i]:match("^(%w+):") + if idType then + table.insert(queries, { + query = 'INSERT INTO whitelist_identifiers (whitelist_id, type, identifier) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE whitelist_id = VALUES(whitelist_id)', + values = {result[1].id, idType, allIdentifiers[i]} + }) + end + end + + if #queries > 0 then + MySQL.transaction(queries, function(success) + if success then + for i = 1, #allIdentifiers do + Whitelist.Cache[allIdentifiers[i]] = result[1].id + end + end + end) + end + + SendDiscordLog(string.format('%s added %s to whitelist', xPlayer.getName(), playerName .. ' (' .. identifierType .. ')'), 3066993) + + if isOnline then + NotifyOnlinePlayer(fullIdentifier) + end + + cb(true, 'Player added successfully') + end) + else + MySQL.insert('INSERT INTO whitelist (player_name, whitelisted, added_by) VALUES (?, ?, ?)', { + playerName, 1, adminIdentifier + }, function(insertId) + if insertId then + local queries = {} + for i = 1, #allIdentifiers do + local idType = allIdentifiers[i]:match("^(%w+):") + if idType then + table.insert(queries, { + query = 'INSERT INTO whitelist_identifiers (whitelist_id, type, identifier) VALUES (?, ?, ?)', + values = {insertId, idType, allIdentifiers[i]} + }) + end + end + + if #queries > 0 then + MySQL.transaction(queries, function(success) + if success then + for i = 1, #allIdentifiers do + Whitelist.Cache[allIdentifiers[i]] = insertId + end + end + end) + end + + SendDiscordLog(string.format('%s added %s to whitelist', xPlayer.getName(), playerName .. ' (' .. identifierType .. ')'), 3066993) + + if isOnline then + NotifyOnlinePlayer(fullIdentifier) + end + + cb(true, 'Player added successfully') + else + cb(false, 'Error inserting into database') + end + end) + end + end) + end) + + elseif action == 'remove' then + MySQL.query('SELECT w.id, w.whitelisted FROM whitelist w JOIN whitelist_identifiers wi ON w.id = wi.whitelist_id WHERE wi.identifier = ?', {fullIdentifier}, function(result) + if not result or #result == 0 or result[1].whitelisted == 0 then + cb(false, 'Player not in whitelist') + return + end + + MySQL.update('UPDATE whitelist SET whitelisted = 0 WHERE id = ?', {result[1].id}, function(affectedRows) + MySQL.query('SELECT identifier FROM whitelist_identifiers WHERE whitelist_id = ?', {result[1].id}, function(identifiers) + for i = 1, #identifiers do + Whitelist.Cache[identifiers[i].identifier] = nil + end + + SendDiscordLog(string.format('%s removed %s from whitelist', xPlayer.getName(), fullIdentifier), 15158332) + cb(true, 'Player removed successfully') + end) + end) + end) + else + cb(false, 'Invalid action') + end +end) + + +ESX.RegisterServerCallback('esx_whitelist:toggleWhitelistStatus', function(source, cb, data) + local playerSource = source + if not playerSource or playerSource == 0 or not IsPlayerAdmin(playerSource) then + cb(false) + return + end + + local xPlayer = ESX.Player(playerSource) + local playerName = xPlayer and xPlayer.getName() or 'Unknown' + + MySQL.update('UPDATE whitelist SET whitelisted = ? WHERE id = ?', {data.status, data.id}, function(affectedRows) + MySQL.query('SELECT identifier FROM whitelist_identifiers WHERE whitelist_id = ?', {data.id}, function(result) + if result and #result > 0 then + for i = 1, #result do + if data.status == 1 then + Whitelist.Cache[result[i].identifier] = data.id + else + Whitelist.Cache[result[i].identifier] = nil + end + end + end + + local action = data.status == 1 and 'added player to whitelist' or 'removed player from whitelist' + SendDiscordLog(string.format('%s %s (ID: %s)', playerName, action, data.id), data.status == 1 and 3066993 or 15158332) + cb(true) + end) + end) +end) + +ESX.RegisterServerCallback('esx_whitelist:detectIdentifier', function(source, cb, rawValue) + local idType, idValue = NormalizeIdentifier(rawValue) + cb({ + type = idType, + value = idValue, + valid = idType ~= nil and idValue ~= nil + }) +end) \ No newline at end of file diff --git a/[core]/esx_whitelist/web/app/globals.css b/[core]/esx_whitelist/web/app/globals.css new file mode 100644 index 000000000..a7164816f --- /dev/null +++ b/[core]/esx_whitelist/web/app/globals.css @@ -0,0 +1,169 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"); + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: transparent; + --foreground: #ffffff; + + --card: #18181b; + --card-foreground: #ffffff; + --popover: #18181b; + --popover-foreground: #ffffff; + --border: #27272a; + --input: #27272a; + --ring: #3b82f6; + + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #27272a; + --secondary-foreground: #ffffff; + --muted: #27272a; + --muted-foreground: #a1a1aa; + --accent: #27272a; + --accent-foreground: #ffffff; + --destructive: #ef4444; + --destructive-foreground: #ffffff; + + --chart-1: #3b82f6; + --chart-2: #8b5cf6; + --chart-3: #ec4899; + --chart-4: #f59e0b; + --chart-5: #10b981; + + --sidebar: #18181b; + --sidebar-foreground: #ffffff; + --sidebar-primary: #3b82f6; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #27272a; + --sidebar-accent-foreground: #ffffff; + --sidebar-border: #27272a; + --sidebar-ring: #3b82f6; + + --font-sans: "Poppins", sans-serif; + --font-mono: "Geist Mono", "Geist Mono Fallback"; + --font-serif: "Source Serif 4", "Source Serif 4 Fallback"; + + --radius: 0.5rem; + + --shadow-x: 0px; + --shadow-y: 1px; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-opacity: 0.05; + --shadow-color: #000000; +} + +@theme inline { + --font-sans: "Poppins", sans-serif; + --font-mono: "Geist Mono", "Geist Mono Fallback"; + --font-serif: "Source Serif 4", "Source Serif 4 Fallback"; + + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-x) var(--shadow-y) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent); + --shadow-xs: var(--shadow-x) var(--shadow-y) var(--shadow-blur) var(--shadow-spread) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent); + --shadow-sm: var(--shadow-x) var(--shadow-y) calc(var(--shadow-blur) * 1.5) var(--shadow-spread) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent); + --shadow-md: calc(var(--shadow-x) * 2) calc(var(--shadow-y) * 2) calc(var(--shadow-blur) * 3) + calc(var(--shadow-spread) - 1px) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent); + --shadow-lg: calc(var(--shadow-x) * 4) calc(var(--shadow-y) * 4) calc(var(--shadow-blur) * 5) + calc(var(--shadow-spread) - 3px) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent); + --shadow-xl: calc(var(--shadow-x) * 6) calc(var(--shadow-y) * 6) calc(var(--shadow-blur) * 7) + calc(var(--shadow-spread) - 5px) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 100%), transparent); + --shadow-2xl: calc(var(--shadow-x) * 8) calc(var(--shadow-y) * 8) calc(max(0px, var(--shadow-blur) * 9 - 8px)) + calc(var(--shadow-spread) - 12px) + color-mix(in srgb, var(--shadow-color) calc(var(--shadow-opacity) * 250%), transparent); + --shadow: var(--shadow-md); + + --color-shadow-color: var(--shadow-color); + --shadow-opacity: var(--shadow-opacity); + --shadow-spread: var(--shadow-spread); + --shadow-blur: var(--shadow-blur); + --shadow-y: var(--shadow-y); + --shadow-x: var(--shadow-x); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + + .tablet-container::-webkit-scrollbar { + display: none; + } + + @keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } + } + + button, + [role="switch"], + [role="combobox"] { + cursor: pointer; + } + + input[type="number"], + input[type="time"], + input[type="url"], + input[type="text"] { + cursor: text; + } +} diff --git a/[core]/esx_whitelist/web/app/layout.tsx b/[core]/esx_whitelist/web/app/layout.tsx new file mode 100644 index 000000000..67940fa8d --- /dev/null +++ b/[core]/esx_whitelist/web/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next' +import './globals.css' + +export const metadata: Metadata = { + title: 'esx_whitelist', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + ) +} \ No newline at end of file diff --git a/[core]/esx_whitelist/web/app/page.tsx b/[core]/esx_whitelist/web/app/page.tsx new file mode 100644 index 000000000..88261f7cf --- /dev/null +++ b/[core]/esx_whitelist/web/app/page.tsx @@ -0,0 +1,1552 @@ +"use client" + +import { useState, useEffect, useRef } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Clock, Users, Bell, Settings, AlertTriangle, UserCheck, Hash, List, X, User, UserPlus, UserMinus, Search, CheckCircle2, Copy, Check, Eye, EyeOff } from "lucide-react" + +type RuleType = "admin-presence" | "player-count" | "scheduled" +type Operator = "<" | ">" | ">=" | "==" +type Action = "enable" | "disable" +type IdentifierType = "license" | "license2" | "steam" | "discord" | "xbl" | "fivem" +type WhitelistAction = "add" | "remove" + +interface Rule { + id: string + type: RuleType + enabled: boolean + priority: number + operator?: Operator + value?: number + action?: Action + startTime?: string + endTime?: string +} + +interface WhitelistEntry { + id: number + identifiers: string[] + playerName: string + whitelisted: number +} + +interface WhitelistConfig { + whitelistEnabled: boolean + gracePeriod: number + kickConnected: boolean + discordWebhook: string + discordEnabled: boolean + discordGuildId: string + discordRoleId: string + rules: Rule[] + locale: string + translations: Record + hasBotToken: boolean +} + +interface CustomSwitchProps { + checked: boolean + onCheckedChange: (checked: boolean) => void + disabled?: boolean + id?: string +} + +const CustomSwitch = ({ checked, onCheckedChange, disabled = false, id }: CustomSwitchProps) => { + return ( + + ) +} + +const useAudioSystem = () => { + const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({}) + + useEffect(() => { + audioRefs.current = { + click: new Audio("https://hebbkx1anhila5yf.public.blob.vercel-storage.com/buttonpress-ZhD2rrsS446feqLbA4Gk4ypddIry6X.wav"), + toggleOn: new Audio("https://hebbkx1anhila5yf.public.blob.vercel-storage.com/turnon-fSgd3bC1WSTe73HcQxjrKTc9muEW19.wav"), + toggleOff: new Audio("https://hebbkx1anhila5yf.public.blob.vercel-storage.com/turnoff-iDZ9CFS8GzYfuOVWECqbJbwoAQYNfD.wav"), + close: new Audio("https://r2.fivemanage.com/ezG2KbZP8dV8OUQgkQ0i6/close.mp3"), + } + + Object.values(audioRefs.current).forEach((audio) => { + audio.load() + }) + }, []) + + const playSound = (type: "click" | "toggleOn" | "toggleOff" | "close") => { + const audio = audioRefs.current[type] + if (audio) { + audio.currentTime = 0 + audio.play().catch(() => {}) + } + } + + return { playSound } +} + +const rateLimitMap = new Map() + +async function fetchNui(eventName: string, data?: any) { + const now = Date.now() + const lastCall = rateLimitMap.get(eventName) || 0 + + if (now - lastCall < 1000) { + return {} + } + + rateLimitMap.set(eventName, now) + + const options = { + method: 'post', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify(data), + } + + const resourceName = (window as any).GetParentResourceName + ? (window as any).GetParentResourceName() + : 'esx_whitelist' + + try { + const resp = await fetch(`https://${resourceName}/${eventName}`, options) + const text = await resp.text() + return text ? JSON.parse(text) : {} + } catch (error) { + return {} + } +} + +export default function WhitelistControlPanel() { + const [visible, setVisible] = useState(false) + const [whitelistEnabled, setWhitelistEnabled] = useState(false) + const [gracePeriod, setGracePeriod] = useState(60) + const [kickConnected, setKickConnected] = useState(false) + const [discordWebhook, setDiscordWebhook] = useState("") + const [discordEnabled, setDiscordEnabled] = useState(false) + const [discordGuildId, setDiscordGuildId] = useState("") + const [discordRoleId, setDiscordRoleId] = useState("") + const [hasBotToken, setHasBotToken] = useState(false) + const [translations, setTranslations] = useState>({}) + const [playerIdentifier, setPlayerIdentifier] = useState("") + const [detectedType, setDetectedType] = useState(null) + const [identifierType, setIdentifierType] = useState("license") + const [whitelistAction, setWhitelistAction] = useState("add") + const [showWhitelistView, setShowWhitelistView] = useState(false) + const [whitelistEntries, setWhitelistEntries] = useState([]) + const [filteredEntries, setFilteredEntries] = useState([]) + const [searchQuery, setSearchQuery] = useState("") + const [rules, setRules] = useState([]) + const [expandedEntries, setExpandedEntries] = useState>(new Set()) + const [isSaving, setIsSaving] = useState(false) + const [copiedId, setCopiedId] = useState(null) + const [validationError, setValidationError] = useState("") + + const { playSound } = useAudioSystem() + + const t = (key: string): string => { + return translations[key] || key + } + + const detectIdentifierType = (value: string): IdentifierType | null => { + const cleanValue = value.trim().replace(/\s+/g, '').replace(/^\w+:/, '') + const len = cleanValue.length + + if (/^[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+$/.test(cleanValue) && len === 36) { + return "license2" + } + + if (/^[0-9a-fA-F]+$/.test(cleanValue) && len === 40) { + return "license" + } + + if (/^\d+$/.test(cleanValue)) { + if (len >= 17 && len <= 19) return "discord" + if (len === 16) return "xbl" + if (len >= 6 && len <= 8) return "fivem" + } + + if (/^[0-9a-fA-F]+$/.test(cleanValue) && len >= 15 && len <= 17) { + return "steam" + } + + return null + } + + const copyToClipboard = (text: string, id: string) => { + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + try { + document.execCommand('copy') + setCopiedId(id) + setTimeout(() => setCopiedId(null), 2000) + } catch (err) { + console.error('Failed to copy:', err) + } finally { + textArea.remove() + } + } + + const getIdentifierColor = (type: string) => { + const colorMap: Record = { + steam: "#66c0f4", + license: "#3b82f6", + license2: "#8b5cf6", + discord: "#5865f2", + xbl: "#107c10", + fivem: "#f97316" + } + return colorMap[type] || "#6b7280" + } + + const validateIdentifier = (value: string): boolean => { + const cleaned = value.trim() + + if (cleaned.length < 6 || cleaned.length > 50) { + setValidationError(t("identifier_length_error") || "Identifier must be between 6-50 characters") + return false + } + + if (!/^[a-zA-Z0-9:-]+$/.test(cleaned)) { + setValidationError(t("identifier_chars_error") || "Invalid characters in identifier") + return false + } + + setValidationError("") + return true + } + + const validateWebhook = (webhook: string): boolean => { + if (!webhook || webhook === '***CONFIGURED***') return true + + if (!webhook.startsWith('https://discord.com/api/webhooks/') && + !webhook.startsWith('https://discordapp.com/api/webhooks/')) { + setValidationError(t("webhook_invalid") || "Invalid Discord webhook URL") + return false + } + + setValidationError("") + return true + } + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const { action, data } = event.data + + if (action === 'openUI') { + setVisible(true) + setWhitelistEnabled(data.whitelistEnabled) + setGracePeriod(data.gracePeriod) + setKickConnected(data.kickConnected) + setDiscordWebhook(data.discordWebhook) + setDiscordEnabled(data.discordEnabled) + setDiscordGuildId(data.discordGuildId) + setDiscordRoleId(data.discordRoleId) + setHasBotToken(data.hasBotToken) + setRules(data.rules) + setTranslations(data.translations || {}) + } else if (action === 'closeUI') { + setVisible(false) + } + } + + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, []) + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && visible) { + e.preventDefault() + playSound("close") + if (showWhitelistView) { + setShowWhitelistView(false) + setSearchQuery("") + setExpandedEntries(new Set()) + } else { + handleClose() + } + } + } + + window.addEventListener('keydown', handleEscape) + return () => window.removeEventListener('keydown', handleEscape) + }, [visible, showWhitelistView]) + + useEffect(() => { + if (searchQuery.trim() === "") { + setFilteredEntries(whitelistEntries) + } else { + const query = searchQuery.toLowerCase() + setFilteredEntries( + whitelistEntries.filter(entry => + entry.playerName?.toLowerCase().includes(query) || + entry.identifiers?.some(id => id.toLowerCase().includes(query)) + ) + ) + } + }, [searchQuery, whitelistEntries]) + + useEffect(() => { + if (playerIdentifier.trim()) { + const detected = detectIdentifierType(playerIdentifier) + setDetectedType(detected) + if (detected && detected !== identifierType) { + setIdentifierType(detected) + } + } else { + setDetectedType(null) + } + }, [playerIdentifier]) + + const toggleExpanded = (id: number) => { + playSound("click") + const newExpanded = new Set(expandedEntries) + if (newExpanded.has(id)) { + newExpanded.delete(id) + } else { + newExpanded.add(id) + } + setExpandedEntries(newExpanded) + } + + const handleClose = () => { + playSound("close") + setVisible(false) + setValidationError("") + fetchNui('closeUI') + } + + const handleWhitelistToggle = (checked: boolean) => { + playSound(checked ? "toggleOn" : "toggleOff") + setWhitelistEnabled(checked) + } + + const handleKickConnectedToggle = (checked: boolean) => { + playSound(checked ? "toggleOn" : "toggleOff") + setKickConnected(checked) + } + + const handleDiscordToggle = (checked: boolean) => { + playSound(checked ? "toggleOn" : "toggleOff") + setDiscordEnabled(checked) + } + + const handleAddRule = (type: RuleType) => { + playSound("click") + const newRule: Rule = { + id: Date.now().toString(), + type, + enabled: true, + priority: rules.length + 1, + ...(type === "admin-presence" && { operator: "<" as Operator, value: 1, action: "enable" as Action }), + ...(type === "player-count" && { operator: ">" as Operator, value: 50, action: "enable" as Action }), + ...(type === "scheduled" && { startTime: "00:00", endTime: "00:00" }), + } + setRules([...rules, newRule]) + } + + const handleRemoveRule = (id: string) => { + playSound("click") + setRules(rules.filter((rule) => rule.id !== id)) + } + + const handleRuleToggle = (id: string, checked: boolean) => { + playSound(checked ? "toggleOn" : "toggleOff") + setRules(rules.map((rule) => (rule.id === id ? { ...rule, enabled: checked } : rule))) + } + + const updateRule = (id: string, updates: Partial) => { + setRules(rules.map((rule) => (rule.id === id ? { ...rule, ...updates } : rule))) + } + + const handleTestWebhook = () => { + playSound("click") + + if (!validateWebhook(discordWebhook)) { + return + } + + fetchNui("testWebhook") + } + + const handleManagePlayer = () => { + playSound("click") + + if (!playerIdentifier.trim()) { + setValidationError(t("identifier_required") || "Identifier is required") + return + } + + if (!validateIdentifier(playerIdentifier)) { + return + } + + fetchNui("managePlayer", { + identifier: playerIdentifier.trim(), + type: identifierType, + action: whitelistAction + }) + + setPlayerIdentifier("") + setDetectedType(null) + setValidationError("") + } + + const handleViewWhitelist = () => { + playSound("click") + fetchNui("getWhitelistEntries").then((entries: WhitelistEntry[]) => { + setWhitelistEntries(entries || []) + setFilteredEntries(entries || []) + setShowWhitelistView(true) + setExpandedEntries(new Set()) + }) + } + + const handleToggleWhitelistStatus = (entry: WhitelistEntry) => { + playSound("click") + const newStatus = entry.whitelisted === 1 ? 0 : 1 + + fetchNui("toggleWhitelistStatus", { + id: entry.id, + status: newStatus + }).then(() => { + setWhitelistEntries(whitelistEntries.map(e => + e.id === entry.id ? { ...e, whitelisted: newStatus } : e + )) + }) + } + + const handleSave = async () => { + if (isSaving) return + + if (!validateWebhook(discordWebhook)) { + return + } + + playSound("click") + setIsSaving(true) + + try { + const configData = { + whitelistEnabled, + gracePeriod, + kickConnected, + discordWebhook: discordWebhook !== '***CONFIGURED***' ? discordWebhook : '', + discordEnabled, + discordGuildId, + discordRoleId, + rules, + } + + await fetchNui("updateConfig", configData) + setValidationError("") + } finally { + setTimeout(() => setIsSaving(false), 1000) + } + } + + const handleReset = () => { + playSound("click") + setValidationError("") + fetchNui("getConfig").then((config: WhitelistConfig) => { + setWhitelistEnabled(config.whitelistEnabled) + setGracePeriod(config.gracePeriod) + setKickConnected(config.kickConnected) + setDiscordWebhook(config.discordWebhook) + setDiscordEnabled(config.discordEnabled) + setDiscordGuildId(config.discordGuildId) + setDiscordRoleId(config.discordRoleId) + setHasBotToken(config.hasBotToken) + setRules(config.rules) + }) + } + + const getRuleIcon = (type: RuleType) => { + switch (type) { + case "admin-presence": + return + case "player-count": + return + case "scheduled": + return + } + } + + const getRuleTitle = (type: RuleType) => { + switch (type) { + case "admin-presence": + return t("admin_presence") + case "player-count": + return t("player_count") + case "scheduled": + return t("scheduled_time") + } + } + + if (!visible) return null + + if (showWhitelistView) { + return ( +
+
+
+
+ ESX Logo +

+ {t("whitelist_list")} +

+
+ +
+ + + +
+ + setSearchQuery(e.target.value)} + className="flex-1 cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + /> +
+
+
+ + + +
+ + + {t("total_entries")}: {filteredEntries.length} + +
+
+ +
+ {filteredEntries.length === 0 ? ( +

+ {searchQuery ? t("no_results") || "No results found" : t("no_entries")} +

+ ) : ( + filteredEntries.map((entry, index) => { + const isExpanded = expandedEntries.has(entry.id) + + const groupedIdentifiers = entry.identifiers.reduce((acc, id) => { + const [type, value] = id.split(':') + if (!acc[type]) acc[type] = [] + acc[type].push({ full: id, value }) + return acc + }, {} as Record>) + + return ( +
+
+
+
+ +
+
+
+

+ {entry.playerName} +

+ + ID: {entry.id} + +
+ + + + {isExpanded && ( +
+ {Object.entries(groupedIdentifiers).map(([type, identifiers]) => ( +
+
+ {identifiers.map((id, idx) => ( +
+ + {type} + + + {id.value} + + +
+ ))} +
+
+ ))} +
+ )} +
+ + {entry.whitelisted === 1 ? t("whitelisted") || "Whitelisted" : t("not_whitelisted") || "Not Whitelisted"} + +
+ +
+
+ ) + }) + )} +
+
+
+
+
+ ) + } + + return ( +
+
+
+
+ ESX Logo +

+ {t("ui_title")} +

+
+

{t("ui_subtitle")}

+
+ + {validationError && ( + + +
+ +

{validationError}

+
+
+
+ )} + + + + + + {t("current_status")} + + + +
+
+
+
+ + {whitelistEnabled ? t("whitelist_enabled") : t("whitelist_disabled")} + +
+
+ +
+ + + +
+ + + + + {t("grace_period")} + + + {t("grace_period_desc")} + + + +
+ + { + const value = Number.parseInt(e.target.value) + setGracePeriod(isNaN(value) ? 0 : value) + }} + className="border cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + min="0" + /> +
+ +
+ + +
+
+
+ + + + + + {t("discord_notifications")} + + + {t("discord_notifications_desc")} + + + +
+ + setDiscordWebhook(e.target.value)} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + /> + {discordWebhook === '***CONFIGURED***' && ( +

+ {t("webhook_configured")} +

+ )} +
+ +
+
+
+ + + +
+
+ + + {t("discord_verification")} + + + {t("discord_verification_desc")} + +
+ +
+
+ + {discordEnabled && ( + <> +
+
+
+

+ {t("discord_token_status")} +

+

+ {hasBotToken ? t("discord_token_configured") : t("discord_token_missing")} +

+
+
+ + {!hasBotToken && ( +
+ +

+ {t("discord_token_warning")} +

+
+ )} + + )} + +
+
+ + setDiscordGuildId(e.target.value)} + disabled={!discordEnabled} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + opacity: discordEnabled ? 1 : 0.5, + }} + /> +
+
+ + setDiscordRoleId(e.target.value)} + disabled={!discordEnabled} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + opacity: discordEnabled ? 1 : 0.5, + }} + /> +
+
+ + + + {!discordEnabled && ( + + +
+
+ + + {t("manage_whitelist")} + + + {t("manage_whitelist_desc")} + +
+ +
+
+ +
+
+ + +
+ +
+
+ + {detectedType && ( + + + {detectedType.toUpperCase()} + + )} +
+ +
+ +
+ + setPlayerIdentifier(e.target.value)} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: detectedType ? "rgba(16, 185, 129, 0.4)" : "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + /> +
+ +
+ +
+
+
+
+ )} + + + +
+
+ + + {t("whitelist_rules")} + + + {t("whitelist_rules_desc")} + +
+
+ + + +
+
+
+ + {rules.map((rule, index) => ( +
+
+
+ handleRuleToggle(rule.id, checked)} /> +
+ {getRuleIcon(rule.type)} + + {getRuleTitle(rule.type)} + +
+ + {t("priority")} {rule.priority} + +
+ +
+ + {(rule.type === "admin-presence" || rule.type === "player-count") && ( +
+
+ + +
+
+ + updateRule(rule.id, { value: Number.parseInt(e.target.value) })} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + min="0" + /> +
+
+ + +
+
+ + updateRule(rule.id, { priority: Number.parseInt(e.target.value) })} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + min="1" + /> +
+
+ )} + + {rule.type === "scheduled" && ( +
+
+ + updateRule(rule.id, { startTime: e.target.value })} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + /> +
+
+ + updateRule(rule.id, { endTime: e.target.value })} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + /> +
+
+ + updateRule(rule.id, { priority: Number.parseInt(e.target.value) })} + className="cursor-text transition-all duration-200" + style={{ + background: "rgba(25, 25, 25, 0.95)", + borderColor: "rgba(251, 155, 4, 0.25)", + color: "#F2F2F2", + fontSize: "14px", + }} + min="1" + /> +
+
+ )} +
+ ))} +
+
+ +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/[core]/esx_whitelist/web/components.json b/[core]/esx_whitelist/web/components.json new file mode 100644 index 000000000..4ee62ee10 --- /dev/null +++ b/[core]/esx_whitelist/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/[core]/esx_whitelist/web/components/theme-provider.tsx b/[core]/esx_whitelist/web/components/theme-provider.tsx new file mode 100644 index 000000000..55c2f6eb6 --- /dev/null +++ b/[core]/esx_whitelist/web/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/[core]/esx_whitelist/web/components/ui/badge.tsx b/[core]/esx_whitelist/web/components/ui/badge.tsx new file mode 100644 index 000000000..fc4126b7a --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/[core]/esx_whitelist/web/components/ui/button.tsx b/[core]/esx_whitelist/web/components/ui/button.tsx new file mode 100644 index 000000000..f64632d15 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/button.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/[core]/esx_whitelist/web/components/ui/card.tsx b/[core]/esx_whitelist/web/components/ui/card.tsx new file mode 100644 index 000000000..db7dd3cc9 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/[core]/esx_whitelist/web/components/ui/input.tsx b/[core]/esx_whitelist/web/components/ui/input.tsx new file mode 100644 index 000000000..c7780e847 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +function Input({ className, type, value, ...props }: React.ComponentProps<'input'>) { + // Convert NaN values to empty string for controlled inputs + const safeValue = typeof value === 'number' && isNaN(value) ? '' : value + + return ( + + ) +} + +export { Input } diff --git a/[core]/esx_whitelist/web/components/ui/label.tsx b/[core]/esx_whitelist/web/components/ui/label.tsx new file mode 100644 index 000000000..5d66da2c6 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/label.tsx @@ -0,0 +1,24 @@ +'use client' + +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' + +import { cn } from '@/lib/utils' + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/[core]/esx_whitelist/web/components/ui/select.tsx b/[core]/esx_whitelist/web/components/ui/select.tsx new file mode 100644 index 000000000..5db0bcac0 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/select.tsx @@ -0,0 +1,185 @@ +'use client' + +import * as React from 'react' +import * as SelectPrimitive from '@radix-ui/react-select' +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = 'default', + children, + ...props +}: React.ComponentProps & { + size?: 'sm' | 'default' +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = 'popper', + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/[core]/esx_whitelist/web/components/ui/separator.tsx b/[core]/esx_whitelist/web/components/ui/separator.tsx new file mode 100644 index 000000000..f43aaf551 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/separator.tsx @@ -0,0 +1,28 @@ +'use client' + +import * as React from 'react' +import * as SeparatorPrimitive from '@radix-ui/react-separator' + +import { cn } from '@/lib/utils' + +function Separator({ + className, + orientation = 'horizontal', + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/[core]/esx_whitelist/web/components/ui/switch.tsx b/[core]/esx_whitelist/web/components/ui/switch.tsx new file mode 100644 index 000000000..3c4cfa325 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/switch.tsx @@ -0,0 +1,31 @@ +'use client' + +import * as React from 'react' +import * as SwitchPrimitive from '@radix-ui/react-switch' + +import { cn } from '@/lib/utils' + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Switch } diff --git a/[core]/esx_whitelist/web/components/ui/use-mobile.tsx b/[core]/esx_whitelist/web/components/ui/use-mobile.tsx new file mode 100644 index 000000000..4331d5c56 --- /dev/null +++ b/[core]/esx_whitelist/web/components/ui/use-mobile.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' + +const MOBILE_BREAKPOINT = 768 + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(undefined) + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener('change', onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener('change', onChange) + }, []) + + return !!isMobile +} diff --git a/[core]/esx_whitelist/web/hooks/useKeyPress.ts b/[core]/esx_whitelist/web/hooks/useKeyPress.ts new file mode 100644 index 000000000..fa97ce953 --- /dev/null +++ b/[core]/esx_whitelist/web/hooks/useKeyPress.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react' + +export const useKeyPress = (targetKey: string) => { + const [keyPressed, setKeyPressed] = useState(false) + + useEffect(() => { + const downHandler = (e: KeyboardEvent) => { + if (e.key === targetKey) { + e.preventDefault() + setKeyPressed(true) + } + } + + const upHandler = (e: KeyboardEvent) => { + if (e.key === targetKey) { + e.preventDefault() + setKeyPressed(false) + } + } + + window.addEventListener('keydown', downHandler) + window.addEventListener('keyup', upHandler) + + return () => { + window.removeEventListener('keydown', downHandler) + window.removeEventListener('keyup', upHandler) + } + }, [targetKey]) + + return keyPressed +} \ No newline at end of file diff --git a/[core]/esx_whitelist/web/lib/utils.ts b/[core]/esx_whitelist/web/lib/utils.ts new file mode 100644 index 000000000..fed2fe91e --- /dev/null +++ b/[core]/esx_whitelist/web/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/[core]/esx_whitelist/web/next-env.d.ts b/[core]/esx_whitelist/web/next-env.d.ts new file mode 100644 index 000000000..1b3be0840 --- /dev/null +++ b/[core]/esx_whitelist/web/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/[core]/esx_whitelist/web/next.config.mjs b/[core]/esx_whitelist/web/next.config.mjs new file mode 100644 index 000000000..8731c96e7 --- /dev/null +++ b/[core]/esx_whitelist/web/next.config.mjs @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + distDir: 'dist', + images: { + unoptimized: true, + }, + trailingSlash: true, + assetPrefix: './', +} + +export default nextConfig \ No newline at end of file diff --git a/[core]/esx_whitelist/web/package-lock.json b/[core]/esx_whitelist/web/package-lock.json new file mode 100644 index 000000000..6949ef01f --- /dev/null +++ b/[core]/esx_whitelist/web/package-lock.json @@ -0,0 +1,3997 @@ +{ + "name": "esx_whitelist", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "esx_whitelist", + "version": "0.1.0", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "1.2.2", + "@radix-ui/react-alert-dialog": "1.1.4", + "@radix-ui/react-aspect-ratio": "1.1.1", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-context-menu": "2.2.4", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-hover-card": "1.1.4", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-menubar": "1.1.4", + "@radix-ui/react-navigation-menu": "1.2.3", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "1.1.1", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-scroll-area": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slider": "1.2.2", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-toggle-group": "1.1.1", + "@radix-ui/react-tooltip": "1.1.6", + "@vercel/analytics": "latest", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "4.1.0", + "embla-carousel-react": "8.5.1", + "geist": "latest", + "input-otp": "1.4.1", + "lucide-react": "^0.454.0", + "next": "15.2.4", + "next-themes": "^0.4.6", + "react": "^19", + "react-day-picker": "9.8.0", + "react-dom": "^19", + "react-hook-form": "^7.60.0", + "react-resizable-panels": "^2.1.7", + "recharts": "2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", + "zod": "3.25.67" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.9", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "postcss": "^8.5", + "tailwindcss": "^4.1.9", + "tw-animate-css": "1.3.3", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, + "node_modules/@emnapi/runtime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", + "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", + "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", + "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", + "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", + "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", + "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", + "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", + "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", + "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz", + "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz", + "integrity": "sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.1.tgz", + "integrity": "sha512-kNU4FIpcFMBLkOUcgeIteH06/8JLBcYY6Le1iKenDGCYNYFX3TQqCZjzkOsz37h7r94/99GTb7YhEr98ZBJibw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz", + "integrity": "sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz", + "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", + "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.4.tgz", + "integrity": "sha512-ap4wdGwK52rJxGkwukU1NrnEodsUFQIooANKu+ey7d6raQ2biTcEf8za1zr0mgFHieevRTB2nK4dJeN8pTAZGQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz", + "integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.4.tgz", + "integrity": "sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz", + "integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.4.tgz", + "integrity": "sha512-+KMpi7VAZuB46+1LD7a30zb5IxyzLgC8m8j42gk3N4TUCcViNQdX8FhoH1HDvYiA8quuqcek4R4bYpPn/SY1GA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.4", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.3.tgz", + "integrity": "sha512-IQWAsQ7dsLIYDrn0WqPU+cdM7MONTv9nqrLVYoie3BPiabSfUVDe6Fr+oEt0Cofsr9ONDcDe9xhmJbL1Uq1yKg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz", + "integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz", + "integrity": "sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz", + "integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz", + "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz", + "integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", + "integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz", + "integrity": "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", + "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz", + "integrity": "sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz", + "integrity": "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.1.tgz", + "integrity": "sha512-OgDLZEA30Ylyz8YSXvnGqIHtERqnUt1KUYTKdw/y8u7Ci6zGiJfXc02jahmcSNK3YcErqioj/9flWC9S1ihfwg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz", + "integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", + "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz", + "integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.15" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz", + "integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.15", + "@tailwindcss/oxide-darwin-arm64": "4.1.15", + "@tailwindcss/oxide-darwin-x64": "4.1.15", + "@tailwindcss/oxide-freebsd-x64": "4.1.15", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.15", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.15", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.15", + "@tailwindcss/oxide-linux-x64-musl": "4.1.15", + "@tailwindcss/oxide-wasm32-wasi": "4.1.15", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.15", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.15" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz", + "integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz", + "integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz", + "integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz", + "integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz", + "integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz", + "integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz", + "integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz", + "integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz", + "integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz", + "integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz", + "integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz", + "integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.15.tgz", + "integrity": "sha512-IZh8IT76KujRz6d15wZw4eoeViT4TqmzVWNNfpuNCTKiaZUwgr5vtPqO4HjuYDyx3MgGR5qgPt1HMzTeLJyA3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.15", + "@tailwindcss/oxide": "4.1.15", + "postcss": "^8.4.41", + "tailwindcss": "4.1.15" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", + "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vercel/analytics": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.5.0.tgz", + "integrity": "sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", + "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", + "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.1.tgz", + "integrity": "sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.5.1", + "embla-carousel-reactive-utils": "8.5.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.1.tgz", + "integrity": "sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.5.1" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/geist": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.5.1.tgz", + "integrity": "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==", + "license": "SIL OPEN FONT LICENSE", + "peerDependencies": { + "next": ">=13.2.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/input-otp": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", + "integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT", + "optional": true + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.454.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz", + "integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", + "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.2.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.2.4", + "@next/swc-darwin-x64": "15.2.4", + "@next/swc-linux-arm64-gnu": "15.2.4", + "@next/swc-linux-arm64-musl": "15.2.4", + "@next/swc-linux-x64-gnu": "15.2.4", + "@next/swc-linux-x64-musl": "15.2.4", + "@next/swc-win32-arm64-msvc": "15.2.4", + "@next/swc-win32-x64-msvc": "15.2.4", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.0.tgz", + "integrity": "sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "1.2.0", + "date-fns": "4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.65.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", + "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz", + "integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==", + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.3.tgz", + "integrity": "sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/[core]/esx_whitelist/web/package.json b/[core]/esx_whitelist/web/package.json new file mode 100644 index 000000000..023078321 --- /dev/null +++ b/[core]/esx_whitelist/web/package.json @@ -0,0 +1,74 @@ +{ + "name": "esx_whitelist", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev", + "lint": "next lint", + "start": "next start" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "1.2.2", + "@radix-ui/react-alert-dialog": "1.1.4", + "@radix-ui/react-aspect-ratio": "1.1.1", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-context-menu": "2.2.4", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-hover-card": "1.1.4", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-menubar": "1.1.4", + "@radix-ui/react-navigation-menu": "1.2.3", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "1.1.1", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-scroll-area": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slider": "1.2.2", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-toggle-group": "1.1.1", + "@radix-ui/react-tooltip": "1.1.6", + "@vercel/analytics": "latest", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "4.1.0", + "embla-carousel-react": "8.5.1", + "geist": "latest", + "input-otp": "1.4.1", + "lucide-react": "^0.454.0", + "next": "15.2.4", + "next-themes": "^0.4.6", + "react": "^19", + "react-day-picker": "9.8.0", + "react-dom": "^19", + "react-hook-form": "^7.60.0", + "react-resizable-panels": "^2.1.7", + "recharts": "2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", + "zod": "3.25.67" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.9", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "postcss": "^8.5", + "tailwindcss": "^4.1.9", + "tw-animate-css": "1.3.3", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/[core]/esx_whitelist/web/pnpm-lock.yaml b/[core]/esx_whitelist/web/pnpm-lock.yaml new file mode 100644 index 000000000..ca98c4816 --- /dev/null +++ b/[core]/esx_whitelist/web/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false \ No newline at end of file diff --git a/[core]/esx_whitelist/web/postcss.config.mjs b/[core]/esx_whitelist/web/postcss.config.mjs new file mode 100644 index 000000000..a869506ee --- /dev/null +++ b/[core]/esx_whitelist/web/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +} + +export default config diff --git a/[core]/esx_whitelist/web/public/esx-logo.png b/[core]/esx_whitelist/web/public/esx-logo.png new file mode 100644 index 000000000..9be59f999 Binary files /dev/null and b/[core]/esx_whitelist/web/public/esx-logo.png differ diff --git a/[core]/esx_whitelist/web/styles/globals.css b/[core]/esx_whitelist/web/styles/globals.css new file mode 100644 index 000000000..23cc7e3a9 --- /dev/null +++ b/[core]/esx_whitelist/web/styles/globals.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/[core]/esx_whitelist/web/tsconfig.json b/[core]/esx_whitelist/web/tsconfig.json new file mode 100644 index 000000000..bfb5cd229 --- /dev/null +++ b/[core]/esx_whitelist/web/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "target": "ES6", + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "next-env.d.ts", + "dist/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +}