39514-vm/scitemcustom.php
2026-05-06 23:34:35 +00:00

1916 lines
81 KiB
PHP

<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
require_once __DIR__ . '/db/scitemcustom.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scitemcustom.php', 'Item Custom');
auth_require_page_access('scitemcustom.php', 'Item Custom');
scstatsitem_bootstrap();
scitemcustom_bootstrap();
function scitemcustom_normalize_sign(?string $sign): string
{
if ($sign === '-') {
return '-';
}
if ($sign === '+') {
return '+';
}
return '';
}
function scitemcustom_normalize_value(?string $value): ?string
{
$raw = str_replace(',', '.', trim((string) $value));
if ($raw === '' || !is_numeric($raw)) {
return null;
}
return number_format(abs((float) $raw), 2, '.', '');
}
function scitemcustom_display_value($value): string
{
if (!is_numeric((string) $value)) {
return '0';
}
$formatted = number_format((float) $value, 2, '.', '');
$formatted = rtrim(rtrim($formatted, '0'), '.');
return $formatted === '' ? '0' : $formatted;
}
function scitemcustom_escape_like(string $value): string
{
return strtr($value, [
'\\' => '\\\\',
'%' => '\\%',
'_' => '\\_',
]);
}
function scitemcustom_normalize_rarity(?string $rarity): string
{
return strtoupper(trim((string) $rarity));
}
function scitemcustom_rarity_class(?string $rarity): string
{
return match (scitemcustom_normalize_rarity($rarity)) {
'L' => 'rarity-L',
'E' => 'rarity-E',
'R' => 'rarity-R',
'U' => 'rarity-U',
'C' => 'rarity-C',
default => '',
};
}
function scitemcustom_rarity_label(?string $rarity): string
{
return match (scitemcustom_normalize_rarity($rarity)) {
'L' => 'Légendaire',
'E' => 'Épique',
'R' => 'Rare',
'U' => 'Peu commun',
'C' => 'Commun',
default => '',
};
}
function scitemcustom_search_available_items(PDO $db, int $ownerAuthId, string $query, ?int $limit = null): array
{
$query = trim($query);
if ($query === '') {
return [];
}
$escapedQuery = scitemcustom_escape_like($query);
$exact = $escapedQuery;
$prefix = $escapedQuery . '%';
$contains = '%' . $escapedQuery . '%';
$limitClause = '';
if ($limit !== null && $limit > 0) {
$limitClause = ' LIMIT ' . (int) $limit;
}
$sql = "SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_uuid, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_rarity
FROM tbl_scobjs
WHERE (
cl_scobjs_name LIKE :contains_name
OR cl_scobjs_type LIKE :contains_type
OR cl_scobjs_subtype LIKE :contains_subtype
OR cl_scobjs_uuid LIKE :contains_uuid
)
ORDER BY
CASE
WHEN cl_scobjs_name = :exact_name THEN 0
WHEN cl_scobjs_name LIKE :prefix_name THEN 1
WHEN cl_scobjs_uuid = :exact_uuid THEN 2
WHEN cl_scobjs_uuid LIKE :prefix_uuid THEN 3
WHEN cl_scobjs_type LIKE :prefix_type THEN 4
WHEN cl_scobjs_subtype LIKE :prefix_subtype THEN 5
ELSE 6
END ASC,
CHAR_LENGTH(cl_scobjs_name) ASC,
cl_scobjs_name ASC,
cl_scobjs_id ASC
{$limitClause}";
$stmt = $db->prepare($sql);
$stmt->execute([
'contains_name' => $contains,
'contains_type' => $contains,
'contains_subtype' => $contains,
'contains_uuid' => $contains,
'exact_name' => $exact,
'prefix_name' => $prefix,
'exact_uuid' => $exact,
'prefix_uuid' => $prefix,
'prefix_type' => $prefix,
'prefix_subtype' => $prefix,
]);
return $stmt->fetchAll() ?: [];
}
function scitemcustom_preview(string $sign, $value, string $unit): string
{
$prefix = $sign === '-' ? '-' : ($sign === '+' ? '+' : '');
return $prefix . scitemcustom_display_value($value) . ' ' . $unit;
}
function scitemcustom_current_owner_auth_id(PDO $db): int
{
$session_user = isset($_SESSION['user']) ? trim((string) $_SESSION['user']) : '';
if ($session_user === '') {
return 0;
}
$stmt_owner = $db->prepare(
'SELECT cl_auth_id
FROM tbl_auth
WHERE cl_auth_user = :user
LIMIT 1'
);
$stmt_owner->execute(['user' => $session_user]);
return (int) $stmt_owner->fetchColumn();
}
function scitemcustom_redirect(?int $itemcustom_id = null): void
{
$location = 'scitemcustom.php';
if ($itemcustom_id !== null && $itemcustom_id > 0) {
$location .= '#itemcustom-' . $itemcustom_id;
}
header('Location: ' . $location);
exit;
}
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
$allowed_signs = ['+', '', '-'];
$sign_labels = ['+' => '+', '' => 'Aucun', '-' => '-'];
$current_owner_auth_id = scitemcustom_current_owner_auth_id($db);
if ($current_owner_auth_id <= 0) {
auth_flash_set('error', 'Utilisateur introuvable. Merci de vous reconnecter.');
header('Location: logout.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
scitemcustom_redirect();
}
$action = $_POST['action'] ?? '';
if ($action === 'add_custom_item') {
$obj_id = (int) ($_POST['obj_id'] ?? 0);
$redirect_itemcustom_id = null;
if ($obj_id <= 0) {
auth_flash_set('error', 'Objet invalide.');
} else {
try {
$stmt_check = $db->prepare('SELECT cl_scobjs_id FROM tbl_scobjs WHERE cl_scobjs_id = :id');
$stmt_check->execute(['id' => $obj_id]);
if (!$stmt_check->fetch()) {
auth_flash_set('error', 'Objet introuvable.');
} else {
$stmt_insert = $db->prepare(
'INSERT INTO tbl_scitemcustom (cl_scitemcustom_owner_auth_id, cl_scitemcustom_obj_id)
VALUES (:owner_auth_id, :obj_id)'
);
$stmt_insert->execute([
'owner_auth_id' => $current_owner_auth_id,
'obj_id' => $obj_id,
]);
$redirect_itemcustom_id = (int) $db->lastInsertId();
auth_flash_set('success', 'Objet ajouté dans Item Custom.');
}
} catch (PDOException $e) {
if ((string) $e->getCode() === '23000') {
auth_flash_set('error', 'Cet objet est déjà présent dans Item Custom.');
} else {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
}
}
scitemcustom_redirect($redirect_itemcustom_id);
}
if ($action === 'delete_custom_item') {
$itemcustom_id = (int) ($_POST['itemcustom_id'] ?? 0);
if ($itemcustom_id > 0) {
try {
$stmt_delete = $db->prepare(
'DELETE FROM tbl_scitemcustom
WHERE cl_scitemcustom_id = :id
AND cl_scitemcustom_owner_auth_id = :owner_auth_id'
);
$stmt_delete->execute([
'id' => $itemcustom_id,
'owner_auth_id' => $current_owner_auth_id,
]);
if ($stmt_delete->rowCount() > 0) {
auth_flash_set('success', 'Objet Item Custom supprimé.');
} else {
auth_flash_set('error', 'Objet introuvable ou non autorisé.');
}
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la suppression : ' . $e->getMessage());
}
}
scitemcustom_redirect();
}
if ($action === 'add_custom_stat') {
$itemcustom_id = (int) ($_POST['itemcustom_id'] ?? 0);
$stat_id = (int) ($_POST['stat_id'] ?? 0);
$sign = scitemcustom_normalize_sign($_POST['sign'] ?? '+');
$value = scitemcustom_normalize_value($_POST['value'] ?? '');
if ($itemcustom_id <= 0 || $stat_id <= 0 || $value === null) {
auth_flash_set('error', 'Données de statistique invalides.');
} else {
try {
$stmt_check = $db->prepare(
'SELECT c.cl_scitemcustom_id, s.cl_scstatsitem_id
FROM tbl_scitemcustom c
JOIN tbl_scstatsitem s ON s.cl_scstatsitem_id = :stat_id
WHERE c.cl_scitemcustom_id = :itemcustom_id
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id'
);
$stmt_check->execute([
'itemcustom_id' => $itemcustom_id,
'stat_id' => $stat_id,
'owner_auth_id' => $current_owner_auth_id,
]);
if (!$stmt_check->fetch()) {
auth_flash_set('error', 'Objet ou statistique introuvable.');
} else {
$stmt_insert = $db->prepare(
'INSERT INTO tbl_scitemcustomstat (
cl_scitemcustomstat_itemcustom_id,
cl_scitemcustomstat_stat_id,
cl_scitemcustomstat_sign,
cl_scitemcustomstat_value
) VALUES (:itemcustom_id, :stat_id, :sign, :value)'
);
$stmt_insert->execute([
'itemcustom_id' => $itemcustom_id,
'stat_id' => $stat_id,
'sign' => $sign,
'value' => $value,
]);
auth_flash_set('success', 'Statistique ajoutée à l\'objet.');
}
} catch (PDOException $e) {
if ((string) $e->getCode() === '23000') {
auth_flash_set('error', 'Cette statistique est déjà configurée pour cet objet.');
} else {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
}
}
scitemcustom_redirect($itemcustom_id);
}
if ($action === 'update_custom_stat') {
$custom_stat_id = (int) ($_POST['custom_stat_id'] ?? 0);
$itemcustom_id = (int) ($_POST['itemcustom_id'] ?? 0);
$stat_id = (int) ($_POST['stat_id'] ?? 0);
$sign = scitemcustom_normalize_sign($_POST['sign'] ?? '+');
$value = scitemcustom_normalize_value($_POST['value'] ?? '');
if ($custom_stat_id <= 0 || $stat_id <= 0 || $value === null) {
auth_flash_set('error', 'Données de mise à jour invalides.');
} else {
try {
$stmt_update = $db->prepare(
'UPDATE tbl_scitemcustomstat cs
JOIN tbl_scitemcustom c ON c.cl_scitemcustom_id = cs.cl_scitemcustomstat_itemcustom_id
JOIN tbl_scstatsitem s ON s.cl_scstatsitem_id = :stat_id
SET cs.cl_scitemcustomstat_stat_id = :stat_id,
cs.cl_scitemcustomstat_sign = :sign,
cs.cl_scitemcustomstat_value = :value
WHERE cs.cl_scitemcustomstat_id = :id
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id'
);
$stmt_update->execute([
'stat_id' => $stat_id,
'sign' => $sign,
'value' => $value,
'id' => $custom_stat_id,
'owner_auth_id' => $current_owner_auth_id,
]);
if ($stmt_update->rowCount() > 0) {
auth_flash_set('success', 'Statistique mise à jour.');
} else {
auth_flash_set('error', 'Statistique introuvable ou non autorisée.');
}
} catch (PDOException $e) {
if ((string) $e->getCode() === '23000') {
auth_flash_set('error', 'Cette statistique est déjà configurée pour cet objet.');
} else {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
}
}
scitemcustom_redirect($itemcustom_id);
}
if ($action === 'delete_custom_stat') {
$custom_stat_id = (int) ($_POST['custom_stat_id'] ?? 0);
$itemcustom_id = (int) ($_POST['itemcustom_id'] ?? 0);
if ($custom_stat_id > 0) {
try {
$stmt_delete = $db->prepare(
'DELETE cs
FROM tbl_scitemcustomstat cs
JOIN tbl_scitemcustom c ON c.cl_scitemcustom_id = cs.cl_scitemcustomstat_itemcustom_id
WHERE cs.cl_scitemcustomstat_id = :id
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id'
);
$stmt_delete->execute([
'id' => $custom_stat_id,
'owner_auth_id' => $current_owner_auth_id,
]);
if ($stmt_delete->rowCount() > 0) {
auth_flash_set('success', 'Statistique supprimée de l\'objet.');
} else {
auth_flash_set('error', 'Statistique introuvable ou non autorisée.');
}
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la suppression : ' . $e->getMessage());
}
}
scitemcustom_redirect($itemcustom_id);
}
}
if ($_SERVER['REQUEST_METHOD'] === 'GET' && (string) ($_GET['ajax'] ?? '') === 'item_suggestions') {
header('Content-Type: application/json; charset=UTF-8');
$query = trim((string) ($_GET['q'] ?? ''));
$items = [];
if (mb_strlen($query) >= 3) {
$items = array_map(static function (array $row): array {
$rarity = (string) ($row['cl_scobjs_rarity'] ?? '');
return [
'id' => (int) $row['cl_scobjs_id'],
'name' => (string) $row['cl_scobjs_name'],
'uuid' => (string) $row['cl_scobjs_uuid'],
'type' => (string) $row['cl_scobjs_type'],
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
'rarity' => scitemcustom_normalize_rarity($rarity),
'rarity_class' => scitemcustom_rarity_class($rarity),
'rarity_label' => scitemcustom_rarity_label($rarity),
];
}, scitemcustom_search_available_items($db, $current_owner_auth_id, $query));
}
echo json_encode(['items' => $items], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$search = trim($_GET['search'] ?? '');
$search_results = $search !== ''
? scitemcustom_search_available_items($db, $current_owner_auth_id, $search, 15)
: [];
$stmt_stats_catalog = $db->query('SELECT * FROM tbl_scstatsitem ORDER BY cl_scstatsitem_name ASC, cl_scstatsitem_id ASC');
$stats_catalog = $stmt_stats_catalog->fetchAll();
$stats_by_id = [];
foreach ($stats_catalog as $stat_catalog_row) {
$stats_by_id[(int) $stat_catalog_row['cl_scstatsitem_id']] = $stat_catalog_row;
}
$sql_custom_items = "SELECT c.*, o.cl_scobjs_name, o.cl_scobjs_uuid, o.cl_scobjs_type, o.cl_scobjs_subtype, o.cl_scobjs_rarity
FROM tbl_scitemcustom c
JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id
ORDER BY o.cl_scobjs_name ASC, c.cl_scitemcustom_id ASC";
$stmt_custom_items = $db->prepare($sql_custom_items);
$stmt_custom_items->execute(['owner_auth_id' => $current_owner_auth_id]);
$custom_items = $stmt_custom_items->fetchAll();
$stmt_custom_stats = $db->prepare(
"SELECT cs.*, st.cl_scstatsitem_name, st.cl_scstatsitem_unit
FROM tbl_scitemcustomstat cs
JOIN tbl_scitemcustom c ON c.cl_scitemcustom_id = cs.cl_scitemcustomstat_itemcustom_id
JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id
ORDER BY cs.cl_scitemcustomstat_itemcustom_id ASC, st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt_custom_stats->execute(['owner_auth_id' => $current_owner_auth_id]);
$custom_stats_rows = $stmt_custom_stats->fetchAll();
$custom_stats_by_item = [];
foreach ($custom_stats_rows as $custom_stat_row) {
$item_key = (int) $custom_stat_row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($custom_stats_by_item[$item_key])) {
$custom_stats_by_item[$item_key] = [];
}
$custom_stats_by_item[$item_key][] = $custom_stat_row;
}
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OBJETS PERSONNALISES | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--rarity-L: #ff8000;
--rarity-E: #a335ee;
--rarity-R: #0070dd;
--rarity-U: #1eff00;
--rarity-C: #ffffff;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
gap: 1rem;
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
box-sizing: border-box;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.45rem 0.75rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.flash {
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border: 1px solid transparent;
}
.flash.success {
background: rgba(0, 255, 136, 0.08);
color: var(--success);
border-color: rgba(0, 255, 136, 0.2);
}
.flash.error {
background: rgba(255, 77, 77, 0.08);
color: #ffb3b3;
border-color: rgba(255, 77, 77, 0.2);
}
.admin-grid {
display: grid;
grid-template-columns: 370px 1fr;
gap: 2rem;
align-items: start;
}
.glass-card {
position: relative;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 14px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.28);
padding: 1.5rem;
}
.item-picker-card {
z-index: 30;
}
.glass-card h2,
.glass-card h3 {
margin-top: 0;
color: var(--primary);
letter-spacing: 1px;
text-transform: uppercase;
}
.stack {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.45rem;
color: var(--primary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.form-control {
width: 100%;
box-sizing: border-box;
padding: 0.85rem 1rem;
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(162, 155, 120, 0.15);
color: #fff;
outline: none;
font-family: 'Electrolize', sans-serif;
}
.form-control:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(162, 155, 120, 0.15);
}
select.form-control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.form-control:focus {
background: #3d444f;
color: #fff;
}
select.form-control option {
background: #353b45;
color: #fff;
}
select.form-control option:checked {
background: #4a5260;
color: #fff;
}
.form-help {
margin-top: 0.4rem;
font-size: 0.8rem;
color: #9fa7b8;
line-height: 1.4;
}
.search-result-list,
.item-card-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.item-picker-form {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.item-picker-wrap {
position: relative;
}
.item-picker-input {
padding-right: 3rem;
}
.item-picker-dropdown {
position: absolute;
top: calc(100% + 0.45rem);
left: 0;
right: 0;
z-index: 20;
display: flex;
flex-direction: column;
gap: 0.65rem;
max-height: 360px;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
padding: 0.75rem;
border-radius: 12px;
border: 1px solid rgba(162, 155, 120, 0.22);
background: rgba(17, 24, 39, 0.96);
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.42);
backdrop-filter: blur(12px);
}
.item-picker-option {
display: flex;
gap: 0.85rem;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 10px;
padding: 0.8rem;
background: rgba(255, 255, 255, 0.035);
color: inherit;
text-align: left;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.item-picker-option:hover,
.item-picker-option:focus-visible,
.item-picker-option.is-active {
outline: none;
border-color: rgba(162, 155, 120, 0.55);
background: rgba(162, 155, 120, 0.09);
transform: translateY(-1px);
}
.item-picker-option-empty {
cursor: default;
}
.item-picker-option-empty:hover,
.item-picker-option-empty:focus-visible {
transform: none;
border-color: rgba(255, 255, 255, 0.07);
background: rgba(255, 255, 255, 0.035);
}
.item-picker-selection {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.9rem;
border: 1px solid rgba(162, 155, 120, 0.22);
border-radius: 12px;
padding: 0.85rem 1rem;
background: rgba(162, 155, 120, 0.06);
}
.item-picker-selection-main {
display: flex;
align-items: flex-start;
gap: 0.9rem;
min-width: 0;
flex: 1;
}
.item-picker-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.item-picker-actions .btn-modern[disabled] {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}
.search-item,
.custom-item-card {
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
}
.search-item {
display: flex;
gap: 0.9rem;
align-items: flex-start;
}
.search-item-main {
display: flex;
gap: 0.9rem;
align-items: flex-start;
min-width: 0;
width: 100%;
}
.item-meta {
display: flex;
align-items: flex-start;
gap: 0.9rem;
min-width: 0;
flex: 1;
}
.search-item-content {
display: flex;
flex-direction: column;
gap: 0.45rem;
min-width: 0;
flex: 1;
}
.search-item-actions {
display: flex;
align-items: flex-start;
}
.search-item-actions .btn-modern {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.item-preview {
width: 56px;
height: 56px;
object-fit: contain;
border-radius: 10px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 0.3rem;
flex-shrink: 0;
}
.item-name {
color: #fff;
font-size: 1rem;
display: block;
}
.item-picker-dropdown .item-name,
.search-result-list .item-name {
font-size: 0.92rem;
}
.rarity-L { color: var(--rarity-L); text-shadow: 0 0 12px rgba(255, 128, 0, 0.28); }
.rarity-E { color: var(--rarity-E); text-shadow: 0 0 12px rgba(163, 53, 238, 0.28); }
.rarity-R { color: var(--rarity-R); text-shadow: 0 0 12px rgba(0, 112, 221, 0.28); }
.rarity-U { color: var(--rarity-U); text-shadow: 0 0 12px rgba(30, 255, 0, 0.28); }
.rarity-C { color: var(--rarity-C); }
.item-submeta {
color: #96a0b5;
font-size: 0.78rem;
line-height: 1.45;
word-break: break-word;
}
.item-picker-dropdown .item-submeta,
.search-result-list .item-submeta {
font-size: 0.71rem;
line-height: 1.35;
}
.custom-item-card {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.custom-item-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
flex: 1;
min-width: 0;
}
.custom-item-summary {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.custom-item-summary-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
flex-wrap: wrap;
margin-left: auto;
}
.custom-item-stats-count {
flex-shrink: 0;
}
.item-toggle-btn {
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.6rem 0.9rem;
border-radius: 999px;
border: 1px solid rgba(162, 155, 120, 0.25);
background: rgba(162, 155, 120, 0.08);
color: #fff;
font-family: 'Electrolize', sans-serif;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.item-toggle-btn:hover {
background: rgba(162, 155, 120, 0.14);
border-color: rgba(162, 155, 120, 0.45);
}
.item-toggle-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(162, 155, 120, 0.18);
}
.item-toggle-label {
color: var(--primary);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 1px;
line-height: 1;
}
.item-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
line-height: 1;
transition: transform 0.2s ease;
}
.item-toggle-btn[aria-expanded="true"] .item-toggle-icon {
transform: rotate(180deg);
}
.custom-item-card {
scroll-margin-top: 1.5rem;
}
.custom-item-card.is-open {
border-color: rgba(162, 155, 120, 0.35);
box-shadow: 0 0 0 1px rgba(162, 155, 120, 0.12);
}
.custom-item-body {
display: grid;
gap: 1.25rem;
}
.custom-item-body[hidden] {
display: none !important;
}
.inline-form {
display: grid;
grid-template-columns: minmax(0, 1.4fr) 110px 120px auto;
gap: 0.75rem;
align-items: end;
}
.modern-table {
width: 100%;
border-collapse: collapse;
min-width: 780px;
}
.modern-table th,
.modern-table td {
padding: 0.9rem 0.8rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
vertical-align: middle;
}
.modern-table th {
color: var(--primary);
text-transform: uppercase;
font-size: 0.78rem;
letter-spacing: 1px;
text-align: left;
}
.modern-table tbody tr:hover {
background: rgba(255, 255, 255, 0.025);
}
.empty-state {
padding: 2rem;
text-align: center;
color: #7f8897;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.35rem 0.65rem;
border-radius: 999px;
border: 1px solid rgba(162, 155, 120, 0.25);
background: rgba(162, 155, 120, 0.08);
color: var(--primary);
font-size: 0.78rem;
white-space: nowrap;
}
.preview-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.45rem 0.75rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #fff;
font-size: 0.85rem;
min-width: 90px;
}
.actions-row {
display: flex;
gap: 0.4rem;
justify-content: flex-end;
flex-wrap: wrap;
}
.muted {
color: #96a0b5;
}
.count-note {
color: #96a0b5;
font-size: 0.85rem;
margin-top: -0.3rem;
margin-bottom: 1rem;
}
.list-filter-bar {
display: flex;
gap: 0.75rem;
align-items: center;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.list-filter-input {
flex: 1;
min-width: 240px;
}
.list-filter-meta {
color: #96a0b5;
font-size: 0.8rem;
}
.hidden-by-filter {
display: none !important;
}
@media (max-width: 1200px) {
.admin-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.admin-layout {
padding: 1rem;
}
.admin-topbar {
padding: 1rem 1.2rem;
flex-direction: column;
align-items: flex-start;
}
.inline-form {
grid-template-columns: 1fr;
}
.search-item-main,
.item-meta {
align-items: stretch;
}
.search-item-actions {
width: 100%;
}
.search-item-actions .btn-modern {
width: 100%;
justify-content: center;
}
.item-picker-selection {
flex-direction: column;
align-items: stretch;
}
.item-picker-actions {
flex-direction: column;
}
.item-picker-actions .btn-modern {
width: 100%;
justify-content: center;
}
.actions-row {
justify-content: stretch;
}
.custom-item-summary {
align-items: stretch;
}
.custom-item-summary-actions {
width: 100%;
justify-content: space-between;
}
.item-toggle-btn {
flex: 1;
justify-content: center;
}
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scitemcustom.php', 'OBJETS PERSONNALISES'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>OBJETS PERSONNALISES</h1>
<p>Associer des objets à autant de bonus / malus de stats que nécessaire</p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scitemcustom.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<aside class="stack">
<section class="glass-card item-picker-card">
<h2>Ajouter un objet</h2>
<p class="count-note">Choisis un objet depuis la base, puis attribue-lui autant de stats que nécessaire.</p>
<form method="post" action="scitemcustom.php" id="item-add-form" class="item-picker-form" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="add_custom_item">
<input type="hidden" name="obj_id" id="item-add-selected-id" value="">
<div class="item-picker-wrap">
<input
type="search"
id="item-add-search"
class="form-control item-picker-input"
placeholder="Rechercher un objet à ajouter..."
aria-label="Rechercher un objet à ajouter"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="item-add-suggestions"
spellcheck="false"
>
<div class="item-picker-dropdown hidden-by-filter" id="item-add-suggestions" role="listbox" aria-label="Suggestions d'objets"></div>
</div>
<div class="form-help" id="item-add-help">Commence à taper le nom d'un objet. La liste se déclenche à partir de 3 caractères saisis et affiche tous les objets correspondants.</div>
<div class="item-picker-selection hidden-by-filter" id="item-add-selection" aria-live="polite">
<div class="item-picker-selection-main">
<img src="" class="item-preview hidden-by-filter" id="item-add-selection-image" alt="">
<div class="search-item-content">
<strong class="item-name" id="item-add-selection-name"></strong>
<div class="item-submeta" id="item-add-selection-meta"></div>
</div>
</div>
<button type="button" class="btn-modern danger" id="item-add-clear">Effacer</button>
</div>
<div class="item-picker-actions">
<button type="submit" class="btn-modern" id="item-add-submit" disabled>Ajouter</button>
<button type="button" class="btn-modern danger" id="item-add-reset">Vider la recherche</button>
</div>
</form>
<noscript>
<div class="form-help" style="margin-top:1rem; margin-bottom:0.75rem;">JavaScript est désactivé : utilise la recherche classique ci-dessous.</div>
<form method="get" action="scitemcustom.php" style="display:flex; gap:0.75rem; flex-wrap:wrap;">
<input type="text" name="search" class="form-control" value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Rechercher un objet..." style="flex:1; min-width:220px;">
<button type="submit" class="btn-modern">Rechercher</button>
<a href="scitemcustom.php" class="btn-modern danger">Reset</a>
</form>
</noscript>
<?php if ($search !== ''): ?>
<div class="form-help" style="margin-top:1rem; margin-bottom:1rem;">Résultats de la recherche classique pour l'ajout dans la liste Item Custom.</div>
<div class="search-result-list">
<?php if (empty($search_results)): ?>
<div class="search-item">
<div class="item-submeta">Aucun objet disponible pour cette recherche.</div>
</div>
<?php else: ?>
<?php foreach ($search_results as $result): ?>
<div class="search-item">
<div class="search-item-main">
<div class="item-meta">
<img src="https://cstone.space/uifimages/<?php echo htmlspecialchars($result['cl_scobjs_uuid'], ENT_QUOTES, 'UTF-8'); ?>.png" class="item-preview" alt="">
<div class="search-item-content">
<?php $result_rarity_class = scitemcustom_rarity_class($result['cl_scobjs_rarity'] ?? ''); ?>
<strong class="item-name <?php echo htmlspecialchars($result_rarity_class, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($result['cl_scobjs_name'], ENT_QUOTES, 'UTF-8'); ?></strong>
<div class="item-submeta">
<?php echo htmlspecialchars($result['cl_scobjs_type'], ENT_QUOTES, 'UTF-8'); ?>
<?php if (!empty($result['cl_scobjs_subtype'])): ?> / <?php echo htmlspecialchars($result['cl_scobjs_subtype'], ENT_QUOTES, 'UTF-8'); ?><?php endif; ?>
</div>
<form method="post" class="search-item-actions">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="add_custom_item">
<input type="hidden" name="obj_id" value="<?php echo (int) $result['cl_scobjs_id']; ?>">
<button type="submit" class="btn-modern">Ajouter</button>
</form>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
<section class="glass-card">
<h2>Rappel</h2>
<div class="form-help">
<strong class="muted">Fonctionnement :</strong><br>
1. Ajoute un objet à la liste.<br>
2. Sélectionne une stat créée dans <em>Stats Item</em>.<br>
3. Choisis le signe <strong>+</strong> ou <strong>-</strong>.<br>
4. Saisis la valeur. L'unité s'applique automatiquement selon la stat choisie.
</div>
<div class="form-help" style="margin-top:1rem;">
Exemple : <strong>Puissance</strong> + <strong>10</strong> avec l'unité <strong>%</strong> donnera automatiquement <strong>+10%</strong>.
</div>
</section>
</aside>
<main class="stack">
<section class="glass-card">
<h2>Liste Item Custom</h2>
<p class="count-note"><?php echo count($custom_items); ?> objet(s) configuré(s) dans cette liste personnalisée.</p>
<?php if (empty($custom_items)): ?>
<div class="empty-state">Aucun objet Item Custom enregistré pour le moment.</div>
<?php else: ?>
<div class="list-filter-bar">
<input
type="text"
id="item-custom-filter"
class="form-control list-filter-input"
placeholder="Filtrer les objets déjà ajoutés par nom, type, sous-type ou UUID..."
autocomplete="off"
>
<button type="button" id="item-custom-filter-reset" class="btn-modern danger">Effacer</button>
<div class="list-filter-meta">
Affichage : <span id="item-custom-visible-count"><?php echo count($custom_items); ?></span> / <?php echo count($custom_items); ?>
</div>
</div>
<div class="form-help" style="margin-top:-0.25rem; margin-bottom:1rem;">Tape quelques lettres pour retrouver rapidement un objet déjà présent dans ta liste Item Custom.</div>
<div id="item-custom-no-results" class="empty-state hidden-by-filter">Aucun objet Item Custom ne correspond à cette recherche.</div>
<div class="item-card-list" id="item-custom-list">
<?php foreach ($custom_items as $item): ?>
<?php $item_id = (int) $item['cl_scitemcustom_id']; ?>
<?php $item_stats = $custom_stats_by_item[$item_id] ?? []; ?>
<?php $item_stats_count = count($item_stats); ?>
<?php $item_search_text = trim(implode(' ', array_filter([
(string) ($item['cl_scobjs_name'] ?? ''),
(string) ($item['cl_scobjs_type'] ?? ''),
(string) ($item['cl_scobjs_subtype'] ?? ''),
(string) ($item['cl_scobjs_uuid'] ?? ''),
]))); ?>
<section class="custom-item-card" id="itemcustom-<?php echo $item_id; ?>" data-item-search="<?php echo htmlspecialchars($item_search_text, ENT_QUOTES, 'UTF-8'); ?>">
<div class="custom-item-summary">
<div class="custom-item-header">
<div class="item-meta">
<img src="https://cstone.space/uifimages/<?php echo htmlspecialchars($item['cl_scobjs_uuid'], ENT_QUOTES, 'UTF-8'); ?>.png" class="item-preview" alt="">
<div>
<?php $item_rarity_class = scitemcustom_rarity_class($item['cl_scobjs_rarity'] ?? ''); ?>
<?php $item_rarity_label = scitemcustom_rarity_label($item['cl_scobjs_rarity'] ?? ''); ?>
<strong class="item-name <?php echo htmlspecialchars($item_rarity_class, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item['cl_scobjs_name'], ENT_QUOTES, 'UTF-8'); ?></strong>
<div class="item-submeta">
<?php echo htmlspecialchars($item['cl_scobjs_type'], ENT_QUOTES, 'UTF-8'); ?>
<?php if (!empty($item['cl_scobjs_subtype'])): ?> / <?php echo htmlspecialchars($item['cl_scobjs_subtype'], ENT_QUOTES, 'UTF-8'); ?><?php endif; ?>
<?php if ($item_rarity_label !== ''): ?> — Rareté : <?php echo htmlspecialchars($item_rarity_label, ENT_QUOTES, 'UTF-8'); ?><?php endif; ?><br>
UUID: <?php echo htmlspecialchars($item['cl_scobjs_uuid'], ENT_QUOTES, 'UTF-8'); ?>
</div>
</div>
</div>
<span class="badge custom-item-stats-count"><?php echo $item_stats_count; ?> stat<?php echo $item_stats_count > 1 ? 's' : ''; ?></span>
</div>
<div class="custom-item-summary-actions">
<button
type="button"
class="item-toggle-btn"
aria-expanded="false"
aria-controls="itemcustom-panel-<?php echo $item_id; ?>"
data-toggle-label-open="Replier"
data-toggle-label-closed="Déplier"
>
<span class="item-toggle-label">Déplier</span>
<span class="item-toggle-icon" aria-hidden="true">▾</span>
</button>
<form method="post" onsubmit="return confirm('Supprimer cet objet Item Custom et toutes ses stats ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_custom_item">
<input type="hidden" name="itemcustom_id" value="<?php echo $item_id; ?>">
<button type="submit" class="btn-modern danger">Supprimer l'objet</button>
</form>
</div>
</div>
<div class="custom-item-body" id="itemcustom-panel-<?php echo $item_id; ?>" hidden>
<div>
<h3>Ajouter un bonus / malus</h3>
<?php if (empty($stats_catalog)): ?>
<div class="form-help">Aucune statistique disponible. Crée d'abord des entrées dans l'onglet <strong>Stats Item</strong>.</div>
<?php else: ?>
<form method="post" class="inline-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="add_custom_stat">
<input type="hidden" name="itemcustom_id" value="<?php echo $item_id; ?>">
<div class="form-group" style="margin-bottom:0;">
<label>Statistique</label>
<select name="stat_id" class="form-control" required>
<?php foreach ($stats_catalog as $stat_option): ?>
<option value="<?php echo (int) $stat_option['cl_scstatsitem_id']; ?>">
<?php echo htmlspecialchars($stat_option['cl_scstatsitem_name'], ENT_QUOTES, 'UTF-8'); ?> (<?php echo htmlspecialchars($stat_option['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom:0;">
<label>Signe</label>
<select name="sign" class="form-control">
<?php foreach ($allowed_signs as $sign_option): ?>
<option value="<?php echo htmlspecialchars($sign_option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $sign_option === '+' ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($sign_labels[$sign_option], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom:0;">
<label>Valeur</label>
<input type="number" name="value" class="form-control" min="0" step="0.01" value="0" required>
</div>
<button type="submit" class="btn-modern">Ajouter la stat</button>
</form>
<?php endif; ?>
</div>
<div style="overflow-x:auto;">
<table class="modern-table">
<thead>
<tr>
<th>Statistique</th>
<th style="width:110px;">Signe</th>
<th style="width:140px;">Valeur</th>
<th style="width:120px;">Unité</th>
<th style="width:130px;">Aperçu</th>
<th style="text-align:right; width:220px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($item_stats)): ?>
<tr>
<td colspan="6" class="empty-state">Aucune statistique configurée pour cet objet.</td>
</tr>
<?php else: ?>
<?php foreach ($item_stats as $item_stat): ?>
<tr>
<td>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="update_custom_stat">
<input type="hidden" name="itemcustom_id" value="<?php echo $item_id; ?>">
<input type="hidden" name="custom_stat_id" value="<?php echo (int) $item_stat['cl_scitemcustomstat_id']; ?>">
<select name="stat_id" class="form-control" required>
<?php foreach ($stats_catalog as $stat_option): ?>
<option value="<?php echo (int) $stat_option['cl_scstatsitem_id']; ?>" <?php echo (int) $stat_option['cl_scstatsitem_id'] === (int) $item_stat['cl_scitemcustomstat_stat_id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($stat_option['cl_scstatsitem_name'], ENT_QUOTES, 'UTF-8'); ?> (<?php echo htmlspecialchars($stat_option['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?>)
</option>
<?php endforeach; ?>
</select>
</td>
<td>
<select name="sign" class="form-control">
<?php foreach ($allowed_signs as $sign_option): ?>
<option value="<?php echo htmlspecialchars($sign_option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $sign_option === $item_stat['cl_scitemcustomstat_sign'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($sign_labels[$sign_option], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td>
<input type="number" name="value" class="form-control" min="0" step="0.01" value="<?php echo htmlspecialchars(scitemcustom_display_value($item_stat['cl_scitemcustomstat_value']), ENT_QUOTES, 'UTF-8'); ?>" required>
</td>
<td>
<span class="badge"><?php echo htmlspecialchars($item_stat['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td>
<span class="preview-pill"><?php echo htmlspecialchars(scitemcustom_preview($item_stat['cl_scitemcustomstat_sign'], $item_stat['cl_scitemcustomstat_value'], $item_stat['cl_scstatsitem_unit']), ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td style="text-align:right;">
<div class="actions-row">
<button type="submit" class="btn-modern btn-mini">Save</button>
</form>
<form method="post" onsubmit="return confirm('Supprimer cette statistique ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_custom_stat">
<input type="hidden" name="itemcustom_id" value="<?php echo $item_id; ?>">
<input type="hidden" name="custom_stat_id" value="<?php echo (int) $item_stat['cl_scitemcustomstat_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</section>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var filterInput = document.getElementById('item-custom-filter');
var resetButton = document.getElementById('item-custom-filter-reset');
var visibleCount = document.getElementById('item-custom-visible-count');
var emptyState = document.getElementById('item-custom-no-results');
var itemCards = Array.prototype.slice.call(document.querySelectorAll('#item-custom-list .custom-item-card'));
var addForm = document.getElementById('item-add-form');
var addSearchInput = document.getElementById('item-add-search');
var addSelectedIdInput = document.getElementById('item-add-selected-id');
var addDropdown = document.getElementById('item-add-suggestions');
var addSelection = document.getElementById('item-add-selection');
var addSelectionImage = document.getElementById('item-add-selection-image');
var addSelectionName = document.getElementById('item-add-selection-name');
var addSelectionMeta = document.getElementById('item-add-selection-meta');
var addSubmit = document.getElementById('item-add-submit');
var addReset = document.getElementById('item-add-reset');
var addClear = document.getElementById('item-add-clear');
var pickerRarityClasses = ['rarity-L', 'rarity-E', 'rarity-R', 'rarity-U', 'rarity-C'];
var pickerRequestTimer = null;
var pickerRequestToken = 0;
var selectedItem = null;
function normalizeValue(value) {
return (value || '')
.toString()
.toLowerCase()
.normalize('NFD')
.replace(/[̀-ͯ]/g, '')
.trim();
}
function escapeHtml(value) {
return (value || '')
.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function setPickerExpanded(isExpanded) {
if (addSearchInput) {
addSearchInput.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
}
}
function hidePickerDropdown() {
if (!addDropdown) {
return;
}
addDropdown.innerHTML = '';
addDropdown.classList.add('hidden-by-filter');
setPickerExpanded(false);
}
function showPickerDropdown() {
if (!addDropdown) {
return;
}
addDropdown.classList.remove('hidden-by-filter');
setPickerExpanded(true);
}
function applyRarityClass(element, rarityClass) {
if (!element) {
return;
}
pickerRarityClasses.forEach(function (className) {
element.classList.remove(className);
});
if (rarityClass && pickerRarityClasses.indexOf(rarityClass) !== -1) {
element.classList.add(rarityClass);
}
}
function updateSelectedItem(item) {
selectedItem = item || null;
if (!addSelectedIdInput || !addSubmit || !addSelection || !addSelectionName || !addSelectionMeta || !addSelectionImage) {
return;
}
if (!selectedItem) {
addSelectedIdInput.value = '';
addSubmit.disabled = true;
addSelection.classList.add('hidden-by-filter');
addSelectionName.textContent = '';
addSelectionMeta.textContent = '';
applyRarityClass(addSelectionName, '');
addSelectionImage.classList.add('hidden-by-filter');
addSelectionImage.setAttribute('src', '');
addSelectionImage.setAttribute('alt', '');
return;
}
addSelectedIdInput.value = String(selectedItem.id || '');
addSubmit.disabled = !addSelectedIdInput.value;
addSelection.classList.remove('hidden-by-filter');
addSelectionName.textContent = selectedItem.name || '';
applyRarityClass(addSelectionName, selectedItem.rarityClass || '');
var meta = selectedItem.type || '';
if (selectedItem.subtype) {
meta += (meta ? ' / ' : '') + selectedItem.subtype;
}
if (selectedItem.uuid) {
meta += (meta ? ' — ' : '') + selectedItem.uuid;
}
if (selectedItem.rarityLabel) {
meta += (meta ? ' — ' : '') + 'Rareté : ' + selectedItem.rarityLabel;
}
addSelectionMeta.textContent = meta;
if (selectedItem.uuid) {
addSelectionImage.classList.remove('hidden-by-filter');
addSelectionImage.setAttribute('src', 'https://cstone.space/uifimages/' + encodeURIComponent(selectedItem.uuid) + '.png');
addSelectionImage.setAttribute('alt', selectedItem.name || 'Objet sélectionné');
} else {
addSelectionImage.classList.add('hidden-by-filter');
addSelectionImage.setAttribute('src', '');
addSelectionImage.setAttribute('alt', '');
}
}
function renderPickerItems(items, query) {
if (!addDropdown) {
return;
}
if (!Array.isArray(items) || items.length === 0) {
addDropdown.innerHTML = '\n <div class="item-picker-option item-picker-option-empty" role="option" aria-disabled="true">\n <div class="search-item-content">\n <strong class="item-name">Aucun objet trouvé</strong>\n <div class="item-submeta">Aucun objet disponible pour “' + escapeHtml(query) + '”.</div>\n </div>\n </div>';
showPickerDropdown();
return;
}
addDropdown.innerHTML = items.map(function (item, index) {
var meta = escapeHtml(item.type || '');
if (item.subtype) {
meta += (meta ? ' / ' : '') + escapeHtml(item.subtype);
}
var rarityClass = item.rarity_class || '';
var rarityLabel = item.rarity_label || '';
if (rarityLabel) {
meta += (meta ? ' — ' : '') + 'Rareté : ' + escapeHtml(rarityLabel);
}
return '' +
'<button type="button" class="item-picker-option' + (index === 0 ? ' is-active' : '') + '" role="option" data-item-id="' + escapeHtml(item.id) + '" data-item-name="' + escapeHtml(item.name) + '" data-item-type="' + escapeHtml(item.type || '') + '" data-item-subtype="' + escapeHtml(item.subtype || '') + '" data-item-uuid="' + escapeHtml(item.uuid || '') + '" data-item-rarity="' + escapeHtml(item.rarity || '') + '" data-item-rarity-class="' + escapeHtml(rarityClass) + '" data-item-rarity-label="' + escapeHtml(rarityLabel) + '">' +
'<div class="search-item-content">' +
'<strong class="item-name' + (rarityClass ? ' ' + escapeHtml(rarityClass) : '') + '">' + escapeHtml(item.name || '') + '</strong>' +
'<div class="item-submeta">' + meta + '</div>' +
'</div>' +
'</button>';
}).join('');
showPickerDropdown();
}
function fetchPickerSuggestions(query) {
if (!addDropdown) {
return;
}
var trimmedQuery = (query || '').trim();
if (trimmedQuery.length < 3) {
hidePickerDropdown();
return;
}
var requestToken = ++pickerRequestToken;
addDropdown.innerHTML = '\n <div class="item-picker-option item-picker-option-empty" role="option" aria-disabled="true">\n <div class="search-item-content">\n <strong class="item-name">Recherche en cours...</strong>\n <div class="item-submeta">Chargement des objets disponibles.</div>\n </div>\n </div>';
showPickerDropdown();
fetch('scitemcustom.php?ajax=item_suggestions&q=' + encodeURIComponent(trimmedQuery), {
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
})
.then(function (response) {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
})
.then(function (payload) {
if (requestToken !== pickerRequestToken) {
return;
}
renderPickerItems(payload && Array.isArray(payload.items) ? payload.items : [], trimmedQuery);
})
.catch(function () {
if (requestToken !== pickerRequestToken) {
return;
}
addDropdown.innerHTML = '\n <div class="item-picker-option item-picker-option-empty" role="option" aria-disabled="true">\n <div class="search-item-content">\n <strong class="item-name">Recherche indisponible</strong>\n <div class="item-submeta">Impossible de charger les suggestions pour le moment.</div>\n </div>\n </div>';
showPickerDropdown();
});
}
function clearSelection(options) {
var settings = options || {};
updateSelectedItem(null);
if (!settings.keepInput && addSearchInput) {
addSearchInput.value = '';
}
}
if (addForm && addSearchInput && addSelectedIdInput && addDropdown && addSelection && addSubmit && addReset && addClear) {
addSearchInput.addEventListener('input', function () {
clearSelection({ keepInput: true });
var query = addSearchInput.value || '';
window.clearTimeout(pickerRequestTimer);
pickerRequestTimer = window.setTimeout(function () {
fetchPickerSuggestions(query);
}, 120);
});
addSearchInput.addEventListener('focus', function () {
if ((addSearchInput.value || '').trim() !== '' && addDropdown.innerHTML.trim() !== '') {
showPickerDropdown();
}
});
addSearchInput.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
hidePickerDropdown();
}
});
addDropdown.addEventListener('click', function (event) {
var option = event.target.closest('.item-picker-option[data-item-id]');
if (!option) {
return;
}
var item = {
id: option.getAttribute('data-item-id') || '',
name: option.getAttribute('data-item-name') || '',
type: option.getAttribute('data-item-type') || '',
subtype: option.getAttribute('data-item-subtype') || '',
uuid: option.getAttribute('data-item-uuid') || '',
rarity: option.getAttribute('data-item-rarity') || '',
rarityClass: option.getAttribute('data-item-rarity-class') || '',
rarityLabel: option.getAttribute('data-item-rarity-label') || ''
};
addSearchInput.value = item.name || '';
updateSelectedItem(item);
hidePickerDropdown();
});
document.addEventListener('click', function (event) {
if (!event.target.closest('#item-add-form')) {
hidePickerDropdown();
}
});
addReset.addEventListener('click', function () {
window.clearTimeout(pickerRequestTimer);
pickerRequestToken += 1;
clearSelection();
hidePickerDropdown();
addSearchInput.focus();
});
addClear.addEventListener('click', function () {
clearSelection();
hidePickerDropdown();
addSearchInput.focus();
});
addForm.addEventListener('submit', function (event) {
if (!addSelectedIdInput.value) {
event.preventDefault();
addSearchInput.focus();
}
});
}
function setCardState(card, isOpen) {
var body = card.querySelector('.custom-item-body');
var toggleButton = card.querySelector('.item-toggle-btn');
var toggleLabel = toggleButton ? toggleButton.querySelector('.item-toggle-label') : null;
if (!body || !toggleButton) {
return;
}
card.classList.toggle('is-open', isOpen);
body.hidden = !isOpen;
toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
if (toggleLabel) {
toggleLabel.textContent = isOpen
? (toggleButton.getAttribute('data-toggle-label-open') || 'Replier')
: (toggleButton.getAttribute('data-toggle-label-closed') || 'Déplier');
}
}
function closeAllCards() {
itemCards.forEach(function (card) {
setCardState(card, false);
});
}
function getCurrentUrlWithoutHash() {
return window.location.pathname + window.location.search;
}
function openCard(card, options) {
var settings = options || {};
itemCards.forEach(function (otherCard) {
setCardState(otherCard, otherCard === card);
});
if (settings.syncHash !== false) {
window.history.replaceState(null, '', '#' + card.id);
}
if (settings.scrollIntoView) {
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function closeCard(card, options) {
var settings = options || {};
setCardState(card, false);
if (settings.syncHash !== false && window.location.hash === '#' + card.id) {
window.history.replaceState(null, '', getCurrentUrlWithoutHash());
}
}
function syncFromHash() {
var hash = window.location.hash || '';
if (hash.indexOf('#itemcustom-') !== 0) {
closeAllCards();
return;
}
var targetCard = document.querySelector(hash + '.custom-item-card');
if (!targetCard) {
closeAllCards();
return;
}
openCard(targetCard, { syncHash: false });
}
itemCards.forEach(function (card) {
var toggleButton = card.querySelector('.item-toggle-btn');
if (!toggleButton) {
return;
}
setCardState(card, false);
toggleButton.addEventListener('click', function () {
var isOpen = toggleButton.getAttribute('aria-expanded') === 'true';
if (isOpen) {
closeCard(card);
} else {
openCard(card, { scrollIntoView: true });
}
});
});
if (itemCards.length > 0) {
syncFromHash();
window.addEventListener('hashchange', syncFromHash);
}
if (!filterInput || !resetButton || !visibleCount || !emptyState || itemCards.length === 0) {
return;
}
function applyFilter() {
var query = normalizeValue(filterInput.value);
var matches = 0;
itemCards.forEach(function (card) {
var searchable = normalizeValue(card.getAttribute('data-item-search'));
var isMatch = query === '' || searchable.indexOf(query) !== -1;
card.classList.toggle('hidden-by-filter', !isMatch);
if (isMatch) {
matches += 1;
}
});
visibleCount.textContent = String(matches);
emptyState.classList.toggle('hidden-by-filter', matches !== 0);
}
filterInput.addEventListener('input', applyFilter);
resetButton.addEventListener('click', function () {
filterInput.value = '';
filterInput.focus();
applyFilter();
});
});
</script>
</body>
</html>