2100 lines
92 KiB
PHP
2100 lines
92 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_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_build_return_url(int $character_id = 0, string $item_source = '', string $item_search = '', bool $keep_item_panel = false): 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;
|
||
}
|
||
|
||
if ($keep_item_panel) {
|
||
$params['item_panel'] = '1';
|
||
}
|
||
|
||
return 'sccharacters.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||
}
|
||
|
||
function sccharacters_attach_item(
|
||
PDO $db,
|
||
int $owner_auth_id,
|
||
int $character_id,
|
||
string $source,
|
||
int $source_id,
|
||
string $requested_category,
|
||
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;
|
||
}
|
||
|
||
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_note
|
||
) VALUES (
|
||
:character_id,
|
||
:source,
|
||
:scobjs_id,
|
||
NULL,
|
||
:slot,
|
||
:note
|
||
)'
|
||
);
|
||
$stmt_insert->execute([
|
||
'character_id' => $character_id,
|
||
'source' => 'base',
|
||
'scobjs_id' => $source_id,
|
||
'slot' => $category,
|
||
'note' => $note !== '' ? $note : null,
|
||
]);
|
||
|
||
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_note
|
||
) VALUES (
|
||
:character_id,
|
||
:source,
|
||
NULL,
|
||
:scitemcustom_id,
|
||
:slot,
|
||
:note
|
||
)'
|
||
);
|
||
$stmt_insert->execute([
|
||
'character_id' => $character_id,
|
||
'source' => 'custom',
|
||
'scitemcustom_id' => $source_id,
|
||
'slot' => $category,
|
||
'note' => $note !== '' ? $note : null,
|
||
]);
|
||
|
||
return true;
|
||
}
|
||
|
||
$error_message = 'Source d’objet invalide.';
|
||
return false;
|
||
}
|
||
|
||
$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;
|
||
|
||
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
|
||
) VALUES (
|
||
:owner_auth_id,
|
||
:name,
|
||
:role,
|
||
:faction,
|
||
:avatar_url,
|
||
:description,
|
||
:notes,
|
||
:share_token,
|
||
:share_enabled
|
||
)'
|
||
);
|
||
$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,
|
||
]);
|
||
|
||
$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;
|
||
|
||
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
|
||
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,
|
||
'id' => $character_id,
|
||
'owner_auth_id' => $current_owner_auth_id,
|
||
]);
|
||
|
||
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'] ?? '');
|
||
$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'] ?? '');
|
||
$error_message = null;
|
||
|
||
if (!sccharacters_attach_item(
|
||
$db,
|
||
$current_owner_auth_id,
|
||
$character_id,
|
||
'base',
|
||
$obj_id,
|
||
$requested_category,
|
||
$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));
|
||
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));
|
||
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'] ?? '');
|
||
$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'] ?? '');
|
||
$error_message = null;
|
||
|
||
if (!sccharacters_attach_item(
|
||
$db,
|
||
$current_owner_auth_id,
|
||
$character_id,
|
||
'custom',
|
||
$itemcustom_id,
|
||
$requested_category,
|
||
$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));
|
||
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));
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'add_selected_items') {
|
||
$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'] ?? '');
|
||
$selected_items = $_POST['selected_items'] ?? [];
|
||
$return_url = sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true);
|
||
$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;
|
||
}
|
||
|
||
if (!is_array($selected_items) || $selected_items === []) {
|
||
auth_flash_set('error', 'Sélectionne au moins un objet avant de valider.');
|
||
header('Location: ' . $return_url);
|
||
exit;
|
||
}
|
||
|
||
$selected_items = array_values(array_unique(array_map('strval', $selected_items)));
|
||
$item_slots = isset($_POST['item_slot']) && is_array($_POST['item_slot']) ? $_POST['item_slot'] : [];
|
||
$item_notes = isset($_POST['item_note']) && is_array($_POST['item_note']) ? $_POST['item_note'] : [];
|
||
$added_count = 0;
|
||
$error_count = 0;
|
||
|
||
foreach ($selected_items as $item_key) {
|
||
if (!preg_match('/^(base|custom):(\d+)$/', $item_key, $matches)) {
|
||
$error_count++;
|
||
continue;
|
||
}
|
||
|
||
$source = (string) $matches[1];
|
||
$source_id = (int) $matches[2];
|
||
$requested_category = sccharacters_clean_text($item_slots[$item_key] ?? '');
|
||
$note = sccharacters_clean_text($item_notes[$item_key] ?? '');
|
||
$error_message = null;
|
||
|
||
if (sccharacters_attach_item(
|
||
$db,
|
||
$current_owner_auth_id,
|
||
$character_id,
|
||
$source,
|
||
$source_id,
|
||
$requested_category,
|
||
$note,
|
||
$error_message
|
||
)) {
|
||
$added_count++;
|
||
continue;
|
||
}
|
||
|
||
$error_count++;
|
||
}
|
||
|
||
if ($added_count <= 0) {
|
||
auth_flash_set('error', 'Aucun objet n’a pu être ajouté à la sélection.');
|
||
header('Location: ' . $return_url);
|
||
exit;
|
||
}
|
||
|
||
if ($error_count > 0) {
|
||
auth_flash_set('success', $added_count . ' objet(s) ajouté(s). Certains éléments sélectionnés ont été ignorés.');
|
||
header('Location: ' . $return_url);
|
||
exit;
|
||
}
|
||
|
||
auth_flash_set('success', $added_count . ' objet(s) ajouté(s) au personnage.');
|
||
header('Location: ' . $return_url);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'delete_character_item') {
|
||
$character_item_id = (int) ($_POST['character_item_id'] ?? 0);
|
||
$character_id = (int) ($_POST['character_id'] ?? 0);
|
||
|
||
$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,
|
||
]);
|
||
|
||
auth_flash_set('success', 'Objet retiré du personnage.');
|
||
header('Location: sccharacters.php?character=' . $character_id);
|
||
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_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_results = [];
|
||
|
||
if ($selected_character) {
|
||
if ($item_source === 'custom') {
|
||
$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";
|
||
$params = ['owner_auth_id' => $current_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
|
||
)";
|
||
$params['search'] = '%' . $item_search . '%';
|
||
}
|
||
|
||
$sql .= "
|
||
GROUP BY c.cl_scitemcustom_id
|
||
ORDER BY o.cl_scobjs_name ASC, c.cl_scitemcustom_id ASC
|
||
LIMIT 25";
|
||
|
||
$stmt_item_results = $db->prepare($sql);
|
||
$stmt_item_results->execute($params);
|
||
$item_results = $stmt_item_results->fetchAll();
|
||
} else {
|
||
$sql = "SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid
|
||
FROM tbl_scobjs
|
||
WHERE 1 = 1";
|
||
$params = [];
|
||
|
||
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
|
||
)";
|
||
$params['search'] = '%' . $item_search . '%';
|
||
}
|
||
|
||
$sql .= "
|
||
ORDER BY cl_scobjs_name ASC
|
||
LIMIT 25";
|
||
|
||
$stmt_item_results = $db->prepare($sql);
|
||
$stmt_item_results->execute($params);
|
||
$item_results = $stmt_item_results->fetchAll();
|
||
}
|
||
}
|
||
|
||
$selected_character_items = [];
|
||
$custom_stats_by_itemcustom = [];
|
||
if ($selected_character) {
|
||
$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
|
||
CASE WHEN TRIM(ci.cl_sccharacteritem_slot) = '' THEN 1 ELSE 0 END,
|
||
ci.cl_sccharacteritem_slot ASC,
|
||
COALESCE(oo.cl_scobjs_name, bo.cl_scobjs_name, 'ZZZ') 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_items_by_category = [];
|
||
foreach (array_keys($item_category_options) as $category_key) {
|
||
$selected_character_items_by_category[$category_key] = [];
|
||
}
|
||
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 = array_filter(
|
||
$selected_character_items_by_category,
|
||
static fn(array $items): bool => $items !== []
|
||
);
|
||
|
||
$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,
|
||
];
|
||
?>
|
||
<!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 1.75rem;
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.admin-topbar h1 {
|
||
margin: 0;
|
||
font-size: 1.6rem;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
background: linear-gradient(90deg, #ffffff, var(--primary));
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.admin-topbar p {
|
||
margin: 0.35rem 0 0;
|
||
color: var(--text-soft);
|
||
font-size: 0.92rem;
|
||
line-height: 1.5;
|
||
max-width: 760px;
|
||
}
|
||
|
||
.topbar-meta {
|
||
text-align: right;
|
||
color: var(--text-soft);
|
||
font-size: 0.85rem;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.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-body h3 {
|
||
font-size: 0.98rem;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.equipment-actions {
|
||
margin-top: auto;
|
||
}
|
||
|
||
.equip-title {
|
||
font-size: 1rem;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
text-align: center;
|
||
padding: 0.7rem 0.9rem;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
color: var(--text-soft);
|
||
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.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-form {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.search-batch-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
padding: 0.9rem 1rem;
|
||
}
|
||
|
||
.search-batch-toolbar-bottom {
|
||
margin-top: 0.2rem;
|
||
}
|
||
|
||
.search-result {
|
||
padding: 0.95rem;
|
||
display: grid;
|
||
grid-template-columns: 130px minmax(0, 1fr) 310px;
|
||
gap: 1rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.search-result-selector {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.55rem;
|
||
min-height: 100%;
|
||
color: var(--text-main);
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.search-result-selector input {
|
||
width: 1rem;
|
||
height: 1rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.search-result strong { display: block; margin-bottom: 0.25rem; }
|
||
|
||
.search-result-main {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.55rem;
|
||
}
|
||
|
||
.item-attach-form {
|
||
display: grid;
|
||
gap: 0.55rem;
|
||
}
|
||
|
||
.item-attach-meta {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 180px) minmax(0, 1fr);
|
||
gap: 0.55rem;
|
||
}
|
||
|
||
.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; }
|
||
|
||
@media (max-width: 1280px) {
|
||
.main-grid {
|
||
grid-template-columns: 340px minmax(0, 1fr);
|
||
}
|
||
|
||
.search-batch-toolbar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-result {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.item-attach-meta {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@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-meta { text-align: left; }
|
||
.list-panel-header,
|
||
.hero-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.source-switch {
|
||
width: 100%;
|
||
}
|
||
.source-switch label {
|
||
min-width: 0;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="admin-layout">
|
||
<header class="admin-topbar">
|
||
<div>
|
||
<h1>Personnages</h1>
|
||
<p>Crée une fiche personnage, rattache-lui des objets depuis la <strong>Base d'Objets</strong> et tes <strong>Objets personnalisés</strong>, puis partage sa page publique uniquement via son lien dédié.</p>
|
||
</div>
|
||
<div class="topbar-meta">
|
||
<div>Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></div>
|
||
<div>Rôle : <?php echo htmlspecialchars($role_label, ENT_QUOTES, 'UTF-8'); ?></div>
|
||
</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">
|
||
<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>
|
||
|
||
<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">
|
||
<h3><?php echo htmlspecialchars((string) $character_row['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h3>
|
||
<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>
|
||
<h2 class="character-hero-name"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h2>
|
||
<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">
|
||
<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']; ?>">
|
||
|
||
<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>
|
||
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||
</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-sections">
|
||
<?php foreach ($selected_character_items_by_category as $category_key => $category_items): ?>
|
||
<section class="equipment-section">
|
||
<div class="equipment-section-heading">
|
||
<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-grid">
|
||
<?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_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">
|
||
<?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>
|
||
<h3 class="equip-title"><?php echo htmlspecialchars($item_name, ENT_QUOTES, 'UTF-8'); ?></h3>
|
||
<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">
|
||
<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']; ?>">
|
||
<button type="submit" class="btn btn-danger btn-small" onclick="return confirm('Retirer cet objet du personnage ?');">Retirer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endforeach; ?>
|
||
</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">
|
||
<input type="hidden" name="character" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
|
||
<input type="hidden" name="item_panel" value="1">
|
||
|
||
<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>
|
||
|
||
<div class="search-input-row">
|
||
<input type="text" name="item_search" placeholder="Nom, type, subtype, UUID..." value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<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: ?>
|
||
La recherche est prête. Tu peux aussi laisser le champ vide pour parcourir les 25 premiers objets de la base.
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php else: ?>
|
||
<form method="post" class="search-results-form" id="itemBatchForm">
|
||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input type="hidden" name="action" value="add_selected_items">
|
||
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_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'); ?>">
|
||
|
||
<div class="search-batch-toolbar panel panel-soft">
|
||
<div>
|
||
<strong>Ajout en lot</strong>
|
||
<div class="muted">Coche plusieurs objets, ajuste leurs catégories ou notes si besoin, puis valide tout en une seule fois.</div>
|
||
</div>
|
||
<button type="submit" class="btn btn-secondary btn-small" id="batchAddButton" disabled>
|
||
Ajouter la sélection (<span id="selectedItemCount">0</span>)
|
||
</button>
|
||
</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_key = ($result_is_custom ? 'custom' : 'base') . ':' . $result_id;
|
||
?>
|
||
<article class="search-result">
|
||
<label class="search-result-selector" for="select-<?php echo htmlspecialchars($item_key, ENT_QUOTES, 'UTF-8'); ?>">
|
||
<input
|
||
type="checkbox"
|
||
id="select-<?php echo htmlspecialchars($item_key, ENT_QUOTES, 'UTF-8'); ?>"
|
||
name="selected_items[]"
|
||
value="<?php echo htmlspecialchars($item_key, ENT_QUOTES, 'UTF-8'); ?>"
|
||
class="item-select-checkbox"
|
||
>
|
||
<span>Sélectionner</span>
|
||
</label>
|
||
|
||
<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 (!empty($item_result_row['cl_scobjs_uuid'])): ?>
|
||
<span class="badge badge-muted">UUID : <?php echo htmlspecialchars((string) $item_result_row['cl_scobjs_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>
|
||
|
||
<div class="item-attach-form">
|
||
<div class="item-attach-meta">
|
||
<select name="item_slot[<?php echo htmlspecialchars($item_key, ENT_QUOTES, 'UTF-8'); ?>]" 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>
|
||
<input type="text" name="item_note[<?php echo htmlspecialchars($item_key, ENT_QUOTES, 'UTF-8'); ?>]" placeholder="Note optionnelle">
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<?php endforeach; ?>
|
||
|
||
<div class="search-batch-toolbar search-batch-toolbar-bottom panel panel-soft">
|
||
<div class="muted">La recherche reste en place après validation pour que tu puisses continuer sur le même set d’objets.</div>
|
||
<button type="submit" class="btn btn-secondary btn-small" disabled data-batch-submit>
|
||
Valider l’ajout sélectionné
|
||
</button>
|
||
</div>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div> </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');
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('input[name="item_source"]').forEach((input) => {
|
||
input.addEventListener('change', () => {
|
||
const form = document.getElementById('itemSearchForm');
|
||
if (form) {
|
||
form.submit();
|
||
}
|
||
});
|
||
});
|
||
|
||
const itemSelectionCheckboxes = Array.from(document.querySelectorAll('.item-select-checkbox'));
|
||
const batchAddButton = document.getElementById('batchAddButton');
|
||
const selectedItemCount = document.getElementById('selectedItemCount');
|
||
const batchSubmitButtons = Array.from(document.querySelectorAll('[data-batch-submit]'));
|
||
|
||
function updateBatchSelectionState() {
|
||
const checkedCount = itemSelectionCheckboxes.filter((checkbox) => checkbox.checked).length;
|
||
|
||
if (selectedItemCount) {
|
||
selectedItemCount.textContent = String(checkedCount);
|
||
}
|
||
|
||
if (batchAddButton) {
|
||
batchAddButton.disabled = checkedCount === 0;
|
||
}
|
||
|
||
batchSubmitButtons.forEach((button) => {
|
||
button.disabled = checkedCount === 0;
|
||
});
|
||
}
|
||
|
||
itemSelectionCheckboxes.forEach((checkbox) => {
|
||
checkbox.addEventListener('change', updateBatchSelectionState);
|
||
});
|
||
|
||
updateBatchSelectionState();
|
||
</script>
|
||
</body>
|
||
</html>
|