3627 lines
158 KiB
PHP
3627 lines
158 KiB
PHP
<?php
|
||
|
||
require_once __DIR__ . '/db/auth.php';
|
||
require_once __DIR__ . '/db/scstatsitem.php';
|
||
require_once __DIR__ . '/db/scitemcustom.php';
|
||
require_once __DIR__ . '/db/sccharacters.php';
|
||
|
||
auth_start_session();
|
||
auth_bootstrap();
|
||
auth_handle_page_access_post('sccharacters.php', 'Personnages');
|
||
auth_require_page_access('sccharacters.php', 'Personnages');
|
||
scstatsitem_bootstrap();
|
||
scitemcustom_bootstrap();
|
||
sccharacters_bootstrap();
|
||
|
||
function sccharacters_current_owner_auth_id(PDO $db): int
|
||
{
|
||
$session_user = auth_current_user();
|
||
if ($session_user === '') {
|
||
return 0;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'SELECT cl_auth_id
|
||
FROM tbl_auth
|
||
WHERE cl_auth_user = :user
|
||
LIMIT 1'
|
||
);
|
||
$stmt->execute(['user' => $session_user]);
|
||
|
||
return (int) $stmt->fetchColumn();
|
||
}
|
||
|
||
function sccharacters_clean_text(?string $value): string
|
||
{
|
||
return trim((string) $value);
|
||
}
|
||
|
||
function sccharacters_normalize_item_quantity($value): ?int
|
||
{
|
||
if ($value === null) {
|
||
return null;
|
||
}
|
||
|
||
$value = trim((string) $value);
|
||
if ($value === '' || !preg_match('/^\d+$/', $value)) {
|
||
return null;
|
||
}
|
||
|
||
$quantity = (int) $value;
|
||
|
||
if ($quantity <= 0) {
|
||
return null;
|
||
}
|
||
|
||
return min($quantity, 999999);
|
||
}
|
||
|
||
function sccharacters_is_valid_url(string $value): bool
|
||
{
|
||
if ($value === '') {
|
||
return true;
|
||
}
|
||
|
||
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||
}
|
||
|
||
function sccharacters_excerpt(string $value, int $limit = 120): string
|
||
{
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
if (function_exists('mb_strimwidth')) {
|
||
return mb_strimwidth($value, 0, $limit, '…', 'UTF-8');
|
||
}
|
||
|
||
if (strlen($value) <= $limit) {
|
||
return $value;
|
||
}
|
||
|
||
return rtrim(substr($value, 0, max(0, $limit - 1))) . '…';
|
||
}
|
||
|
||
function sccharacters_share_url(string $token): string
|
||
{
|
||
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
|
||
|| ((string) ($_SERVER['SERVER_PORT'] ?? '') === '443');
|
||
|
||
$scheme = $is_https ? 'https' : 'http';
|
||
$host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
|
||
|
||
if ($host === '') {
|
||
$host = '127.0.0.1';
|
||
}
|
||
|
||
return $scheme . '://' . $host . '/sccharacter.php?share=' . rawurlencode($token);
|
||
}
|
||
|
||
function sccharacters_find_owned_character(PDO $db, int $character_id, int $owner_auth_id): ?array
|
||
{
|
||
if ($character_id <= 0 || $owner_auth_id <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'SELECT *
|
||
FROM tbl_sccharacters
|
||
WHERE cl_sccharacter_id = :id
|
||
AND cl_sccharacter_owner_auth_id = :owner_auth_id
|
||
LIMIT 1'
|
||
);
|
||
$stmt->execute([
|
||
'id' => $character_id,
|
||
'owner_auth_id' => $owner_auth_id,
|
||
]);
|
||
|
||
$row = $stmt->fetch();
|
||
|
||
return $row ?: null;
|
||
}
|
||
|
||
function sccharacters_allowed_item_per_page(): array
|
||
{
|
||
return [25, 50, 100, 200];
|
||
}
|
||
|
||
function sccharacters_normalize_item_per_page($value): int
|
||
{
|
||
$per_page = (int) $value;
|
||
$allowed = sccharacters_allowed_item_per_page();
|
||
|
||
return in_array($per_page, $allowed, true) ? $per_page : 50;
|
||
}
|
||
|
||
function sccharacters_build_return_url(
|
||
int $character_id = 0,
|
||
string $item_source = '',
|
||
string $item_search = '',
|
||
bool $keep_item_panel = false,
|
||
int $item_page = 1,
|
||
int $item_per_page = 50
|
||
): string {
|
||
$params = [];
|
||
|
||
if ($character_id > 0) {
|
||
$params['character'] = $character_id;
|
||
}
|
||
|
||
if (in_array($item_source, ['base', 'custom'], true)) {
|
||
$params['item_source'] = $item_source;
|
||
}
|
||
|
||
if ($item_search !== '') {
|
||
$params['item_search'] = $item_search;
|
||
}
|
||
|
||
$item_page = max(1, $item_page);
|
||
if ($item_page > 1) {
|
||
$params['item_page'] = $item_page;
|
||
}
|
||
|
||
$item_per_page = sccharacters_normalize_item_per_page($item_per_page);
|
||
if ($item_per_page !== 50) {
|
||
$params['item_per_page'] = $item_per_page;
|
||
}
|
||
|
||
if ($keep_item_panel) {
|
||
$params['item_panel'] = '1';
|
||
}
|
||
|
||
return 'sccharacters.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||
}
|
||
|
||
function sccharacters_staged_items_session_key(int $owner_auth_id, int $character_id): string
|
||
{
|
||
return $owner_auth_id . ':' . $character_id;
|
||
}
|
||
|
||
function sccharacters_get_staged_items(int $owner_auth_id, int $character_id): array
|
||
{
|
||
if ($owner_auth_id <= 0 || $character_id <= 0) {
|
||
return [];
|
||
}
|
||
|
||
if (!isset($_SESSION['sccharacters_staged_items']) || !is_array($_SESSION['sccharacters_staged_items'])) {
|
||
$_SESSION['sccharacters_staged_items'] = [];
|
||
}
|
||
|
||
$session_key = sccharacters_staged_items_session_key($owner_auth_id, $character_id);
|
||
$items = $_SESSION['sccharacters_staged_items'][$session_key] ?? [];
|
||
|
||
return is_array($items) ? $items : [];
|
||
}
|
||
|
||
function sccharacters_save_staged_items(int $owner_auth_id, int $character_id, array $items): void
|
||
{
|
||
if (!isset($_SESSION['sccharacters_staged_items']) || !is_array($_SESSION['sccharacters_staged_items'])) {
|
||
$_SESSION['sccharacters_staged_items'] = [];
|
||
}
|
||
|
||
$session_key = sccharacters_staged_items_session_key($owner_auth_id, $character_id);
|
||
|
||
if ($owner_auth_id <= 0 || $character_id <= 0 || $items === []) {
|
||
unset($_SESSION['sccharacters_staged_items'][$session_key]);
|
||
return;
|
||
}
|
||
|
||
$_SESSION['sccharacters_staged_items'][$session_key] = $items;
|
||
}
|
||
|
||
function sccharacters_parse_item_key(string $item_key): ?array
|
||
{
|
||
if (!preg_match('/^(base|custom):(\d+)$/', $item_key, $matches)) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'source' => (string) $matches[1],
|
||
'source_id' => (int) $matches[2],
|
||
];
|
||
}
|
||
|
||
function sccharacters_merge_selected_items(array $staged_items, array $selected_items, array $item_slots, array $item_notes, array $item_quantities = []): array
|
||
{
|
||
$selected_items = array_values(array_unique(array_map('strval', $selected_items)));
|
||
$staged_count = 0;
|
||
$error_count = 0;
|
||
|
||
foreach ($selected_items as $item_key) {
|
||
$parsed = sccharacters_parse_item_key($item_key);
|
||
if (!$parsed) {
|
||
$error_count++;
|
||
continue;
|
||
}
|
||
|
||
$staged_items[$item_key] = [
|
||
'source' => $parsed['source'],
|
||
'source_id' => $parsed['source_id'],
|
||
'category' => sccharacters_clean_text($item_slots[$item_key] ?? ''),
|
||
'note' => sccharacters_clean_text($item_notes[$item_key] ?? ''),
|
||
'quantity' => sccharacters_normalize_item_quantity($item_quantities[$item_key] ?? null),
|
||
];
|
||
$staged_count++;
|
||
}
|
||
|
||
return [
|
||
'items' => $staged_items,
|
||
'staged_count' => $staged_count,
|
||
'error_count' => $error_count,
|
||
];
|
||
}
|
||
|
||
function sccharacters_fetch_staged_item_details(PDO $db, int $owner_auth_id, array $staged_items): array
|
||
{
|
||
$details = [];
|
||
$base_ids = [];
|
||
$custom_ids = [];
|
||
|
||
foreach ($staged_items as $item_key => $item_data) {
|
||
$parsed = sccharacters_parse_item_key((string) $item_key);
|
||
if (!$parsed) {
|
||
continue;
|
||
}
|
||
|
||
if ($parsed['source'] === 'custom') {
|
||
$custom_ids[] = $parsed['source_id'];
|
||
} else {
|
||
$base_ids[] = $parsed['source_id'];
|
||
}
|
||
}
|
||
|
||
$base_ids = array_values(array_unique(array_filter($base_ids)));
|
||
$custom_ids = array_values(array_unique(array_filter($custom_ids)));
|
||
|
||
if ($base_ids !== []) {
|
||
$placeholders = implode(',', array_fill(0, count($base_ids), '?'));
|
||
$stmt = $db->prepare(
|
||
"SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid
|
||
FROM tbl_scobjs
|
||
WHERE cl_scobjs_id IN ({$placeholders})"
|
||
);
|
||
$stmt->execute($base_ids);
|
||
|
||
foreach ($stmt->fetchAll() as $row) {
|
||
$item_key = 'base:' . (int) $row['cl_scobjs_id'];
|
||
$details[$item_key] = [
|
||
'item_key' => $item_key,
|
||
'source' => 'base',
|
||
'source_id' => (int) $row['cl_scobjs_id'],
|
||
'name' => (string) ($row['cl_scobjs_name'] ?? ''),
|
||
'type' => (string) ($row['cl_scobjs_type'] ?? ''),
|
||
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
|
||
'uuid' => (string) ($row['cl_scobjs_uuid'] ?? ''),
|
||
'is_custom' => false,
|
||
'stat_count' => null,
|
||
];
|
||
}
|
||
}
|
||
|
||
if ($custom_ids !== []) {
|
||
$placeholders = implode(',', array_fill(0, count($custom_ids), '?'));
|
||
$stmt = $db->prepare(
|
||
"SELECT c.cl_scitemcustom_id,
|
||
o.cl_scobjs_name,
|
||
o.cl_scobjs_type,
|
||
o.cl_scobjs_subtype,
|
||
o.cl_scobjs_uuid,
|
||
COUNT(cs.cl_scitemcustomstat_id) AS cl_scitemcustom_stat_count
|
||
FROM tbl_scitemcustom c
|
||
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
|
||
LEFT JOIN tbl_scitemcustomstat cs ON cs.cl_scitemcustomstat_itemcustom_id = c.cl_scitemcustom_id
|
||
WHERE c.cl_scitemcustom_owner_auth_id = ?
|
||
AND c.cl_scitemcustom_id IN ({$placeholders})
|
||
GROUP BY c.cl_scitemcustom_id"
|
||
);
|
||
$stmt->execute(array_merge([$owner_auth_id], $custom_ids));
|
||
|
||
foreach ($stmt->fetchAll() as $row) {
|
||
$item_key = 'custom:' . (int) $row['cl_scitemcustom_id'];
|
||
$details[$item_key] = [
|
||
'item_key' => $item_key,
|
||
'source' => 'custom',
|
||
'source_id' => (int) $row['cl_scitemcustom_id'],
|
||
'name' => (string) ($row['cl_scobjs_name'] ?? ''),
|
||
'type' => (string) ($row['cl_scobjs_type'] ?? ''),
|
||
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
|
||
'uuid' => (string) ($row['cl_scobjs_uuid'] ?? ''),
|
||
'is_custom' => true,
|
||
'stat_count' => (int) ($row['cl_scitemcustom_stat_count'] ?? 0),
|
||
];
|
||
}
|
||
}
|
||
|
||
return $details;
|
||
}
|
||
|
||
function sccharacters_fetch_owned_character_item_ids(PDO $db, int $owner_auth_id, int $character_id): array
|
||
{
|
||
if ($character_id <= 0 || $owner_auth_id <= 0) {
|
||
return [];
|
||
}
|
||
|
||
sccharacters_reindex_character_items($db, $character_id);
|
||
|
||
$stmt = $db->prepare(
|
||
'SELECT ci.cl_sccharacteritem_id
|
||
FROM tbl_sccharacteritems ci
|
||
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
|
||
WHERE ci.cl_sccharacteritem_character_id = :character_id
|
||
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id
|
||
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC'
|
||
);
|
||
$stmt->execute([
|
||
'character_id' => $character_id,
|
||
'owner_auth_id' => $owner_auth_id,
|
||
]);
|
||
|
||
return array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||
}
|
||
|
||
function sccharacters_save_character_item_order(PDO $db, int $character_id, array $ordered_item_ids): void
|
||
{
|
||
if ($character_id <= 0 || $ordered_item_ids === []) {
|
||
return;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'UPDATE tbl_sccharacteritems
|
||
SET cl_sccharacteritem_sort_order = :sort_order
|
||
WHERE cl_sccharacteritem_character_id = :character_id
|
||
AND cl_sccharacteritem_id = :item_id'
|
||
);
|
||
|
||
$db->beginTransaction();
|
||
|
||
try {
|
||
$position = 1;
|
||
foreach ($ordered_item_ids as $item_id) {
|
||
$stmt->execute([
|
||
'sort_order' => $position,
|
||
'character_id' => $character_id,
|
||
'item_id' => (int) $item_id,
|
||
]);
|
||
$position++;
|
||
}
|
||
|
||
$db->commit();
|
||
} catch (Throwable $exception) {
|
||
if ($db->inTransaction()) {
|
||
$db->rollBack();
|
||
}
|
||
|
||
throw $exception;
|
||
}
|
||
}
|
||
|
||
function sccharacters_move_owned_character_item(
|
||
PDO $db,
|
||
int $owner_auth_id,
|
||
int $character_id,
|
||
int $character_item_id,
|
||
string $direction
|
||
): bool {
|
||
$ordered_item_ids = sccharacters_fetch_owned_character_item_ids($db, $owner_auth_id, $character_id);
|
||
if ($ordered_item_ids === []) {
|
||
return false;
|
||
}
|
||
|
||
$current_index = array_search($character_item_id, $ordered_item_ids, true);
|
||
if ($current_index === false) {
|
||
return false;
|
||
}
|
||
|
||
$target_index = $current_index;
|
||
$last_index = count($ordered_item_ids) - 1;
|
||
|
||
switch ($direction) {
|
||
case 'top':
|
||
$target_index = 0;
|
||
break;
|
||
case 'up':
|
||
$target_index = max(0, $current_index - 1);
|
||
break;
|
||
case 'down':
|
||
$target_index = min($last_index, $current_index + 1);
|
||
break;
|
||
case 'bottom':
|
||
$target_index = $last_index;
|
||
break;
|
||
default:
|
||
return false;
|
||
}
|
||
|
||
if ($target_index === $current_index) {
|
||
return true;
|
||
}
|
||
|
||
$moving_item_id = $ordered_item_ids[$current_index];
|
||
array_splice($ordered_item_ids, $current_index, 1);
|
||
array_splice($ordered_item_ids, $target_index, 0, [$moving_item_id]);
|
||
|
||
sccharacters_save_character_item_order($db, $character_id, $ordered_item_ids);
|
||
|
||
return true;
|
||
}
|
||
|
||
function sccharacters_fetch_owned_character_item_rows(PDO $db, int $owner_auth_id, int $character_id): array
|
||
{
|
||
if ($character_id <= 0 || $owner_auth_id <= 0) {
|
||
return [];
|
||
}
|
||
|
||
sccharacters_reindex_character_items($db, $character_id);
|
||
|
||
$stmt = $db->prepare(
|
||
"SELECT
|
||
ci.cl_sccharacteritem_id,
|
||
ci.cl_sccharacteritem_slot,
|
||
ci.cl_sccharacteritem_source,
|
||
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
|
||
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
|
||
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
|
||
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype
|
||
FROM tbl_sccharacteritems ci
|
||
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
|
||
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
|
||
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
|
||
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
|
||
WHERE ci.cl_sccharacteritem_character_id = :character_id
|
||
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id
|
||
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC"
|
||
);
|
||
$stmt->execute([
|
||
'character_id' => $character_id,
|
||
'owner_auth_id' => $owner_auth_id,
|
||
]);
|
||
|
||
return $stmt->fetchAll();
|
||
}
|
||
|
||
function sccharacters_group_item_ids_by_category(array $item_rows): array
|
||
{
|
||
$grouped = [];
|
||
|
||
foreach ($item_rows as $item_row) {
|
||
$is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
|
||
$item_type = $is_custom
|
||
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
|
||
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
|
||
$item_subtype = $is_custom
|
||
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
|
||
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
|
||
$category_key = sccharacters_resolve_item_category(
|
||
(string) ($item_row['cl_sccharacteritem_slot'] ?? ''),
|
||
$item_type,
|
||
$item_subtype
|
||
);
|
||
|
||
if (!isset($grouped[$category_key])) {
|
||
$grouped[$category_key] = [];
|
||
}
|
||
|
||
$grouped[$category_key][] = (int) ($item_row['cl_sccharacteritem_id'] ?? 0);
|
||
}
|
||
|
||
return $grouped;
|
||
}
|
||
|
||
function sccharacters_flatten_grouped_item_ids(array $grouped_item_ids, array $category_order): array
|
||
{
|
||
$ordered_ids = [];
|
||
$sorted_groups = sccharacters_sort_items_by_category_order($grouped_item_ids, $category_order);
|
||
|
||
foreach ($sorted_groups as $category_item_ids) {
|
||
foreach ($category_item_ids as $item_id) {
|
||
$item_id = (int) $item_id;
|
||
if ($item_id > 0) {
|
||
$ordered_ids[] = $item_id;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $ordered_ids;
|
||
}
|
||
|
||
function sccharacters_move_owned_character_category(
|
||
PDO $db,
|
||
int $owner_auth_id,
|
||
int $character_id,
|
||
string $category_key,
|
||
string $direction
|
||
): bool {
|
||
$character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
|
||
if (!$character) {
|
||
return false;
|
||
}
|
||
|
||
$grouped_item_ids = sccharacters_group_item_ids_by_category(
|
||
sccharacters_fetch_owned_character_item_rows($db, $owner_auth_id, $character_id)
|
||
);
|
||
if (!isset($grouped_item_ids[$category_key])) {
|
||
return false;
|
||
}
|
||
|
||
$category_order = sccharacters_character_category_order($character);
|
||
$visible_order = [];
|
||
foreach ($category_order as $ordered_category_key) {
|
||
if (isset($grouped_item_ids[$ordered_category_key])) {
|
||
$visible_order[] = $ordered_category_key;
|
||
}
|
||
}
|
||
|
||
if ($visible_order === []) {
|
||
return false;
|
||
}
|
||
|
||
$current_index = array_search($category_key, $visible_order, true);
|
||
if ($current_index === false) {
|
||
return false;
|
||
}
|
||
|
||
$target_index = $current_index;
|
||
if ($direction === 'up') {
|
||
$target_index = max(0, $current_index - 1);
|
||
} elseif ($direction === 'down') {
|
||
$target_index = min(count($visible_order) - 1, $current_index + 1);
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
if ($target_index === $current_index) {
|
||
return true;
|
||
}
|
||
|
||
$moving_category_key = $visible_order[$current_index];
|
||
array_splice($visible_order, $current_index, 1);
|
||
array_splice($visible_order, $target_index, 0, [$moving_category_key]);
|
||
|
||
$visible_lookup = array_fill_keys(array_keys($grouped_item_ids), true);
|
||
$visible_pointer = 0;
|
||
$rebuilt_order = [];
|
||
foreach ($category_order as $ordered_category_key) {
|
||
if (isset($visible_lookup[$ordered_category_key])) {
|
||
$rebuilt_order[] = $visible_order[$visible_pointer] ?? $ordered_category_key;
|
||
$visible_pointer++;
|
||
} else {
|
||
$rebuilt_order[] = $ordered_category_key;
|
||
}
|
||
}
|
||
|
||
sccharacters_save_character_category_order($db, $character_id, $rebuilt_order);
|
||
sccharacters_save_character_item_order(
|
||
$db,
|
||
$character_id,
|
||
sccharacters_flatten_grouped_item_ids($grouped_item_ids, $rebuilt_order)
|
||
);
|
||
|
||
return true;
|
||
}
|
||
|
||
function sccharacters_reorder_owned_character_items(
|
||
PDO $db,
|
||
int $owner_auth_id,
|
||
int $character_id,
|
||
string $category_key,
|
||
array $ordered_item_ids
|
||
): bool {
|
||
$character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
|
||
if (!$character) {
|
||
return false;
|
||
}
|
||
|
||
$grouped_item_ids = sccharacters_group_item_ids_by_category(
|
||
sccharacters_fetch_owned_character_item_rows($db, $owner_auth_id, $character_id)
|
||
);
|
||
if (!isset($grouped_item_ids[$category_key])) {
|
||
return false;
|
||
}
|
||
|
||
$current_item_ids = array_map('intval', $grouped_item_ids[$category_key]);
|
||
$posted_item_ids = array_values(array_filter(array_map('intval', $ordered_item_ids), static fn ($item_id) => $item_id > 0));
|
||
|
||
sort($current_item_ids);
|
||
$sorted_posted = $posted_item_ids;
|
||
sort($sorted_posted);
|
||
|
||
if ($posted_item_ids === [] || $sorted_posted !== $current_item_ids) {
|
||
return false;
|
||
}
|
||
|
||
$grouped_item_ids[$category_key] = $posted_item_ids;
|
||
sccharacters_save_character_item_order(
|
||
$db,
|
||
$character_id,
|
||
sccharacters_flatten_grouped_item_ids($grouped_item_ids, sccharacters_character_category_order($character))
|
||
);
|
||
|
||
return true;
|
||
}
|
||
|
||
function sccharacters_attach_item(
|
||
PDO $db,
|
||
int $owner_auth_id,
|
||
int $character_id,
|
||
string $source,
|
||
int $source_id,
|
||
string $requested_category,
|
||
?int $quantity,
|
||
string $note,
|
||
?string &$error_message = null
|
||
): bool {
|
||
if ($character_id <= 0 || $owner_auth_id <= 0 || $source_id <= 0) {
|
||
$error_message = 'Paramètres d’ajout invalides.';
|
||
return false;
|
||
}
|
||
|
||
$character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
|
||
if (!$character) {
|
||
$error_message = 'Personnage introuvable.';
|
||
return false;
|
||
}
|
||
|
||
$sort_order = sccharacters_next_item_sort_order($db, $character_id);
|
||
|
||
if ($source === 'base') {
|
||
$stmt_source = $db->prepare(
|
||
'SELECT cl_scobjs_id AS item_id, cl_scobjs_type, cl_scobjs_subtype
|
||
FROM tbl_scobjs
|
||
WHERE cl_scobjs_id = :id
|
||
LIMIT 1'
|
||
);
|
||
$stmt_source->execute(['id' => $source_id]);
|
||
$item_row = $stmt_source->fetch();
|
||
|
||
if (!$item_row) {
|
||
$error_message = 'Objet de base introuvable.';
|
||
return false;
|
||
}
|
||
|
||
$category = sccharacters_resolve_item_category(
|
||
$requested_category,
|
||
(string) ($item_row['cl_scobjs_type'] ?? ''),
|
||
(string) ($item_row['cl_scobjs_subtype'] ?? '')
|
||
);
|
||
|
||
$stmt_insert = $db->prepare(
|
||
'INSERT INTO tbl_sccharacteritems (
|
||
cl_sccharacteritem_character_id,
|
||
cl_sccharacteritem_source,
|
||
cl_sccharacteritem_scobjs_id,
|
||
cl_sccharacteritem_scitemcustom_id,
|
||
cl_sccharacteritem_slot,
|
||
cl_sccharacteritem_quantity,
|
||
cl_sccharacteritem_note,
|
||
cl_sccharacteritem_sort_order
|
||
) VALUES (
|
||
:character_id,
|
||
:source,
|
||
:scobjs_id,
|
||
NULL,
|
||
:slot,
|
||
:quantity,
|
||
:note,
|
||
:sort_order
|
||
)'
|
||
);
|
||
$stmt_insert->execute([
|
||
'character_id' => $character_id,
|
||
'source' => 'base',
|
||
'scobjs_id' => $source_id,
|
||
'slot' => $category,
|
||
'quantity' => $quantity,
|
||
'note' => $note !== '' ? $note : null,
|
||
'sort_order' => $sort_order,
|
||
]);
|
||
|
||
return true;
|
||
}
|
||
|
||
if ($source === 'custom') {
|
||
$stmt_source = $db->prepare(
|
||
'SELECT c.cl_scitemcustom_id AS item_id, o.cl_scobjs_type, o.cl_scobjs_subtype
|
||
FROM tbl_scitemcustom c
|
||
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
|
||
WHERE c.cl_scitemcustom_id = :id
|
||
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id
|
||
LIMIT 1'
|
||
);
|
||
$stmt_source->execute([
|
||
'id' => $source_id,
|
||
'owner_auth_id' => $owner_auth_id,
|
||
]);
|
||
$item_row = $stmt_source->fetch();
|
||
|
||
if (!$item_row) {
|
||
$error_message = 'Objet personnalisé introuvable ou non autorisé.';
|
||
return false;
|
||
}
|
||
|
||
$category = sccharacters_resolve_item_category(
|
||
$requested_category,
|
||
(string) ($item_row['cl_scobjs_type'] ?? ''),
|
||
(string) ($item_row['cl_scobjs_subtype'] ?? '')
|
||
);
|
||
|
||
$stmt_insert = $db->prepare(
|
||
'INSERT INTO tbl_sccharacteritems (
|
||
cl_sccharacteritem_character_id,
|
||
cl_sccharacteritem_source,
|
||
cl_sccharacteritem_scobjs_id,
|
||
cl_sccharacteritem_scitemcustom_id,
|
||
cl_sccharacteritem_slot,
|
||
cl_sccharacteritem_quantity,
|
||
cl_sccharacteritem_note,
|
||
cl_sccharacteritem_sort_order
|
||
) VALUES (
|
||
:character_id,
|
||
:source,
|
||
NULL,
|
||
:scitemcustom_id,
|
||
:slot,
|
||
:quantity,
|
||
:note,
|
||
:sort_order
|
||
)'
|
||
);
|
||
$stmt_insert->execute([
|
||
'character_id' => $character_id,
|
||
'source' => 'custom',
|
||
'scitemcustom_id' => $source_id,
|
||
'slot' => $category,
|
||
'quantity' => $quantity,
|
||
'note' => $note !== '' ? $note : null,
|
||
'sort_order' => $sort_order,
|
||
]);
|
||
|
||
return true;
|
||
}
|
||
|
||
$error_message = 'Source d’objet invalide.';
|
||
return false;
|
||
}
|
||
|
||
function sccharacters_update_character_item(
|
||
PDO $db,
|
||
int $owner_auth_id,
|
||
int $character_id,
|
||
int $character_item_id,
|
||
string $requested_category,
|
||
?int $quantity,
|
||
string $note,
|
||
?string &$error_message = null
|
||
): bool {
|
||
if ($character_id <= 0 || $owner_auth_id <= 0 || $character_item_id <= 0) {
|
||
$error_message = 'Paramètres de mise à jour invalides.';
|
||
return false;
|
||
}
|
||
|
||
$stmt_item = $db->prepare(
|
||
"SELECT
|
||
ci.cl_sccharacteritem_id,
|
||
ci.cl_sccharacteritem_source,
|
||
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
|
||
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
|
||
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
|
||
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype
|
||
FROM tbl_sccharacteritems ci
|
||
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
|
||
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
|
||
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
|
||
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
|
||
WHERE ci.cl_sccharacteritem_id = :character_item_id
|
||
AND c.cl_sccharacter_id = :character_id
|
||
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id
|
||
LIMIT 1"
|
||
);
|
||
$stmt_item->execute([
|
||
'character_item_id' => $character_item_id,
|
||
'character_id' => $character_id,
|
||
'owner_auth_id' => $owner_auth_id,
|
||
]);
|
||
$item_row = $stmt_item->fetch();
|
||
|
||
if (!$item_row) {
|
||
$error_message = 'Objet introuvable ou non autorisé.';
|
||
return false;
|
||
}
|
||
|
||
$is_custom = (string) ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
|
||
$item_type = $is_custom
|
||
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
|
||
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
|
||
$item_subtype = $is_custom
|
||
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
|
||
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
|
||
$category = sccharacters_resolve_item_category($requested_category, $item_type, $item_subtype);
|
||
|
||
$stmt_update = $db->prepare(
|
||
'UPDATE tbl_sccharacteritems
|
||
SET cl_sccharacteritem_slot = :slot,
|
||
cl_sccharacteritem_quantity = :quantity,
|
||
cl_sccharacteritem_note = :note
|
||
WHERE cl_sccharacteritem_id = :character_item_id
|
||
AND cl_sccharacteritem_character_id = :character_id'
|
||
);
|
||
$stmt_update->execute([
|
||
'slot' => $category,
|
||
'quantity' => $quantity,
|
||
'note' => $note !== '' ? $note : null,
|
||
'character_item_id' => $character_item_id,
|
||
'character_id' => $character_id,
|
||
]);
|
||
|
||
return true;
|
||
}
|
||
|
||
$flash = auth_flash_get();
|
||
$flash_type = $flash['type'] ?? '';
|
||
$flash_message = $flash['message'] ?? '';
|
||
$db = db();
|
||
$csrf_token = auth_csrf_token();
|
||
$current_owner_auth_id = sccharacters_current_owner_auth_id($db);
|
||
$current_session_user = auth_current_user();
|
||
$current_session_role = auth_current_role();
|
||
$role_label = auth_role_label($current_session_role);
|
||
|
||
if ($current_owner_auth_id <= 0) {
|
||
auth_flash_set('error', 'Impossible d’identifier le compte connecté.');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$submitted_csrf = (string) ($_POST['csrf_token'] ?? '');
|
||
if (!auth_validate_csrf($submitted_csrf)) {
|
||
auth_flash_set('error', 'Jeton CSRF invalide.');
|
||
header('Location: sccharacters.php');
|
||
exit;
|
||
}
|
||
|
||
$action = (string) ($_POST['action'] ?? '');
|
||
|
||
if ($action === 'create_character') {
|
||
$name = sccharacters_clean_text($_POST['character_name'] ?? '');
|
||
$role = sccharacters_clean_text($_POST['character_role'] ?? '');
|
||
$faction = sccharacters_clean_text($_POST['character_faction'] ?? '');
|
||
$avatar_url = sccharacters_clean_text($_POST['character_avatar_url'] ?? '');
|
||
$description = sccharacters_clean_text($_POST['character_description'] ?? '');
|
||
$notes = sccharacters_clean_text($_POST['character_notes'] ?? '');
|
||
$share_enabled = isset($_POST['character_share_enabled']) ? 1 : 0;
|
||
$is_pinned = isset($_POST['character_is_pinned']) ? 1 : 0;
|
||
$posted_category_order_state = sccharacters_clean_text($_POST['category_order_state'] ?? '');
|
||
$posted_item_order_state_raw = trim((string) ($_POST['item_order_state'] ?? ''));
|
||
|
||
if ($name === '') {
|
||
auth_flash_set('error', 'Le nom du personnage est obligatoire.');
|
||
header('Location: sccharacters.php?mode=create');
|
||
exit;
|
||
}
|
||
|
||
if (!sccharacters_is_valid_url($avatar_url)) {
|
||
auth_flash_set('error', 'L’URL de l’avatar n’est pas valide.');
|
||
header('Location: sccharacters.php?mode=create');
|
||
exit;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'INSERT INTO tbl_sccharacters (
|
||
cl_sccharacter_owner_auth_id,
|
||
cl_sccharacter_name,
|
||
cl_sccharacter_role,
|
||
cl_sccharacter_faction,
|
||
cl_sccharacter_avatar_url,
|
||
cl_sccharacter_description,
|
||
cl_sccharacter_notes,
|
||
cl_sccharacter_share_token,
|
||
cl_sccharacter_share_enabled,
|
||
cl_sccharacter_is_pinned
|
||
) VALUES (
|
||
:owner_auth_id,
|
||
:name,
|
||
:role,
|
||
:faction,
|
||
:avatar_url,
|
||
:description,
|
||
:notes,
|
||
:share_token,
|
||
:share_enabled,
|
||
:is_pinned
|
||
)'
|
||
);
|
||
$stmt->execute([
|
||
'owner_auth_id' => $current_owner_auth_id,
|
||
'name' => $name,
|
||
'role' => $role,
|
||
'faction' => $faction,
|
||
'avatar_url' => $avatar_url,
|
||
'description' => $description !== '' ? $description : null,
|
||
'notes' => $notes !== '' ? $notes : null,
|
||
'share_token' => sccharacters_generate_share_token($db),
|
||
'share_enabled' => $share_enabled,
|
||
'is_pinned' => $is_pinned,
|
||
]);
|
||
|
||
$new_character_id = (int) $db->lastInsertId();
|
||
auth_flash_set('success', 'Personnage créé avec succès.');
|
||
header('Location: sccharacters.php?character=' . $new_character_id);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'update_character') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
|
||
|
||
if (!$character) {
|
||
auth_flash_set('error', 'Personnage introuvable.');
|
||
header('Location: sccharacters.php');
|
||
exit;
|
||
}
|
||
|
||
$name = sccharacters_clean_text($_POST['character_name'] ?? '');
|
||
$role = sccharacters_clean_text($_POST['character_role'] ?? '');
|
||
$faction = sccharacters_clean_text($_POST['character_faction'] ?? '');
|
||
$avatar_url = sccharacters_clean_text($_POST['character_avatar_url'] ?? '');
|
||
$description = sccharacters_clean_text($_POST['character_description'] ?? '');
|
||
$notes = sccharacters_clean_text($_POST['character_notes'] ?? '');
|
||
$share_enabled = isset($_POST['character_share_enabled']) ? 1 : 0;
|
||
$is_pinned = isset($_POST['character_is_pinned']) ? 1 : 0;
|
||
$posted_category_order_state = sccharacters_clean_text($_POST['category_order_state'] ?? '');
|
||
$posted_item_order_state_raw = trim((string) ($_POST['item_order_state'] ?? ''));
|
||
|
||
if ($name === '') {
|
||
auth_flash_set('error', 'Le nom du personnage est obligatoire.');
|
||
header('Location: sccharacters.php?character=' . $character_id);
|
||
exit;
|
||
}
|
||
|
||
if (!sccharacters_is_valid_url($avatar_url)) {
|
||
auth_flash_set('error', 'L’URL de l’avatar n’est pas valide.');
|
||
header('Location: sccharacters.php?character=' . $character_id);
|
||
exit;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'UPDATE tbl_sccharacters
|
||
SET cl_sccharacter_name = :name,
|
||
cl_sccharacter_role = :role,
|
||
cl_sccharacter_faction = :faction,
|
||
cl_sccharacter_avatar_url = :avatar_url,
|
||
cl_sccharacter_description = :description,
|
||
cl_sccharacter_notes = :notes,
|
||
cl_sccharacter_share_enabled = :share_enabled,
|
||
cl_sccharacter_is_pinned = :is_pinned
|
||
WHERE cl_sccharacter_id = :id
|
||
AND cl_sccharacter_owner_auth_id = :owner_auth_id'
|
||
);
|
||
$stmt->execute([
|
||
'name' => $name,
|
||
'role' => $role,
|
||
'faction' => $faction,
|
||
'avatar_url' => $avatar_url,
|
||
'description' => $description !== '' ? $description : null,
|
||
'notes' => $notes !== '' ? $notes : null,
|
||
'share_enabled' => $share_enabled,
|
||
'is_pinned' => $is_pinned,
|
||
'id' => $character_id,
|
||
'owner_auth_id' => $current_owner_auth_id,
|
||
]);
|
||
|
||
$current_item_rows = sccharacters_fetch_owned_character_item_rows($db, $current_owner_auth_id, $character_id);
|
||
$grouped_item_ids = sccharacters_group_item_ids_by_category($current_item_rows);
|
||
$current_visible_category_keys = array_keys(
|
||
sccharacters_sort_items_by_category_order($grouped_item_ids, sccharacters_character_category_order($character))
|
||
);
|
||
|
||
$submitted_visible_category_order = $current_visible_category_keys;
|
||
if ($posted_category_order_state !== '') {
|
||
$candidate_category_order = array_values(array_filter(
|
||
array_map('sccharacters_clean_text', preg_split('/\s*,\s*/', $posted_category_order_state, -1, PREG_SPLIT_NO_EMPTY) ?: []),
|
||
static fn (string $category_key): bool => $category_key !== ''
|
||
));
|
||
|
||
if ($candidate_category_order !== []) {
|
||
$candidate_sorted = $candidate_category_order;
|
||
$current_sorted = $current_visible_category_keys;
|
||
sort($candidate_sorted);
|
||
sort($current_sorted);
|
||
|
||
if ($candidate_sorted === $current_sorted) {
|
||
$submitted_visible_category_order = $candidate_category_order;
|
||
}
|
||
}
|
||
}
|
||
|
||
$submitted_item_order_state = [];
|
||
if ($posted_item_order_state_raw !== '') {
|
||
$decoded_item_order_state = json_decode($posted_item_order_state_raw, true);
|
||
if (is_array($decoded_item_order_state)) {
|
||
$submitted_item_order_state = $decoded_item_order_state;
|
||
}
|
||
}
|
||
|
||
foreach ($grouped_item_ids as $category_key => $current_category_item_ids) {
|
||
$current_category_item_ids = array_values(array_map('intval', $current_category_item_ids));
|
||
$candidate_category_item_ids = $submitted_item_order_state[$category_key] ?? null;
|
||
if (!is_array($candidate_category_item_ids)) {
|
||
$grouped_item_ids[$category_key] = $current_category_item_ids;
|
||
continue;
|
||
}
|
||
|
||
$candidate_category_item_ids = array_values(array_filter(
|
||
array_map('intval', $candidate_category_item_ids),
|
||
static fn (int $item_id): bool => $item_id > 0
|
||
));
|
||
|
||
$sorted_candidate_item_ids = $candidate_category_item_ids;
|
||
$sorted_current_category_item_ids = $current_category_item_ids;
|
||
sort($sorted_candidate_item_ids);
|
||
sort($sorted_current_category_item_ids);
|
||
|
||
if ($candidate_category_item_ids !== [] && $sorted_candidate_item_ids === $sorted_current_category_item_ids) {
|
||
$grouped_item_ids[$category_key] = $candidate_category_item_ids;
|
||
} else {
|
||
$grouped_item_ids[$category_key] = $current_category_item_ids;
|
||
}
|
||
}
|
||
|
||
$persisted_category_order = sccharacters_character_category_order($character);
|
||
$visible_category_lookup = array_fill_keys(array_keys($grouped_item_ids), true);
|
||
$visible_pointer = 0;
|
||
$rebuilt_category_order = [];
|
||
foreach ($persisted_category_order as $ordered_category_key) {
|
||
if (isset($visible_category_lookup[$ordered_category_key])) {
|
||
$rebuilt_category_order[] = $submitted_visible_category_order[$visible_pointer] ?? $ordered_category_key;
|
||
$visible_pointer++;
|
||
} else {
|
||
$rebuilt_category_order[] = $ordered_category_key;
|
||
}
|
||
}
|
||
|
||
sccharacters_save_character_category_order($db, $character_id, $rebuilt_category_order);
|
||
sccharacters_save_character_item_order(
|
||
$db,
|
||
$character_id,
|
||
sccharacters_flatten_grouped_item_ids($grouped_item_ids, $rebuilt_category_order)
|
||
);
|
||
|
||
auth_flash_set('success', 'Personnage mis à jour.');
|
||
header('Location: sccharacters.php?character=' . $character_id);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'delete_character') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
|
||
|
||
if (!$character) {
|
||
auth_flash_set('error', 'Personnage introuvable.');
|
||
header('Location: sccharacters.php');
|
||
exit;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'DELETE FROM tbl_sccharacters
|
||
WHERE cl_sccharacter_id = :id
|
||
AND cl_sccharacter_owner_auth_id = :owner_auth_id'
|
||
);
|
||
$stmt->execute([
|
||
'id' => $character_id,
|
||
'owner_auth_id' => $current_owner_auth_id,
|
||
]);
|
||
|
||
auth_flash_set('success', 'Personnage supprimé.');
|
||
header('Location: sccharacters.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'regenerate_share_token') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
|
||
|
||
if (!$character) {
|
||
auth_flash_set('error', 'Personnage introuvable.');
|
||
header('Location: sccharacters.php');
|
||
exit;
|
||
}
|
||
|
||
$stmt = $db->prepare(
|
||
'UPDATE tbl_sccharacters
|
||
SET cl_sccharacter_share_token = :token
|
||
WHERE cl_sccharacter_id = :id
|
||
AND cl_sccharacter_owner_auth_id = :owner_auth_id'
|
||
);
|
||
$stmt->execute([
|
||
'token' => sccharacters_generate_share_token($db),
|
||
'id' => $character_id,
|
||
'owner_auth_id' => $current_owner_auth_id,
|
||
]);
|
||
|
||
auth_flash_set('success', 'Lien public régénéré.');
|
||
header('Location: sccharacters.php?character=' . $character_id);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'add_base_item') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$obj_id = (int) ($_POST['base_obj_id'] ?? 0);
|
||
$requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
|
||
$quantity = sccharacters_normalize_item_quantity($_POST['item_quantity'] ?? null);
|
||
$note = sccharacters_clean_text($_POST['item_note'] ?? '');
|
||
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
|
||
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
|
||
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
|
||
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
|
||
$error_message = null;
|
||
|
||
if (!sccharacters_attach_item(
|
||
$db,
|
||
$current_owner_auth_id,
|
||
$character_id,
|
||
'base',
|
||
$obj_id,
|
||
$requested_category,
|
||
$quantity,
|
||
$note,
|
||
$error_message
|
||
)) {
|
||
auth_flash_set('error', $error_message ?? 'Impossible d’ajouter l’objet.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
auth_flash_set('success', 'Objet de la base ajouté au personnage.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'add_custom_item') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$itemcustom_id = (int) ($_POST['custom_item_id'] ?? 0);
|
||
$requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
|
||
$quantity = sccharacters_normalize_item_quantity($_POST['item_quantity'] ?? null);
|
||
$note = sccharacters_clean_text($_POST['item_note'] ?? '');
|
||
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'custom');
|
||
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
|
||
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
|
||
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
|
||
$error_message = null;
|
||
|
||
if (!sccharacters_attach_item(
|
||
$db,
|
||
$current_owner_auth_id,
|
||
$character_id,
|
||
'custom',
|
||
$itemcustom_id,
|
||
$requested_category,
|
||
$quantity,
|
||
$note,
|
||
$error_message
|
||
)) {
|
||
auth_flash_set('error', $error_message ?? 'Impossible d’ajouter l’objet.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
auth_flash_set('success', 'Objet personnalisé ajouté au personnage.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'update_character_item') {
|
||
$character_item_id = (int) ($_POST['character_item_id'] ?? 0);
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
|
||
$quantity = sccharacters_normalize_item_quantity($_POST['item_quantity'] ?? null);
|
||
$note = sccharacters_clean_text($_POST['item_note'] ?? '');
|
||
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
|
||
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
|
||
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
|
||
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
|
||
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
|
||
$error_message = null;
|
||
|
||
if (!sccharacters_update_character_item(
|
||
$db,
|
||
$current_owner_auth_id,
|
||
$character_id,
|
||
$character_item_id,
|
||
$requested_category,
|
||
$quantity,
|
||
$note,
|
||
$error_message
|
||
)) {
|
||
auth_flash_set('error', $error_message ?? 'Impossible de mettre à jour cet objet.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
auth_flash_set('success', 'Objet mis à jour.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'move_character_category') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$category_key = sccharacters_clean_text($_POST['category_key'] ?? '');
|
||
$move_direction = sccharacters_clean_text($_POST['move_direction'] ?? '');
|
||
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
|
||
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
|
||
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
|
||
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
|
||
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
|
||
|
||
if (!sccharacters_move_owned_character_category($db, $current_owner_auth_id, $character_id, $category_key, $move_direction)) {
|
||
auth_flash_set('error', 'Impossible de déplacer cette catégorie.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
auth_flash_set('success', 'Ordre des catégories mis à jour.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'reorder_character_items') {
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$category_key = sccharacters_clean_text($_POST['category_key'] ?? '');
|
||
$ordered_item_ids = preg_split('/\s*,\s*/', (string) ($_POST['ordered_item_ids'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
|
||
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
|
||
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
|
||
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
|
||
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
|
||
|
||
if (!sccharacters_reorder_owned_character_items($db, $current_owner_auth_id, $character_id, $category_key, $ordered_item_ids)) {
|
||
auth_flash_set('error', 'Impossible de réordonner les objets.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
auth_flash_set('success', 'Ordre des objets mis à jour.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'delete_character_item') {
|
||
$character_item_id = (int) ($_POST['character_item_id'] ?? 0);
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
|
||
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
|
||
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
|
||
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
|
||
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
|
||
|
||
$stmt = $db->prepare(
|
||
'DELETE ci
|
||
FROM tbl_sccharacteritems ci
|
||
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
|
||
WHERE ci.cl_sccharacteritem_id = :character_item_id
|
||
AND c.cl_sccharacter_id = :character_id
|
||
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id'
|
||
);
|
||
$stmt->execute([
|
||
'character_item_id' => $character_item_id,
|
||
'character_id' => $character_id,
|
||
'owner_auth_id' => $current_owner_auth_id,
|
||
]);
|
||
|
||
sccharacters_reindex_character_items($db, $character_id);
|
||
|
||
auth_flash_set('success', 'Objet retiré du personnage.');
|
||
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
|
||
exit;
|
||
}
|
||
}
|
||
|
||
$stmt_characters = $db->prepare(
|
||
'SELECT c.*, COUNT(ci.cl_sccharacteritem_id) AS cl_sccharacter_item_count
|
||
FROM tbl_sccharacters c
|
||
LEFT JOIN tbl_sccharacteritems ci ON ci.cl_sccharacteritem_character_id = c.cl_sccharacter_id
|
||
WHERE c.cl_sccharacter_owner_auth_id = :owner_auth_id
|
||
GROUP BY c.cl_sccharacter_id
|
||
ORDER BY c.cl_sccharacter_is_pinned DESC, c.cl_sccharacter_updated_at DESC, c.cl_sccharacter_name ASC'
|
||
);
|
||
$stmt_characters->execute(['owner_auth_id' => $current_owner_auth_id]);
|
||
$characters = $stmt_characters->fetchAll();
|
||
|
||
$character_lookup = [];
|
||
foreach ($characters as $character_row) {
|
||
$character_lookup[(int) $character_row['cl_sccharacter_id']] = $character_row;
|
||
}
|
||
|
||
$mode = (string) ($_GET['mode'] ?? '');
|
||
$selected_character_id = (int) ($_GET['character'] ?? 0);
|
||
$selected_character = null;
|
||
|
||
if ($selected_character_id > 0 && isset($character_lookup[$selected_character_id])) {
|
||
$selected_character = $character_lookup[$selected_character_id];
|
||
} elseif ($characters !== []) {
|
||
$selected_character = $characters[0];
|
||
$selected_character_id = (int) $selected_character['cl_sccharacter_id'];
|
||
}
|
||
|
||
$create_panel_open = ($mode === 'create') || ($characters === []);
|
||
$item_source = sccharacters_clean_text($_GET['item_source'] ?? 'base');
|
||
if (!in_array($item_source, ['base', 'custom'], true)) {
|
||
$item_source = 'base';
|
||
}
|
||
|
||
$item_search = sccharacters_clean_text($_GET['item_search'] ?? '');
|
||
$item_panel_open = (string) ($_GET['item_panel'] ?? '') === '1';
|
||
$item_page = max(1, (int) ($_GET['item_page'] ?? 1));
|
||
$item_per_page = sccharacters_normalize_item_per_page($_GET['item_per_page'] ?? 50);
|
||
$item_results = [];
|
||
$item_total_results = 0;
|
||
$item_total_pages = 1;
|
||
$item_result_offset_start = 0;
|
||
$item_result_offset_end = 0;
|
||
|
||
if ($selected_character) {
|
||
if ($item_source === 'custom') {
|
||
$count_sql = "SELECT COUNT(DISTINCT c.cl_scitemcustom_id)
|
||
FROM tbl_scitemcustom c
|
||
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
|
||
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id";
|
||
$params = ['owner_auth_id' => $current_owner_auth_id];
|
||
|
||
if ($item_search !== '') {
|
||
$count_sql .= " AND (
|
||
o.cl_scobjs_name LIKE :search
|
||
OR o.cl_scobjs_type LIKE :search
|
||
OR o.cl_scobjs_subtype LIKE :search
|
||
OR o.cl_scobjs_uuid LIKE :search
|
||
)";
|
||
$params['search'] = '%' . $item_search . '%';
|
||
}
|
||
|
||
$stmt_item_count = $db->prepare($count_sql);
|
||
$stmt_item_count->execute($params);
|
||
$item_total_results = (int) $stmt_item_count->fetchColumn();
|
||
$item_total_pages = max(1, (int) ceil($item_total_results / $item_per_page));
|
||
$item_page = min($item_page, $item_total_pages);
|
||
$item_offset = max(0, ($item_page - 1) * $item_per_page);
|
||
|
||
$sql = "SELECT c.cl_scitemcustom_id,
|
||
o.cl_scobjs_name,
|
||
o.cl_scobjs_type,
|
||
o.cl_scobjs_subtype,
|
||
o.cl_scobjs_uuid,
|
||
COUNT(cs.cl_scitemcustomstat_id) AS cl_scitemcustom_stat_count
|
||
FROM tbl_scitemcustom c
|
||
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
|
||
LEFT JOIN tbl_scitemcustomstat cs ON cs.cl_scitemcustomstat_itemcustom_id = c.cl_scitemcustom_id
|
||
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id";
|
||
|
||
if ($item_search !== '') {
|
||
$sql .= " AND (
|
||
o.cl_scobjs_name LIKE :search
|
||
OR o.cl_scobjs_type LIKE :search
|
||
OR o.cl_scobjs_subtype LIKE :search
|
||
OR o.cl_scobjs_uuid LIKE :search
|
||
)";
|
||
}
|
||
|
||
$sql .= "
|
||
GROUP BY c.cl_scitemcustom_id
|
||
ORDER BY o.cl_scobjs_name ASC, c.cl_scitemcustom_id ASC
|
||
LIMIT " . (int) $item_per_page . " OFFSET " . (int) $item_offset;
|
||
|
||
$stmt_item_results = $db->prepare($sql);
|
||
$stmt_item_results->execute($params);
|
||
$item_results = $stmt_item_results->fetchAll();
|
||
} else {
|
||
$count_sql = "SELECT COUNT(*)
|
||
FROM tbl_scobjs
|
||
WHERE 1 = 1";
|
||
$params = [];
|
||
|
||
if ($item_search !== '') {
|
||
$count_sql .= " AND (
|
||
cl_scobjs_name LIKE :search
|
||
OR cl_scobjs_type LIKE :search
|
||
OR cl_scobjs_subtype LIKE :search
|
||
OR cl_scobjs_uuid LIKE :search
|
||
)";
|
||
$params['search'] = '%' . $item_search . '%';
|
||
}
|
||
|
||
$stmt_item_count = $db->prepare($count_sql);
|
||
$stmt_item_count->execute($params);
|
||
$item_total_results = (int) $stmt_item_count->fetchColumn();
|
||
$item_total_pages = max(1, (int) ceil($item_total_results / $item_per_page));
|
||
$item_page = min($item_page, $item_total_pages);
|
||
$item_offset = max(0, ($item_page - 1) * $item_per_page);
|
||
|
||
$sql = "SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid
|
||
FROM tbl_scobjs
|
||
WHERE 1 = 1";
|
||
|
||
if ($item_search !== '') {
|
||
$sql .= " AND (
|
||
cl_scobjs_name LIKE :search
|
||
OR cl_scobjs_type LIKE :search
|
||
OR cl_scobjs_subtype LIKE :search
|
||
OR cl_scobjs_uuid LIKE :search
|
||
)";
|
||
}
|
||
|
||
$sql .= "
|
||
ORDER BY cl_scobjs_name ASC
|
||
LIMIT " . (int) $item_per_page . " OFFSET " . (int) $item_offset;
|
||
|
||
$stmt_item_results = $db->prepare($sql);
|
||
$stmt_item_results->execute($params);
|
||
$item_results = $stmt_item_results->fetchAll();
|
||
}
|
||
|
||
if ($item_total_results > 0) {
|
||
$item_result_offset_start = (($item_page - 1) * $item_per_page) + 1;
|
||
$item_result_offset_end = min($item_total_results, $item_result_offset_start + count($item_results) - 1);
|
||
}
|
||
|
||
}
|
||
|
||
$selected_character_items = [];
|
||
$custom_stats_by_itemcustom = [];
|
||
if ($selected_character) {
|
||
sccharacters_reindex_character_items($db, (int) $selected_character['cl_sccharacter_id']);
|
||
|
||
$stmt_character_items = $db->prepare(
|
||
"SELECT
|
||
ci.*,
|
||
bo.cl_scobjs_name AS cl_sccharacteritem_base_name,
|
||
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
|
||
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
|
||
bo.cl_scobjs_uuid AS cl_sccharacteritem_base_uuid,
|
||
co.cl_scitemcustom_id AS cl_sccharacteritem_custom_ref_id,
|
||
oo.cl_scobjs_name AS cl_sccharacteritem_custom_name,
|
||
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
|
||
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype,
|
||
oo.cl_scobjs_uuid AS cl_sccharacteritem_custom_uuid
|
||
FROM tbl_sccharacteritems ci
|
||
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
|
||
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
|
||
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
|
||
WHERE ci.cl_sccharacteritem_character_id = :character_id
|
||
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC"
|
||
);
|
||
$stmt_character_items->execute(['character_id' => (int) $selected_character['cl_sccharacter_id']]);
|
||
$selected_character_items = $stmt_character_items->fetchAll();
|
||
|
||
$custom_item_ids = [];
|
||
foreach ($selected_character_items as $character_item_row) {
|
||
if (($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom' && !empty($character_item_row['cl_sccharacteritem_scitemcustom_id'])) {
|
||
$custom_item_ids[] = (int) $character_item_row['cl_sccharacteritem_scitemcustom_id'];
|
||
}
|
||
}
|
||
$custom_item_ids = array_values(array_unique(array_filter($custom_item_ids)));
|
||
|
||
if ($custom_item_ids !== []) {
|
||
$placeholders = implode(',', array_fill(0, count($custom_item_ids), '?'));
|
||
$stmt_custom_stats = $db->prepare(
|
||
"SELECT
|
||
cs.cl_scitemcustomstat_itemcustom_id,
|
||
st.cl_scstatsitem_name,
|
||
st.cl_scstatsitem_unit,
|
||
cs.cl_scitemcustomstat_sign,
|
||
cs.cl_scitemcustomstat_value
|
||
FROM tbl_scitemcustomstat cs
|
||
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
|
||
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
|
||
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
|
||
);
|
||
$stmt_custom_stats->execute($custom_item_ids);
|
||
|
||
foreach ($stmt_custom_stats->fetchAll() as $custom_stat_row) {
|
||
$itemcustom_id = (int) $custom_stat_row['cl_scitemcustomstat_itemcustom_id'];
|
||
if (!isset($custom_stats_by_itemcustom[$itemcustom_id])) {
|
||
$custom_stats_by_itemcustom[$itemcustom_id] = [];
|
||
}
|
||
$custom_stats_by_itemcustom[$itemcustom_id][] = $custom_stat_row;
|
||
}
|
||
}
|
||
}
|
||
|
||
$item_category_options = sccharacters_item_category_options();
|
||
$selected_character_category_order = $selected_character
|
||
? sccharacters_character_category_order($selected_character)
|
||
: sccharacters_default_category_order();
|
||
$selected_character_items_by_category = [];
|
||
foreach ($selected_character_items as $character_item_row) {
|
||
$is_custom = ($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
|
||
$item_type = $is_custom
|
||
? (string) ($character_item_row['cl_sccharacteritem_custom_type'] ?? '')
|
||
: (string) ($character_item_row['cl_sccharacteritem_base_type'] ?? '');
|
||
$item_subtype = $is_custom
|
||
? (string) ($character_item_row['cl_sccharacteritem_custom_subtype'] ?? '')
|
||
: (string) ($character_item_row['cl_sccharacteritem_base_subtype'] ?? '');
|
||
$category_key = sccharacters_resolve_item_category(
|
||
(string) ($character_item_row['cl_sccharacteritem_slot'] ?? ''),
|
||
$item_type,
|
||
$item_subtype
|
||
);
|
||
if (!isset($selected_character_items_by_category[$category_key])) {
|
||
$selected_character_items_by_category[$category_key] = [];
|
||
}
|
||
$selected_character_items_by_category[$category_key][] = $character_item_row;
|
||
}
|
||
$selected_character_items_by_category = sccharacters_sort_items_by_category_order(
|
||
$selected_character_items_by_category,
|
||
$selected_character_category_order
|
||
);
|
||
$selected_character_category_order_meta = [];
|
||
$visible_category_keys = array_keys($selected_character_items_by_category);
|
||
$visible_category_count = count($visible_category_keys);
|
||
foreach ($visible_category_keys as $visible_category_index => $visible_category_key) {
|
||
$selected_character_category_order_meta[$visible_category_key] = [
|
||
'is_first' => $visible_category_index === 0,
|
||
'is_last' => $visible_category_index === ($visible_category_count - 1),
|
||
];
|
||
}
|
||
$selected_character_visible_category_order_state = implode(',', $visible_category_keys);
|
||
$selected_character_item_order_state = [];
|
||
foreach ($selected_character_items_by_category as $category_key => $category_items) {
|
||
$selected_character_item_order_state[$category_key] = array_values(array_map(
|
||
static fn (array $item_row): int => (int) ($item_row['cl_sccharacteritem_id'] ?? 0),
|
||
$category_items
|
||
));
|
||
}
|
||
$selected_character_item_order_state_json = json_encode(
|
||
$selected_character_item_order_state,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
if (!is_string($selected_character_item_order_state_json)) {
|
||
$selected_character_item_order_state_json = '{}';
|
||
}
|
||
$create_character = [
|
||
'cl_sccharacter_name' => '',
|
||
'cl_sccharacter_role' => '',
|
||
'cl_sccharacter_faction' => '',
|
||
'cl_sccharacter_avatar_url' => '',
|
||
'cl_sccharacter_description' => '',
|
||
'cl_sccharacter_notes' => '',
|
||
'cl_sccharacter_share_enabled' => 0,
|
||
'cl_sccharacter_is_pinned' => 0,
|
||
];
|
||
$item_has_previous_page = $item_page > 1;
|
||
$item_has_next_page = $item_page < $item_total_pages;
|
||
$item_query_base_params = [];
|
||
if ($selected_character) {
|
||
$item_query_base_params = [
|
||
'character' => (int) $selected_character['cl_sccharacter_id'],
|
||
'item_panel' => 1,
|
||
'item_source' => $item_source,
|
||
];
|
||
|
||
if ($item_search !== '') {
|
||
$item_query_base_params['item_search'] = $item_search;
|
||
}
|
||
|
||
if ($item_per_page !== 50) {
|
||
$item_query_base_params['item_per_page'] = $item_per_page;
|
||
}
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PERSONNAGES | 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-soft: rgba(162, 155, 120, 0.18);
|
||
--primary-border: rgba(162, 155, 120, 0.34);
|
||
--bg-dark: #080a0f;
|
||
--card-bg: rgba(16, 20, 29, 0.86);
|
||
--card-bg-strong: rgba(20, 25, 37, 0.95);
|
||
--text-main: #ece9df;
|
||
--text-soft: rgba(236, 233, 223, 0.74);
|
||
--danger: #ff6b6b;
|
||
--success: #49d17d;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Electrolize';
|
||
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
background: radial-gradient(circle at top right, #1d2234 0%, #0a0d13 55%, #050608 100%);
|
||
color: var(--text-main);
|
||
font-family: 'Electrolize', sans-serif;
|
||
}
|
||
|
||
.admin-layout {
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-width: 1480px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.admin-topbar,
|
||
.panel,
|
||
.character-card,
|
||
.equip-card,
|
||
.search-result,
|
||
.empty-state {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--primary-border);
|
||
border-radius: 18px;
|
||
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.28);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
.admin-topbar {
|
||
padding: 1.5rem 2rem;
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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.65rem 1.2rem;
|
||
border: 1px solid var(--primary-border);
|
||
background: rgba(255, 255, 255, 0.02);
|
||
color: #fff;
|
||
font-family: 'Electrolize', sans-serif;
|
||
font-size: 0.88rem;
|
||
text-transform: uppercase;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
border-radius: 10px;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.4rem;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.btn-modern:hover {
|
||
background: rgba(106, 208, 255, 0.16);
|
||
border-color: rgba(106, 208, 255, 0.65);
|
||
box-shadow: 0 0 16px rgba(106, 208, 255, 0.18);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-modern.danger {
|
||
border-color: rgba(255, 123, 123, 0.35);
|
||
color: #ffb3b3;
|
||
}
|
||
|
||
.btn-modern.danger:hover {
|
||
background: rgba(255, 107, 107, 0.16);
|
||
border-color: rgba(255, 107, 107, 0.6);
|
||
box-shadow: 0 0 16px rgba(255, 107, 107, 0.2);
|
||
}
|
||
|
||
.flash {
|
||
padding: 1rem 1.1rem;
|
||
border-radius: 14px;
|
||
border: 1px solid transparent;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.flash.success { background: rgba(73, 209, 125, 0.14); border-color: rgba(73, 209, 125, 0.3); color: #d6ffe4; }
|
||
.flash.error { background: rgba(255, 107, 107, 0.12); border-color: rgba(255, 107, 107, 0.28); color: #ffd7d7; }
|
||
|
||
.main-grid {
|
||
display: grid;
|
||
grid-template-columns: 390px minmax(0, 1fr);
|
||
gap: 1.5rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.sidebar-column,
|
||
.right-column {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.2rem;
|
||
}
|
||
|
||
.panel { padding: 1.3rem; }
|
||
|
||
.panel-soft {
|
||
background: rgba(255,255,255,0.03);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.panel-title {
|
||
margin: 0 0 0.35rem;
|
||
font-size: 1.05rem;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.panel-subtitle {
|
||
margin: 0;
|
||
color: var(--text-soft);
|
||
font-size: 0.9rem;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.list-panel {
|
||
padding-bottom: 1rem;
|
||
}
|
||
|
||
.list-panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
align-items: flex-start;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.accordion-panel {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.accordion-summary {
|
||
list-style: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
padding: 1.15rem 1.3rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.accordion-summary::-webkit-details-marker { display: none; }
|
||
.accordion-panel[open] .accordion-summary {
|
||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
background: rgba(255,255,255,0.02);
|
||
}
|
||
|
||
.accordion-indicator {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 999px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255,255,255,0.05);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
color: var(--primary);
|
||
flex: 0 0 34px;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.accordion-panel[open] .accordion-indicator {
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.accordion-body {
|
||
padding: 1.25rem 1.3rem 1.3rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 0.9rem;
|
||
}
|
||
|
||
.field,
|
||
.field-full { display: flex; flex-direction: column; gap: 0.45rem; }
|
||
.field-full { grid-column: 1 / -1; }
|
||
|
||
label {
|
||
font-size: 0.78rem;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-soft);
|
||
}
|
||
|
||
input[type="text"],
|
||
input[type="url"],
|
||
textarea,
|
||
select {
|
||
width: 100%;
|
||
border: 1px solid rgba(255,255,255,0.09);
|
||
background: rgba(7, 10, 16, 0.72);
|
||
color: var(--text-main);
|
||
border-radius: 12px;
|
||
padding: 0.82rem 0.9rem;
|
||
font-family: inherit;
|
||
font-size: 0.95rem;
|
||
outline: none;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
input:focus,
|
||
textarea:focus,
|
||
select:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(162, 155, 120, 0.13);
|
||
}
|
||
|
||
textarea { min-height: 110px; resize: vertical; }
|
||
|
||
.checkbox-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.7rem;
|
||
padding: 0.85rem 0.95rem;
|
||
border-radius: 12px;
|
||
background: rgba(255,255,255,0.03);
|
||
border: 1px solid rgba(255,255,255,0.06);
|
||
}
|
||
|
||
.checkbox-row input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
margin: 0;
|
||
}
|
||
|
||
.checkbox-row label {
|
||
text-transform: none;
|
||
letter-spacing: 0;
|
||
font-size: 0.92rem;
|
||
color: var(--text-main);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-row {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
flex-wrap: wrap;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.btn,
|
||
.btn-link {
|
||
appearance: none;
|
||
border: 0;
|
||
border-radius: 12px;
|
||
padding: 0.85rem 1rem;
|
||
font-family: inherit;
|
||
font-size: 0.92rem;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.45rem;
|
||
}
|
||
|
||
.btn:hover,
|
||
.btn-link:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 12px 24px rgba(0,0,0,0.22);
|
||
}
|
||
|
||
.btn-small,
|
||
.btn-link.btn-small {
|
||
padding: 0.68rem 0.85rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.btn-primary { background: linear-gradient(135deg, #d2c6a0, #93875f); color: #0d1017; }
|
||
.btn-secondary { background: rgba(255,255,255,0.07); color: var(--text-main); border: 1px solid rgba(255,255,255,0.08); }
|
||
.btn-danger { background: rgba(255, 107, 107, 0.14); color: #ffe7e7; border: 1px solid rgba(255, 107, 107, 0.28); }
|
||
|
||
.characters-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.8rem;
|
||
max-height: calc(100vh - 360px);
|
||
overflow: auto;
|
||
padding-right: 0.15rem;
|
||
}
|
||
|
||
.character-card {
|
||
display: grid;
|
||
grid-template-columns: 56px minmax(0, 1fr);
|
||
gap: 0.85rem;
|
||
padding: 0.85rem;
|
||
color: inherit;
|
||
text-decoration: none;
|
||
border-color: rgba(255,255,255,0.08);
|
||
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||
}
|
||
|
||
.character-card:hover {
|
||
transform: translateY(-1px);
|
||
border-color: rgba(162, 155, 120, 0.45);
|
||
background: rgba(24, 30, 42, 0.94);
|
||
}
|
||
|
||
.character-card.is-active {
|
||
border-color: rgba(162, 155, 120, 0.6);
|
||
background: rgba(27, 32, 45, 0.96);
|
||
box-shadow: 0 0 0 1px rgba(162, 155, 120, 0.18), 0 18px 45px rgba(0, 0, 0, 0.28);
|
||
}
|
||
|
||
.character-avatar,
|
||
.character-avatar-fallback {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 16px;
|
||
object-fit: cover;
|
||
background: linear-gradient(145deg, rgba(162, 155, 120, 0.3), rgba(255,255,255,0.08));
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
}
|
||
|
||
.character-avatar-fallback,
|
||
.character-hero-fallback,
|
||
.equip-thumb-fallback {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.character-card-body,
|
||
.equip-content { min-width: 0; }
|
||
|
||
.character-card-body h3,
|
||
.equip-title,
|
||
.character-hero-name {
|
||
margin: 0;
|
||
}
|
||
|
||
.character-card-title,
|
||
.hero-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.character-card-body h3 {
|
||
font-size: 0.98rem;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.pin-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 1.8rem;
|
||
height: 1.8rem;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(162, 155, 120, 0.34);
|
||
background: rgba(162, 155, 120, 0.14);
|
||
color: #f8f3e1;
|
||
font-size: 0.95rem;
|
||
line-height: 1;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.pin-indicator-input {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(162, 155, 120, 0.25);
|
||
background: rgba(162, 155, 120, 0.1);
|
||
color: #f8f3e1;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1rem;
|
||
line-height: 1;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.muted {
|
||
color: var(--text-soft);
|
||
font-size: 0.88rem;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.badge-row,
|
||
.hero-meta,
|
||
.equip-meta {
|
||
display: flex;
|
||
gap: 0.45rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
padding: 0.32rem 0.56rem;
|
||
border-radius: 999px;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
font-size: 0.77rem;
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.badge-primary {
|
||
background: rgba(162, 155, 120, 0.18);
|
||
border-color: rgba(162, 155, 120, 0.34);
|
||
color: #f8f3e1;
|
||
}
|
||
|
||
.badge-muted { color: var(--text-soft); }
|
||
|
||
.badge-success {
|
||
background: rgba(73, 209, 125, 0.12);
|
||
border-color: rgba(73, 209, 125, 0.3);
|
||
color: #d6ffe4;
|
||
}
|
||
|
||
.character-hero {
|
||
display: grid;
|
||
grid-template-columns: 110px minmax(0, 1fr);
|
||
gap: 1.2rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.character-hero-avatar,
|
||
.character-hero-fallback {
|
||
width: 110px;
|
||
height: 110px;
|
||
border-radius: 24px;
|
||
object-fit: cover;
|
||
background: linear-gradient(145deg, rgba(162, 155, 120, 0.3), rgba(255,255,255,0.08));
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
}
|
||
|
||
.character-hero-fallback {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.character-hero-name {
|
||
font-size: 1.5rem;
|
||
margin-bottom: 0.55rem;
|
||
}
|
||
|
||
.hero-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
align-items: flex-start;
|
||
margin-bottom: 0.8rem;
|
||
}
|
||
|
||
.share-box {
|
||
margin-top: 1rem;
|
||
padding: 1rem;
|
||
border-radius: 14px;
|
||
background: rgba(255,255,255,0.03);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
}
|
||
|
||
.share-box input {
|
||
margin-top: 0.65rem;
|
||
font-size: 0.88rem;
|
||
}
|
||
|
||
.edit-form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 0.9rem;
|
||
}
|
||
|
||
.equipment-sections {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.equipment-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.8rem;
|
||
}
|
||
|
||
.equipment-section-heading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.8rem;
|
||
padding-bottom: 0.35rem;
|
||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.equipment-section-heading-main {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.65rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.equipment-category-order-form {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.equipment-section-title {
|
||
margin: 0;
|
||
font-size: 0.92rem;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.equipment-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.equip-card {
|
||
padding: 1rem;
|
||
display: flex;
|
||
gap: 0.9rem;
|
||
align-items: flex-start;
|
||
cursor: grab;
|
||
transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||
}
|
||
|
||
.equip-card.dragging {
|
||
opacity: 0.45;
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.equip-card.drag-over {
|
||
border-color: rgba(162, 155, 120, 0.55);
|
||
background: rgba(162, 155, 120, 0.09);
|
||
}
|
||
|
||
.equip-thumb,
|
||
.equip-thumb-fallback {
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 16px;
|
||
flex: 0 0 64px;
|
||
object-fit: cover;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
}
|
||
|
||
.equip-thumb-fallback {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.equip-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.6rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.equip-title-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.drag-handle {
|
||
color: var(--text-soft);
|
||
font-size: 1rem;
|
||
line-height: 1;
|
||
letter-spacing: -0.1em;
|
||
user-select: none;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.drag-hint {
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.equipment-actions {
|
||
margin-top: auto;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.75rem;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.equipment-action-buttons {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, auto);
|
||
align-items: start;
|
||
gap: 0.55rem;
|
||
justify-content: end;
|
||
}
|
||
|
||
.equipment-edit {
|
||
width: auto;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.equipment-edit[open] {
|
||
grid-column: 1 / -1;
|
||
width: min(100%, 34rem);
|
||
justify-self: end;
|
||
}
|
||
|
||
.equipment-edit-summary,
|
||
.equipment-icon-button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 2.45rem;
|
||
min-width: 2.45rem;
|
||
height: 2.45rem;
|
||
min-height: 2.45rem;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
list-style: none;
|
||
line-height: 1;
|
||
}
|
||
|
||
.equipment-edit-summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
.equipment-edit-form {
|
||
margin-top: 0.85rem;
|
||
padding: 0.95rem;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
background: rgba(255,255,255,0.04);
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
width: min(100%, 34rem);
|
||
}
|
||
|
||
.equipment-edit-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(110px, 135px) minmax(0, 1fr);
|
||
gap: 0.75rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.equipment-edit-grid textarea {
|
||
grid-column: 1 / -1;
|
||
min-height: 96px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.item-attach-meta textarea {
|
||
grid-column: 1 / -1;
|
||
min-height: 72px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.equipment-edit-form input,
|
||
.equipment-edit-form select,
|
||
.equipment-edit-form textarea,
|
||
.item-attach-meta input,
|
||
.item-attach-meta select,
|
||
.item-attach-meta textarea {
|
||
width: 100%;
|
||
}
|
||
|
||
.equipment-edit-form-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.equipment-reorder-form {
|
||
display: none;
|
||
}
|
||
|
||
.order-btn {
|
||
min-width: 2.2rem;
|
||
padding-inline: 0.55rem;
|
||
}
|
||
|
||
.equip-title {
|
||
font-size: 1rem;
|
||
line-height: 1.3;
|
||
margin: 0;
|
||
}
|
||
|
||
.equip-stats {
|
||
display: flex;
|
||
gap: 0.45rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.equip-stat {
|
||
padding: 0.32rem 0.56rem;
|
||
border-radius: 999px;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
font-size: 0.77rem;
|
||
color: #f2f2f2;
|
||
}
|
||
|
||
.search-toolbar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.85rem;
|
||
}
|
||
|
||
.item-search-form {
|
||
display: grid;
|
||
gap: 0.9rem;
|
||
}
|
||
|
||
.search-top-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-controls-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 170px auto;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.source-switch {
|
||
display: inline-grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
padding: 0.28rem;
|
||
border-radius: 14px;
|
||
background: rgba(255,255,255,0.04);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
gap: 0.25rem;
|
||
width: fit-content;
|
||
}
|
||
|
||
.source-switch input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.source-switch label {
|
||
min-width: 155px;
|
||
min-height: 46px;
|
||
margin: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 0.7rem 0.9rem;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.82rem;
|
||
font-weight: 400;
|
||
line-height: 1.2;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
color: var(--text-soft);
|
||
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.source-switch label:hover {
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.source-switch input:checked + label {
|
||
background: rgba(162, 155, 120, 0.16);
|
||
border-color: rgba(162, 155, 120, 0.32);
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.search-input-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-results {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.search-results-summary,
|
||
.pagination-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
padding: 0.9rem 1rem;
|
||
}
|
||
|
||
.search-result {
|
||
padding: 0.95rem;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 360px;
|
||
gap: 1rem;
|
||
align-items: start;
|
||
}
|
||
|
||
body.show-item-preview .search-result {
|
||
grid-template-columns: 96px minmax(0, 1fr) 360px;
|
||
}
|
||
|
||
.search-result strong { display: block; margin-bottom: 0.25rem; }
|
||
|
||
.search-result-main {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.55rem;
|
||
}
|
||
|
||
.search-result-preview {
|
||
display: none;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
}
|
||
|
||
body.show-item-preview .search-result-preview {
|
||
display: flex;
|
||
}
|
||
|
||
.preview-container {
|
||
position: relative;
|
||
width: 80px;
|
||
height: 80px;
|
||
cursor: zoom-in;
|
||
}
|
||
|
||
.item-preview,
|
||
.item-preview-fallback {
|
||
width: 80px;
|
||
height: 80px;
|
||
object-fit: cover;
|
||
border-radius: 16px;
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
background: rgba(0,0,0,0.35);
|
||
display: block;
|
||
}
|
||
|
||
.item-preview-fallback {
|
||
display: grid;
|
||
place-items: center;
|
||
font-size: 1.2rem;
|
||
color: var(--text-soft);
|
||
}
|
||
|
||
.preview-floating {
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
position: absolute;
|
||
top: -10px;
|
||
left: 92px;
|
||
z-index: 20;
|
||
padding: 5px;
|
||
background: var(--card-bg);
|
||
border: 1px solid rgba(162, 155, 120, 0.4);
|
||
border-radius: 12px;
|
||
box-shadow: 0 14px 32px rgba(0,0,0,0.45);
|
||
backdrop-filter: blur(12px);
|
||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.preview-floating img {
|
||
width: 260px;
|
||
height: 260px;
|
||
object-fit: contain;
|
||
display: block;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.preview-container:hover .preview-floating,
|
||
.preview-container:focus-within .preview-floating {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
.item-attach-form {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 0.75rem;
|
||
align-self: stretch;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.item-attach-meta {
|
||
display: grid;
|
||
grid-template-columns: minmax(96px, 112px) minmax(0, 1fr);
|
||
gap: 0.55rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.item-attach-actions {
|
||
display: flex;
|
||
align-self: stretch;
|
||
}
|
||
|
||
.item-add-button {
|
||
min-width: 3rem;
|
||
min-height: 100%;
|
||
height: 100%;
|
||
padding: 0.4rem;
|
||
font-size: 1.55rem;
|
||
line-height: 1;
|
||
font-weight: 700;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.search-results-summary {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.preview-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.55rem;
|
||
font-size: 0.9rem;
|
||
color: var(--text-main);
|
||
user-select: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.preview-toggle input {
|
||
width: 1rem;
|
||
height: 1rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.search-result-range {
|
||
font-size: 0.88rem;
|
||
color: var(--text-soft);
|
||
}
|
||
|
||
.pagination-links {
|
||
display: flex;
|
||
gap: 0.55rem;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.pagination-label {
|
||
font-size: 0.88rem;
|
||
color: var(--text-soft);
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 1.2rem;
|
||
color: var(--text-soft);
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-state strong { color: var(--text-main); display: block; margin-bottom: 0.35rem; }
|
||
|
||
.nav-tabs {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
border-bottom: 1px solid var(--primary-border);
|
||
padding-bottom: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
@media (max-width: 1280px) {
|
||
.main-grid {
|
||
grid-template-columns: 340px minmax(0, 1fr);
|
||
}
|
||
|
||
.search-results-summary,
|
||
.pagination-bar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-result,
|
||
body.show-item-preview .search-result {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.search-result-preview {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.item-attach-form,
|
||
.item-attach-meta,
|
||
.search-controls-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.item-attach-actions {
|
||
justify-content: stretch;
|
||
}
|
||
|
||
.item-add-button {
|
||
width: 100%;
|
||
min-height: 2.75rem;
|
||
height: auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1180px) {
|
||
.main-grid,
|
||
.equipment-grid,
|
||
.edit-form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.characters-stack {
|
||
max-height: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.admin-layout { padding: 1rem; }
|
||
.form-grid,
|
||
.character-hero,
|
||
.search-input-row,
|
||
.source-switch {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.topbar-actions { width: 100%; justify-content: flex-start; }
|
||
.list-panel-header,
|
||
.hero-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.source-switch {
|
||
width: 100%;
|
||
}
|
||
.source-switch label {
|
||
min-width: 0;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="sccharacters-page">
|
||
<?php echo auth_render_page_access_widget('sccharacters.php', 'Personnages'); ?>
|
||
<div class="admin-layout">
|
||
<header class="admin-topbar">
|
||
<div class="topbar-info">
|
||
<h1>R.E.A.C.T. Characters Control</h1>
|
||
<p>Niveau d'accès : <strong><?php echo htmlspecialchars($role_label, ENT_QUOTES, 'UTF-8'); ?></strong></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('sccharacters.php'); ?>
|
||
|
||
<?php if ($flash_message !== ''): ?>
|
||
<div class="flash <?php echo $flash_type === 'success' ? 'success' : 'error'; ?>">
|
||
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="main-grid">
|
||
<aside class="sidebar-column">
|
||
<details class="panel accordion-panel" <?php echo $create_panel_open ? 'open' : ''; ?>>
|
||
<summary class="accordion-summary">
|
||
<div>
|
||
<h2 class="panel-title">Créer un personnage</h2>
|
||
<p class="panel-subtitle">Le formulaire est replié par défaut pour laisser un maximum de place à ta liste de personnages.</p>
|
||
</div>
|
||
<span class="accordion-indicator">+</span>
|
||
</summary>
|
||
<div class="accordion-body">
|
||
<form method="post" id="characterCreateForm">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="create_character">
|
||
|
||
<div class="form-grid">
|
||
<div class="field-full">
|
||
<label for="newCharacterName">Nom</label>
|
||
<input type="text" id="newCharacterName" name="character_name" maxlength="190" required value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field">
|
||
<label for="newCharacterRole">Rôle / Classe</label>
|
||
<input type="text" id="newCharacterRole" name="character_role" maxlength="190" value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field">
|
||
<label for="newCharacterFaction">Faction / Groupe</label>
|
||
<input type="text" id="newCharacterFaction" name="character_faction" maxlength="190" value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_faction'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field-full">
|
||
<label for="newCharacterAvatar">Avatar (URL d’image)</label>
|
||
<input type="url" id="newCharacterAvatar" name="character_avatar_url" placeholder="https://..." value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_avatar_url'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field-full">
|
||
<label for="newCharacterDescription">Description</label>
|
||
<textarea id="newCharacterDescription" name="character_description"><?php echo htmlspecialchars((string) $create_character['cl_sccharacter_description'], ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||
</div>
|
||
<div class="field-full">
|
||
<label for="newCharacterNotes">Notes privées</label>
|
||
<textarea id="newCharacterNotes" name="character_notes"><?php echo htmlspecialchars((string) $create_character['cl_sccharacter_notes'], ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||
</div>
|
||
<div class="field-full">
|
||
<div class="checkbox-row">
|
||
<input type="checkbox" id="newCharacterShareEnabled" name="character_share_enabled" value="1" <?php echo !empty($create_character['cl_sccharacter_share_enabled']) ? 'checked' : ''; ?>>
|
||
<label for="newCharacterShareEnabled">Activer l’accès public via le lien partagé</label>
|
||
</div>
|
||
</div>
|
||
<div class="field-full">
|
||
<div class="checkbox-row">
|
||
<span class="pin-indicator-input" aria-hidden="true">📌</span>
|
||
<input type="checkbox" id="newCharacterPinned" name="character_is_pinned" value="1" <?php echo !empty($create_character['cl_sccharacter_is_pinned']) ? 'checked' : ''; ?>>
|
||
<label for="newCharacterPinned">Toujours afficher ce personnage en haut de la liste</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn btn-primary">Créer le personnage</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
|
||
<div class="panel list-panel">
|
||
<div class="list-panel-header">
|
||
<div>
|
||
<h2 class="panel-title">Liste des personnages</h2>
|
||
<p class="panel-subtitle"><?php echo count($characters); ?> personnage(s). Sélectionne une fiche pour voir son détail, la modifier et lui attribuer son équipement.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="characters-stack">
|
||
<?php if ($characters === []): ?>
|
||
<div class="empty-state">
|
||
<strong>Aucun personnage pour le moment</strong>
|
||
Ouvre le bloc « Créer un personnage » ci-dessus pour commencer.
|
||
</div>
|
||
<?php else: ?>
|
||
<?php foreach ($characters as $character_row): ?>
|
||
<?php
|
||
$character_id = (int) $character_row['cl_sccharacter_id'];
|
||
$is_active = $selected_character && $character_id === (int) $selected_character['cl_sccharacter_id'];
|
||
$avatar_url = trim((string) ($character_row['cl_sccharacter_avatar_url'] ?? ''));
|
||
$initial = function_exists('mb_substr')
|
||
? mb_strtoupper(mb_substr((string) $character_row['cl_sccharacter_name'], 0, 1, 'UTF-8'), 'UTF-8')
|
||
: strtoupper(substr((string) $character_row['cl_sccharacter_name'], 0, 1));
|
||
$character_link = 'sccharacters.php?character=' . $character_id . '&item_source=' . rawurlencode($item_source);
|
||
if ($item_search !== '') {
|
||
$character_link .= '&item_search=' . rawurlencode($item_search);
|
||
}
|
||
?>
|
||
<a class="character-card<?php echo $is_active ? ' is-active' : ''; ?>" href="<?php echo htmlspecialchars($character_link, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<?php if ($avatar_url !== ''): ?>
|
||
<img class="character-avatar" src="<?php echo htmlspecialchars($avatar_url, ENT_QUOTES, 'UTF-8'); ?>" alt="Avatar de <?php echo htmlspecialchars((string) $character_row['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className: 'character-avatar-fallback', textContent: '<?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?>'}));">
|
||
<?php else: ?>
|
||
<div class="character-avatar-fallback"><?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?></div>
|
||
<?php endif; ?>
|
||
<div class="character-card-body">
|
||
<div class="character-card-title">
|
||
<h3><?php echo htmlspecialchars((string) $character_row['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h3>
|
||
<?php if (!empty($character_row['cl_sccharacter_is_pinned'])): ?>
|
||
<span class="pin-indicator" title="Personnage épinglé" aria-label="Personnage épinglé">📌</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="badge-row" style="margin:0.42rem 0 0.52rem;">
|
||
<?php if (trim((string) $character_row['cl_sccharacter_role']) !== ''): ?>
|
||
<span class="badge badge-primary"><?php echo htmlspecialchars((string) $character_row['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php endif; ?>
|
||
<?php if (trim((string) $character_row['cl_sccharacter_faction']) !== ''): ?>
|
||
<span class="badge"><?php echo htmlspecialchars((string) $character_row['cl_sccharacter_faction'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php endif; ?>
|
||
<span class="badge badge-muted"><?php echo (int) $character_row['cl_sccharacter_item_count']; ?> objet(s)</span>
|
||
<?php if (!empty($character_row['cl_sccharacter_share_enabled'])): ?>
|
||
<span class="badge badge-success">Partage actif</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="muted">
|
||
<?php echo htmlspecialchars(trim((string) $character_row['cl_sccharacter_description']) !== '' ? sccharacters_excerpt((string) $character_row['cl_sccharacter_description'], 110) : 'Aucune description pour le moment.', ENT_QUOTES, 'UTF-8'); ?>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="right-column">
|
||
<?php if ($selected_character): ?>
|
||
<?php
|
||
$hero_avatar = trim((string) ($selected_character['cl_sccharacter_avatar_url'] ?? ''));
|
||
$hero_initial = function_exists('mb_substr')
|
||
? mb_strtoupper(mb_substr((string) $selected_character['cl_sccharacter_name'], 0, 1, 'UTF-8'), 'UTF-8')
|
||
: strtoupper(substr((string) $selected_character['cl_sccharacter_name'], 0, 1));
|
||
?>
|
||
<div class="panel">
|
||
<div class="character-hero">
|
||
<?php if ($hero_avatar !== ''): ?>
|
||
<img class="character-hero-avatar" src="<?php echo htmlspecialchars($hero_avatar, ENT_QUOTES, 'UTF-8'); ?>" alt="Avatar de <?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className: 'character-hero-fallback', textContent: '<?php echo htmlspecialchars($hero_initial, ENT_QUOTES, 'UTF-8'); ?>'}));">
|
||
<?php else: ?>
|
||
<div class="character-hero-fallback"><?php echo htmlspecialchars($hero_initial, ENT_QUOTES, 'UTF-8'); ?></div>
|
||
<?php endif; ?>
|
||
<div>
|
||
<div class="hero-header">
|
||
<div>
|
||
<div class="hero-name-row">
|
||
<h2 class="character-hero-name"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h2>
|
||
<?php if (!empty($selected_character['cl_sccharacter_is_pinned'])): ?>
|
||
<span class="pin-indicator" title="Personnage épinglé" aria-label="Personnage épinglé">📌</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="hero-meta">
|
||
<?php if (trim((string) $selected_character['cl_sccharacter_role']) !== ''): ?>
|
||
<span class="badge badge-primary"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php endif; ?>
|
||
<?php if (trim((string) $selected_character['cl_sccharacter_faction']) !== ''): ?>
|
||
<span class="badge"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_faction'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php endif; ?>
|
||
<span class="badge badge-muted"><?php echo count($selected_character_items); ?> équipement(s)</span>
|
||
</div>
|
||
</div>
|
||
<span class="badge <?php echo !empty($selected_character['cl_sccharacter_share_enabled']) ? 'badge-success' : 'badge-muted'; ?>">
|
||
<?php echo !empty($selected_character['cl_sccharacter_share_enabled']) ? 'Lien public actif' : 'Lien public désactivé'; ?>
|
||
</span>
|
||
</div>
|
||
|
||
<p class="muted">
|
||
<?php echo nl2br(htmlspecialchars(trim((string) $selected_character['cl_sccharacter_description']) !== '' ? (string) $selected_character['cl_sccharacter_description'] : 'Aucune description publique renseignée pour ce personnage.', ENT_QUOTES, 'UTF-8')); ?>
|
||
</p>
|
||
|
||
<div class="share-box">
|
||
<div class="badge-row">
|
||
<span class="badge badge-muted">Partage par lien uniquement</span>
|
||
<span class="badge badge-muted">Page publique dédiée</span>
|
||
</div>
|
||
<input type="text" id="shareLinkField" readonly value="<?php echo htmlspecialchars(sccharacters_share_url((string) $selected_character['cl_sccharacter_share_token']), ENT_QUOTES, 'UTF-8'); ?>">
|
||
<div class="btn-row" style="margin-top:0.75rem;">
|
||
<button type="button" class="btn btn-secondary btn-small" onclick="copyShareLink()">Copier le lien</button>
|
||
<form method="post" style="display:inline-flex;">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="regenerate_share_token">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<button type="submit" class="btn btn-secondary btn-small">Régénérer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<details class="panel accordion-panel">
|
||
<summary class="accordion-summary">
|
||
<div>
|
||
<h2 class="panel-title">Modifier la fiche</h2>
|
||
<p class="panel-subtitle">Bloc repliable pour garder la fiche visible sans laisser le formulaire ouvert en permanence.</p>
|
||
</div>
|
||
<span class="accordion-indicator">+</span>
|
||
</summary>
|
||
<div class="accordion-body">
|
||
<form method="post" id="characterEditForm">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="update_character">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="category_order_state" id="characterCategoryOrderState" value="<?php echo htmlspecialchars($selected_character_visible_category_order_state, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_order_state" id="characterItemOrderState" value="<?php echo htmlspecialchars($selected_character_item_order_state_json, ENT_QUOTES, 'UTF-8'); ?>">
|
||
|
||
<div class="edit-form-grid">
|
||
<div class="field-full">
|
||
<label for="editCharacterName">Nom</label>
|
||
<input type="text" id="editCharacterName" name="character_name" maxlength="190" required value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field">
|
||
<label for="editCharacterRole">Rôle / Classe</label>
|
||
<input type="text" id="editCharacterRole" name="character_role" maxlength="190" value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field">
|
||
<label for="editCharacterFaction">Faction / Groupe</label>
|
||
<input type="text" id="editCharacterFaction" name="character_faction" maxlength="190" value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_faction'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field-full">
|
||
<label for="editCharacterAvatar">Avatar (URL d’image)</label>
|
||
<input type="url" id="editCharacterAvatar" name="character_avatar_url" placeholder="https://..." value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_avatar_url'], ENT_QUOTES, 'UTF-8'); ?>">
|
||
</div>
|
||
<div class="field-full">
|
||
<label for="editCharacterDescription">Description</label>
|
||
<textarea id="editCharacterDescription" name="character_description"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_description'], ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||
</div>
|
||
<div class="field-full">
|
||
<label for="editCharacterNotes">Notes privées</label>
|
||
<textarea id="editCharacterNotes" name="character_notes"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_notes'], ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||
</div>
|
||
<div class="field-full">
|
||
<div class="checkbox-row">
|
||
<input type="checkbox" id="editCharacterShareEnabled" name="character_share_enabled" value="1" <?php echo !empty($selected_character['cl_sccharacter_share_enabled']) ? 'checked' : ''; ?>>
|
||
<label for="editCharacterShareEnabled">Activer l’accès public via le lien partagé</label>
|
||
</div>
|
||
</div>
|
||
<div class="field-full">
|
||
<div class="checkbox-row">
|
||
<span class="pin-indicator-input" aria-hidden="true">📌</span>
|
||
<input type="checkbox" id="editCharacterPinned" name="character_is_pinned" value="1" <?php echo !empty($selected_character['cl_sccharacter_is_pinned']) ? 'checked' : ''; ?>>
|
||
<label for="editCharacterPinned">Toujours afficher ce personnage en haut de la liste</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||
<span class="muted order-state-indicator" id="characterOrderStateIndicator" hidden>Des changements d’ordre sont en attente d’enregistrement.</span>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="btn-row">
|
||
<form method="post" style="display:inline-flex;">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="delete_character">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<button type="submit" class="btn btn-danger" onclick="return confirm('Supprimer ce personnage et son équipement ?');">Supprimer le personnage</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="panel accordion-panel" open>
|
||
<summary class="accordion-summary">
|
||
<div>
|
||
<h2 class="panel-title">Équipement attribué</h2>
|
||
<p class="panel-subtitle">Bloc repliable pour consulter rapidement l’équipement et le refermer quand tu veux dégager la vue.</p>
|
||
</div>
|
||
<span class="accordion-indicator">+</span>
|
||
</summary>
|
||
<div class="accordion-body">
|
||
|
||
<?php if ($selected_character_items === []): ?>
|
||
<div class="empty-state">
|
||
<strong>Aucun équipement attribué</strong>
|
||
Utilise le module de recherche plus bas pour ajouter un objet de la base ou un objet personnalisé.
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="equipment-order-toolbar">
|
||
<span class="muted">Tu peux réorganiser librement les catégories et les objets ci-dessous, puis enregistrer quand tu veux.</span>
|
||
<button type="submit" form="characterEditForm" class="btn btn-primary btn-small">Enregistrer l’ordre</button>
|
||
</div>
|
||
<div class="equipment-sections" id="characterEquipmentSections">
|
||
<?php foreach ($selected_character_items_by_category as $category_key => $category_items): ?>
|
||
<?php $category_order_meta = $selected_character_category_order_meta[$category_key] ?? ['is_first' => false, 'is_last' => false]; ?>
|
||
<section class="equipment-section" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<div class="equipment-section-heading">
|
||
<div class="equipment-section-heading-main">
|
||
<h3 class="equipment-section-title"><?php echo htmlspecialchars(sccharacters_item_category_label((string) $category_key), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||
<span class="badge badge-muted"><?php echo count($category_items); ?> objet(s)</span>
|
||
</div>
|
||
<div class="equipment-category-order-controls" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<button type="button" class="btn btn-secondary btn-small order-btn category-order-btn" data-move-direction="up" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>" title="Afficher cette catégorie plus haut" aria-label="Afficher cette catégorie plus haut" <?php echo $category_order_meta['is_first'] ? 'disabled' : ''; ?>>↑</button>
|
||
<button type="button" class="btn btn-secondary btn-small order-btn category-order-btn" data-move-direction="down" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>" title="Afficher cette catégorie plus bas" aria-label="Afficher cette catégorie plus bas" <?php echo $category_order_meta['is_last'] ? 'disabled' : ''; ?>>↓</button>
|
||
</div>
|
||
</div>
|
||
<div class="equipment-grid equipment-grid-sortable" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<?php foreach ($category_items as $character_item_row): ?>
|
||
<?php
|
||
$is_custom = ($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
|
||
$item_name = $is_custom
|
||
? (string) ($character_item_row['cl_sccharacteritem_custom_name'] ?? 'Objet personnalisé indisponible')
|
||
: (string) ($character_item_row['cl_sccharacteritem_base_name'] ?? 'Objet indisponible');
|
||
$item_type = $is_custom
|
||
? (string) ($character_item_row['cl_sccharacteritem_custom_type'] ?? '')
|
||
: (string) ($character_item_row['cl_sccharacteritem_base_type'] ?? '');
|
||
$item_subtype = $is_custom
|
||
? (string) ($character_item_row['cl_sccharacteritem_custom_subtype'] ?? '')
|
||
: (string) ($character_item_row['cl_sccharacteritem_base_subtype'] ?? '');
|
||
$item_uuid = $is_custom
|
||
? (string) ($character_item_row['cl_sccharacteritem_custom_uuid'] ?? '')
|
||
: (string) ($character_item_row['cl_sccharacteritem_base_uuid'] ?? '');
|
||
$item_category = sccharacters_resolve_item_category(
|
||
(string) ($character_item_row['cl_sccharacteritem_slot'] ?? ''),
|
||
$item_type,
|
||
$item_subtype
|
||
);
|
||
$item_quantity = sccharacters_normalize_item_quantity($character_item_row['cl_sccharacteritem_quantity'] ?? null);
|
||
$item_title = $item_quantity !== null ? $item_quantity . 'x ' . $item_name : $item_name;
|
||
$item_note = trim((string) ($character_item_row['cl_sccharacteritem_note'] ?? ''));
|
||
$item_stats = [];
|
||
if ($is_custom) {
|
||
$item_stats = $custom_stats_by_itemcustom[(int) ($character_item_row['cl_sccharacteritem_scitemcustom_id'] ?? 0)] ?? [];
|
||
}
|
||
?>
|
||
<article class="equip-card" draggable="true" data-item-id="<?php echo (int) $character_item_row['cl_sccharacteritem_id']; ?>">
|
||
<?php if ($item_uuid !== ''): ?>
|
||
<img class="equip-thumb" src="https://cstone.space/uifimages/<?php echo htmlspecialchars($item_uuid, ENT_QUOTES, 'UTF-8'); ?>.png" alt="Aperçu de <?php echo htmlspecialchars($item_name, ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className: 'equip-thumb-fallback', textContent: '◈'}));">
|
||
<?php else: ?>
|
||
<div class="equip-thumb-fallback">◈</div>
|
||
<?php endif; ?>
|
||
<div class="equip-content">
|
||
<div>
|
||
<div class="equip-title-row">
|
||
<h3 class="equip-title"><?php echo htmlspecialchars($item_title, ENT_QUOTES, 'UTF-8'); ?></h3>
|
||
<span class="drag-handle" title="Glisser pour changer l’ordre" aria-hidden="true">⋮⋮</span>
|
||
</div>
|
||
<div class="equip-meta" style="margin-top:0.45rem;">
|
||
<span class="badge <?php echo $is_custom ? 'badge-primary' : 'badge-muted'; ?>"><?php echo $is_custom ? 'Objet perso.' : 'Base d’objets'; ?></span>
|
||
<span class="badge"><?php echo htmlspecialchars(sccharacters_item_category_label($item_category), ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php if ($item_type !== ''): ?><span class="badge badge-muted"><?php echo htmlspecialchars($item_type, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
|
||
<?php if ($item_subtype !== ''): ?><span class="badge badge-muted"><?php echo htmlspecialchars($item_subtype, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($item_stats !== []): ?>
|
||
<div class="equip-stats">
|
||
<?php foreach ($item_stats as $stat_row): ?>
|
||
<?php
|
||
$sign = (string) ($stat_row['cl_scitemcustomstat_sign'] ?? '');
|
||
$value = rtrim(rtrim(number_format((float) ($stat_row['cl_scitemcustomstat_value'] ?? 0), 2, '.', ''), '0'), '.');
|
||
if ($value === '') {
|
||
$value = '0';
|
||
}
|
||
?>
|
||
<span class="equip-stat"><?php echo htmlspecialchars((string) $stat_row['cl_scstatsitem_name'], ENT_QUOTES, 'UTF-8'); ?> : <?php echo htmlspecialchars($sign . $value . ' ' . (string) $stat_row['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($item_note !== ''): ?>
|
||
<div class="muted"><?php echo nl2br(htmlspecialchars($item_note, ENT_QUOTES, 'UTF-8')); ?></div>
|
||
<?php endif; ?>
|
||
|
||
<div class="equipment-actions">
|
||
<span class="drag-hint muted">Glisser-déposer pour réordonner dans cette catégorie</span>
|
||
<div class="equipment-action-buttons">
|
||
<details class="equipment-edit">
|
||
<summary class="btn btn-secondary btn-small equipment-edit-summary" title="Modifier l’objet" aria-label="Modifier l’objet"><span aria-hidden="true">✎</span></summary>
|
||
<form method="post" class="equipment-edit-form">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="update_character_item">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="character_item_id" value="<?php echo (int) $character_item_row['cl_sccharacteritem_id']; ?>">
|
||
<input type="hidden" name="item_source_context" value="<?php echo htmlspecialchars($item_source, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_search_context" value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_page_context" value="<?php echo (int) $item_page; ?>">
|
||
<input type="hidden" name="item_per_page_context" value="<?php echo (int) $item_per_page; ?>">
|
||
<input type="hidden" name="item_panel_context" value="<?php echo ($item_search !== '' || $item_panel_open) ? '1' : '0'; ?>">
|
||
<div class="equipment-edit-grid">
|
||
<input type="number" name="item_quantity" min="1" step="1" inputmode="numeric" placeholder="Quantité" value="<?php echo $item_quantity !== null ? (int) $item_quantity : ''; ?>" aria-label="Quantité de l'objet">
|
||
<select name="item_slot" aria-label="Catégorie de l'objet">
|
||
<?php foreach ($item_category_options as $category_value => $category_label): ?>
|
||
<option value="<?php echo htmlspecialchars($category_value, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $item_category === $category_value ? 'selected' : ''; ?>>
|
||
<?php echo htmlspecialchars($category_label, ENT_QUOTES, 'UTF-8'); ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<textarea name="item_note" class="item-note-field" rows="4" placeholder="Note optionnelle"><?php echo htmlspecialchars($item_note, ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||
</div>
|
||
<div class="equipment-edit-form-actions">
|
||
<button type="submit" class="btn btn-primary btn-small">Enregistrer</button>
|
||
</div>
|
||
</form>
|
||
</details>
|
||
<form method="post" style="display:inline-flex;">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="delete_character_item">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="character_item_id" value="<?php echo (int) $character_item_row['cl_sccharacteritem_id']; ?>">
|
||
<input type="hidden" name="item_source_context" value="<?php echo htmlspecialchars($item_source, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_search_context" value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_page_context" value="<?php echo (int) $item_page; ?>">
|
||
<input type="hidden" name="item_per_page_context" value="<?php echo (int) $item_per_page; ?>">
|
||
<input type="hidden" name="item_panel_context" value="<?php echo ($item_search !== '' || $item_panel_open) ? '1' : '0'; ?>">
|
||
<button type="submit" class="btn btn-danger btn-small equipment-icon-button" title="Retirer l’objet" aria-label="Retirer l’objet" onclick="return confirm('Retirer cet objet du personnage ?');"><span aria-hidden="true">🗑</span></button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="panel accordion-panel" <?php echo ($item_search !== '' || $item_panel_open) ? 'open' : ''; ?>>
|
||
<summary class="accordion-summary">
|
||
<div>
|
||
<h2 class="panel-title">Ajouter des objets</h2>
|
||
<p class="panel-subtitle">Recherche repliable : tu l’ouvres quand tu équipes le personnage, tu la refermes dès que c’est fait.</p>
|
||
</div>
|
||
<span class="accordion-indicator">+</span>
|
||
</summary>
|
||
<div class="accordion-body">
|
||
<div class="search-toolbar">
|
||
<form method="get" class="item-search-form" id="itemSearchForm" data-character-id="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="character" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="item_panel" value="1">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
|
||
<div class="search-top-row">
|
||
<div class="source-switch" role="radiogroup" aria-label="Source de recherche d’objet">
|
||
<input type="radio" name="item_source" id="itemSourceBase" value="base" <?php echo $item_source === 'base' ? 'checked' : ''; ?>>
|
||
<label for="itemSourceBase">Base d’objets</label>
|
||
|
||
<input type="radio" name="item_source" id="itemSourceCustom" value="custom" <?php echo $item_source === 'custom' ? 'checked' : ''; ?>>
|
||
<label for="itemSourceCustom">Objets perso.</label>
|
||
</div>
|
||
|
||
<label class="preview-toggle" for="itemPreviewToggle">
|
||
<input type="checkbox" id="itemPreviewToggle">
|
||
<span>Afficher l’aperçu</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="search-controls-row">
|
||
<input type="text" name="item_search" placeholder="Nom, type, subtype, UUID..." value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<select name="item_per_page" aria-label="Nombre de résultats affichés">
|
||
<?php foreach (sccharacters_allowed_item_per_page() as $per_page_option): ?>
|
||
<option value="<?php echo (int) $per_page_option; ?>" <?php echo $item_per_page === $per_page_option ? 'selected' : ''; ?>>
|
||
<?php echo (int) $per_page_option; ?> résultats / page
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="btn btn-primary btn-small">Rechercher</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="search-results">
|
||
<?php if ($item_results === []): ?>
|
||
<div class="empty-state">
|
||
<strong><?php echo $item_source === 'custom' ? 'Aucun objet personnalisé trouvé' : 'Aucun objet trouvé'; ?></strong>
|
||
<?php if ($item_source === 'custom'): ?>
|
||
<?php if ($item_search !== ''): ?>
|
||
Aucun objet personnalisé ne correspond à « <?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?> ».
|
||
<?php else: ?>
|
||
Commence par créer des objets dans l’onglet « Objets perso. » pour pouvoir les attribuer à tes personnages.
|
||
<?php endif; ?>
|
||
<?php else: ?>
|
||
<?php if ($item_search !== ''): ?>
|
||
Aucun objet de la base ne correspond à « <?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?> ».
|
||
<?php else: ?>
|
||
Lance une recherche ou parcours le catalogue page par page avec le sélecteur de résultats.
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="search-results-summary panel panel-soft">
|
||
<div>
|
||
<strong>Résultats de recherche</strong>
|
||
<div class="search-result-range">
|
||
<?php echo (int) $item_result_offset_start; ?>–<?php echo (int) $item_result_offset_end; ?> sur <?php echo (int) $item_total_results; ?> résultat(s)
|
||
· page <?php echo (int) $item_page; ?> / <?php echo (int) $item_total_pages; ?>
|
||
</div>
|
||
</div>
|
||
<div class="muted">Le bouton <strong>+</strong> ajoute l’objet immédiatement au personnage sans casser ta recherche en cours.</div>
|
||
</div>
|
||
|
||
<?php foreach ($item_results as $item_result_row): ?>
|
||
<?php
|
||
$result_is_custom = $item_source === 'custom';
|
||
$result_name = (string) $item_result_row['cl_scobjs_name'];
|
||
$result_type = trim(((string) $item_result_row['cl_scobjs_type']) . ' · ' . ((string) $item_result_row['cl_scobjs_subtype']), ' ·');
|
||
$recommended_category = sccharacters_resolve_item_category(
|
||
'',
|
||
(string) ($item_result_row['cl_scobjs_type'] ?? ''),
|
||
(string) ($item_result_row['cl_scobjs_subtype'] ?? '')
|
||
);
|
||
$result_id = $result_is_custom
|
||
? (int) ($item_result_row['cl_scitemcustom_id'] ?? 0)
|
||
: (int) ($item_result_row['cl_scobjs_id'] ?? 0);
|
||
$item_action = $result_is_custom ? 'add_custom_item' : 'add_base_item';
|
||
$item_id_field = $result_is_custom ? 'custom_item_id' : 'base_obj_id';
|
||
$item_uuid = (string) ($item_result_row['cl_scobjs_uuid'] ?? '');
|
||
$item_image_url = $item_uuid !== '' ? 'https://cstone.space/uifimages/' . rawurlencode($item_uuid) . '.png' : '';
|
||
?>
|
||
<article class="search-result panel panel-soft">
|
||
<div class="search-result-preview">
|
||
<?php if ($item_image_url !== ''): ?>
|
||
<div class="preview-container">
|
||
<img src="<?php echo htmlspecialchars($item_image_url, ENT_QUOTES, 'UTF-8'); ?>" class="item-preview" alt="Aperçu de <?php echo htmlspecialchars($result_name, ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className:'item-preview-fallback', textContent:'◈'}));">
|
||
<div class="preview-floating" aria-hidden="true">
|
||
<img src="<?php echo htmlspecialchars($item_image_url, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy" onerror="this.closest('.preview-floating').remove();">
|
||
</div>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="item-preview-fallback">◈</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="search-result-main">
|
||
<div>
|
||
<strong><?php echo htmlspecialchars($result_name, ENT_QUOTES, 'UTF-8'); ?></strong>
|
||
<div class="muted"><?php echo htmlspecialchars($result_type !== '' ? $result_type : 'Type non renseigné', ENT_QUOTES, 'UTF-8'); ?></div>
|
||
</div>
|
||
<div class="badge-row">
|
||
<span class="badge <?php echo $result_is_custom ? 'badge-primary' : 'badge-muted'; ?>"><?php echo $result_is_custom ? 'Objet perso.' : 'Base d’objets'; ?></span>
|
||
<?php if ($item_uuid !== ''): ?>
|
||
<span class="badge badge-muted">UUID : <?php echo htmlspecialchars($item_uuid, ENT_QUOTES, 'UTF-8'); ?></span>
|
||
<?php endif; ?>
|
||
<?php if ($result_is_custom): ?>
|
||
<span class="badge badge-muted"><?php echo (int) $item_result_row['cl_scitemcustom_stat_count']; ?> stat(s)</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<form method="post" class="item-attach-form">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="<?php echo htmlspecialchars($item_action, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="<?php echo htmlspecialchars($item_id_field, ENT_QUOTES, 'UTF-8'); ?>" value="<?php echo (int) $result_id; ?>">
|
||
<input type="hidden" name="item_source_context" value="<?php echo htmlspecialchars($item_source, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_search_context" value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="item_page_context" value="<?php echo (int) $item_page; ?>">
|
||
<input type="hidden" name="item_per_page_context" value="<?php echo (int) $item_per_page; ?>">
|
||
|
||
<div class="item-attach-meta">
|
||
<input type="number" name="item_quantity" min="1" step="1" inputmode="numeric" placeholder="Quantité" aria-label="Quantité de l'objet">
|
||
<select name="item_slot" aria-label="Catégorie de l'objet">
|
||
<?php foreach ($item_category_options as $category_value => $category_label): ?>
|
||
<option value="<?php echo htmlspecialchars($category_value, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $recommended_category === $category_value ? 'selected' : ''; ?>>
|
||
<?php echo htmlspecialchars($category_label, ENT_QUOTES, 'UTF-8'); ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<textarea name="item_note" class="item-note-field" rows="2" placeholder="Note optionnelle"></textarea>
|
||
</div>
|
||
|
||
<div class="item-attach-actions">
|
||
<button type="submit" class="btn btn-primary item-add-button" title="Ajouter immédiatement" aria-label="Ajouter <?php echo htmlspecialchars($result_name, ENT_QUOTES, 'UTF-8'); ?> au personnage">+</button>
|
||
</div>
|
||
</form>
|
||
</article>
|
||
<?php endforeach; ?>
|
||
|
||
<div class="pagination-bar panel panel-soft">
|
||
<div class="pagination-label">Navigation dans les résultats</div>
|
||
<div class="pagination-links">
|
||
<?php
|
||
$prev_params = $item_query_base_params;
|
||
$next_params = $item_query_base_params;
|
||
if ($item_has_previous_page) {
|
||
if (($item_page - 1) > 1) {
|
||
$prev_params['item_page'] = $item_page - 1;
|
||
}
|
||
}
|
||
if ($item_has_next_page) {
|
||
$next_params['item_page'] = $item_page + 1;
|
||
}
|
||
?>
|
||
<?php if ($item_has_previous_page): ?>
|
||
<a class="btn-link btn-small btn-secondary" href="sccharacters.php?<?php echo htmlspecialchars(http_build_query($prev_params), ENT_QUOTES, 'UTF-8'); ?>">← Page précédente</a>
|
||
<?php endif; ?>
|
||
<?php if ($item_has_next_page): ?>
|
||
<a class="btn-link btn-small btn-secondary" href="sccharacters.php?<?php echo htmlspecialchars(http_build_query($next_params), ENT_QUOTES, 'UTF-8'); ?>">Page suivante →</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div> </details>
|
||
|
||
<?php else: ?>
|
||
<div class="empty-state">
|
||
<strong>Aucun personnage sélectionné</strong>
|
||
Crée un personnage via le bloc repliable de gauche, puis sélectionne-le dans la liste pour gérer sa fiche et son équipement.
|
||
</div>
|
||
<?php endif; ?>
|
||
</section>
|
||
</div>
|
||
<script>
|
||
function copyShareLink() {
|
||
const input = document.getElementById('shareLinkField');
|
||
if (!input) {
|
||
return;
|
||
}
|
||
|
||
input.select();
|
||
input.setSelectionRange(0, input.value.length);
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(input.value).catch(() => document.execCommand('copy'));
|
||
} else {
|
||
document.execCommand('copy');
|
||
}
|
||
}
|
||
|
||
const itemSearchForm = document.getElementById('itemSearchForm');
|
||
const itemPreviewToggle = document.getElementById('itemPreviewToggle');
|
||
const itemSourceInputs = Array.from(document.querySelectorAll('input[name="item_source"]'));
|
||
const previewStorageKey = 'sccharacters-item-preview-v2';
|
||
|
||
function setPreviewState(enabled) {
|
||
document.body.classList.toggle('show-item-preview', enabled);
|
||
|
||
if (itemPreviewToggle) {
|
||
itemPreviewToggle.checked = enabled;
|
||
}
|
||
}
|
||
|
||
let previewEnabled = true;
|
||
|
||
try {
|
||
const storedPreviewState = window.localStorage.getItem(previewStorageKey);
|
||
previewEnabled = storedPreviewState === null ? true : storedPreviewState === '1';
|
||
} catch (error) {
|
||
previewEnabled = true;
|
||
}
|
||
|
||
setPreviewState(previewEnabled);
|
||
|
||
if (itemPreviewToggle) {
|
||
itemPreviewToggle.addEventListener('change', () => {
|
||
const enabled = itemPreviewToggle.checked;
|
||
setPreviewState(enabled);
|
||
|
||
try {
|
||
window.localStorage.setItem(previewStorageKey, enabled ? '1' : '0');
|
||
} catch (error) {
|
||
// Ignore storage issues silently.
|
||
}
|
||
});
|
||
}
|
||
|
||
itemSourceInputs.forEach((input) => {
|
||
input.addEventListener('change', () => {
|
||
if (itemSearchForm) {
|
||
itemSearchForm.submit();
|
||
}
|
||
});
|
||
});
|
||
|
||
function initEquipmentOrdering() {
|
||
const editForm = document.getElementById('characterEditForm');
|
||
const categoryOrderInput = document.getElementById('characterCategoryOrderState');
|
||
const itemOrderInput = document.getElementById('characterItemOrderState');
|
||
const orderStateIndicator = document.getElementById('characterOrderStateIndicator');
|
||
const equipmentSections = document.getElementById('characterEquipmentSections');
|
||
const categoryButtons = Array.from(document.querySelectorAll('.category-order-btn'));
|
||
const grids = Array.from(document.querySelectorAll('.equipment-grid-sortable'));
|
||
|
||
if (!editForm || !categoryOrderInput || !itemOrderInput || !equipmentSections) {
|
||
return;
|
||
}
|
||
|
||
const readItemOrderState = () => {
|
||
try {
|
||
const parsed = JSON.parse(itemOrderInput.value || '{}');
|
||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
};
|
||
|
||
let hasPendingOrderChanges = false;
|
||
|
||
const markPendingOrderChanges = () => {
|
||
hasPendingOrderChanges = true;
|
||
if (orderStateIndicator) {
|
||
orderStateIndicator.hidden = false;
|
||
}
|
||
};
|
||
|
||
const syncCategoryButtonState = () => {
|
||
const sections = Array.from(equipmentSections.querySelectorAll('.equipment-section[data-category-key]'));
|
||
const firstKey = sections[0]?.dataset.categoryKey || '';
|
||
const lastKey = sections[sections.length - 1]?.dataset.categoryKey || '';
|
||
|
||
categoryButtons.forEach((button) => {
|
||
const categoryKey = button.dataset.categoryKey || '';
|
||
const direction = button.dataset.moveDirection || '';
|
||
button.disabled = (direction === 'up' && categoryKey === firstKey)
|
||
|| (direction === 'down' && categoryKey === lastKey);
|
||
});
|
||
};
|
||
|
||
const syncCategoryOrderState = () => {
|
||
const orderedKeys = Array.from(equipmentSections.querySelectorAll('.equipment-section[data-category-key]'))
|
||
.map((section) => section.dataset.categoryKey || '')
|
||
.filter(Boolean);
|
||
categoryOrderInput.value = orderedKeys.join(',');
|
||
syncCategoryButtonState();
|
||
};
|
||
|
||
const syncItemOrderStateForGrid = (grid) => {
|
||
const categoryKey = grid.dataset.categoryKey || '';
|
||
if (!categoryKey) {
|
||
return;
|
||
}
|
||
|
||
const state = readItemOrderState();
|
||
state[categoryKey] = Array.from(grid.querySelectorAll('.equip-card[data-item-id]'))
|
||
.map((card) => Number(card.dataset.itemId || 0))
|
||
.filter((itemId) => Number.isInteger(itemId) && itemId > 0);
|
||
itemOrderInput.value = JSON.stringify(state);
|
||
};
|
||
|
||
categoryButtons.forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const categoryKey = button.dataset.categoryKey || '';
|
||
const direction = button.dataset.moveDirection || '';
|
||
const section = equipmentSections.querySelector(`.equipment-section[data-category-key="${categoryKey}"]`);
|
||
if (!section) {
|
||
return;
|
||
}
|
||
|
||
if (direction === 'up') {
|
||
const previousSection = section.previousElementSibling;
|
||
if (previousSection) {
|
||
equipmentSections.insertBefore(section, previousSection);
|
||
}
|
||
} else if (direction === 'down') {
|
||
const nextSection = section.nextElementSibling;
|
||
if (nextSection) {
|
||
equipmentSections.insertBefore(nextSection, section);
|
||
}
|
||
}
|
||
|
||
syncCategoryOrderState();
|
||
markPendingOrderChanges();
|
||
});
|
||
});
|
||
|
||
grids.forEach((grid) => {
|
||
const cards = Array.from(grid.querySelectorAll('.equip-card[data-item-id]'));
|
||
if (cards.length < 2) {
|
||
syncItemOrderStateForGrid(grid);
|
||
return;
|
||
}
|
||
|
||
let draggedCard = null;
|
||
let originalOrder = cards.map((card) => card.dataset.itemId).join(',');
|
||
|
||
const currentOrder = () => Array.from(grid.querySelectorAll('.equip-card[data-item-id]'))
|
||
.map((card) => card.dataset.itemId)
|
||
.filter(Boolean)
|
||
.join(',');
|
||
|
||
const clearDragMarkers = () => {
|
||
grid.querySelectorAll('.equip-card.drag-over').forEach((card) => card.classList.remove('drag-over'));
|
||
};
|
||
|
||
cards.forEach((card) => {
|
||
card.addEventListener('dragstart', (event) => {
|
||
draggedCard = card;
|
||
originalOrder = currentOrder();
|
||
card.classList.add('dragging');
|
||
if (event.dataTransfer) {
|
||
event.dataTransfer.effectAllowed = 'move';
|
||
event.dataTransfer.setData('text/plain', card.dataset.itemId || '');
|
||
}
|
||
});
|
||
|
||
card.addEventListener('dragover', (event) => {
|
||
if (!draggedCard || draggedCard === card || draggedCard.parentElement !== grid) {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
clearDragMarkers();
|
||
card.classList.add('drag-over');
|
||
|
||
const rect = card.getBoundingClientRect();
|
||
const insertBefore = event.clientY < rect.top + (rect.height / 2);
|
||
grid.insertBefore(draggedCard, insertBefore ? card : card.nextSibling);
|
||
});
|
||
|
||
card.addEventListener('dragleave', () => {
|
||
card.classList.remove('drag-over');
|
||
});
|
||
|
||
card.addEventListener('drop', (event) => {
|
||
event.preventDefault();
|
||
card.classList.remove('drag-over');
|
||
});
|
||
|
||
card.addEventListener('dragend', () => {
|
||
card.classList.remove('dragging');
|
||
clearDragMarkers();
|
||
|
||
if (!draggedCard) {
|
||
return;
|
||
}
|
||
|
||
const updatedOrder = currentOrder();
|
||
draggedCard = null;
|
||
|
||
if (updatedOrder !== '' && updatedOrder !== originalOrder) {
|
||
syncItemOrderStateForGrid(grid);
|
||
markPendingOrderChanges();
|
||
}
|
||
});
|
||
});
|
||
|
||
grid.addEventListener('dragover', (event) => {
|
||
if (!draggedCard || draggedCard.parentElement !== grid) {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
const targetCard = event.target.closest('.equip-card[data-item-id]');
|
||
if (!targetCard) {
|
||
grid.appendChild(draggedCard);
|
||
}
|
||
});
|
||
|
||
grid.addEventListener('drop', (event) => {
|
||
event.preventDefault();
|
||
clearDragMarkers();
|
||
});
|
||
|
||
syncItemOrderStateForGrid(grid);
|
||
});
|
||
|
||
editForm.addEventListener('submit', () => {
|
||
syncCategoryOrderState();
|
||
grids.forEach((grid) => syncItemOrderStateForGrid(grid));
|
||
hasPendingOrderChanges = false;
|
||
if (orderStateIndicator) {
|
||
orderStateIndicator.hidden = true;
|
||
}
|
||
});
|
||
|
||
syncCategoryOrderState();
|
||
}
|
||
|
||
initEquipmentOrdering();
|
||
</script>
|
||
</body>
|
||
</html>
|