ReleaseV07+Avatars

This commit is contained in:
Flatlogic Bot 2026-02-20 03:12:27 +00:00
parent 4388017d2d
commit 923147d500
7 changed files with 317 additions and 35 deletions

View File

@ -6,7 +6,8 @@ $action = $_GET['action'] ?? 'search';
if ($action === 'search') {
$q = $_GET['query'] ?? 'avatar';
$url = 'https://api.pexels.com/v1/search?query=' . urlencode($q) . '&per_page=12&page=1';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$url = 'https://api.pexels.com/v1/search?query=' . urlencode($q) . '&per_page=26&page=' . $page;
$data = pexels_get($url);
if (!$data) {
echo json_encode(['error' => 'Failed to fetch images']);

58
api/upload_avatar.php Normal file
View File

@ -0,0 +1,58 @@
<?php
require_once __DIR__ . '/../auth/session.php';
header('Content-Type: application/json');
$user = getCurrentUser();
if (!$user) {
echo json_encode(['success' => false, 'error' => 'Non autorisé']);
exit;
}
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu ou erreur de téléchargement']);
exit;
}
$file = $_FILES['avatar'];
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$maxSize = 2 * 1024 * 1024; // 2MB
if (!in_array($file['type'], $allowedTypes)) {
echo json_encode(['success' => false, 'error' => 'Format de fichier non supporté (JPG, PNG, WebP, GIF uniquement)']);
exit;
}
if ($file['size'] > $maxSize) {
echo json_encode(['success' => false, 'error' => 'Le fichier est trop volumineux (max 2Mo)']);
exit;
}
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
if (empty($extension)) {
$extensions = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif'
];
$extension = $extensions[$file['type']] ?? 'jpg';
}
$filename = 'avatar_' . $user['id'] . '_' . time() . '.' . $extension;
$targetPath = __DIR__ . '/../assets/images/avatars/' . $filename;
$relativeUrl = 'assets/images/avatars/' . $filename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
// Optionally delete old local avatar if it exists
if (!empty($user['avatar_url']) && strpos($user['avatar_url'], 'assets/images/avatars/') === 0) {
$oldFile = __DIR__ . '/../' . $user['avatar_url'];
if (file_exists($oldFile)) {
unlink($oldFile);
}
}
echo json_encode(['success' => true, 'url' => $relativeUrl]);
} else {
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'enregistrement du fichier']);
}

View File

@ -0,0 +1,79 @@
<?php
require_once __DIR__ . '/../auth/session.php';
require_once __DIR__ . '/../includes/permissions.php';
header('Content-Type: application/json');
$user = getCurrentUser();
if (!$user) {
echo json_encode(['success' => false, 'error' => 'Non autorisé']);
exit;
}
$server_id = $_POST['server_id'] ?? 0;
if (!$server_id) {
echo json_encode(['success' => false, 'error' => 'ID du serveur manquant']);
exit;
}
if (!Permissions::hasPermission($user['id'], $server_id, Permissions::MANAGE_SERVER)) {
echo json_encode(['success' => false, 'error' => 'Vous n\'avez pas la permission de gérer ce serveur']);
exit;
}
if (!isset($_FILES['icon']) || $_FILES['icon']['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu ou erreur de téléchargement']);
exit;
}
$file = $_FILES['icon'];
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$maxSize = 2 * 1024 * 1024; // 2MB
if (!in_array($file['type'], $allowedTypes)) {
echo json_encode(['success' => false, 'error' => 'Format de fichier non supporté (JPG, PNG, WebP, GIF uniquement)']);
exit;
}
if ($file['size'] > $maxSize) {
echo json_encode(['success' => false, 'error' => 'Le fichier est trop volumineux (max 2Mo)']);
exit;
}
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
if (empty($extension)) {
$extensions = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif'
];
$extension = $extensions[$file['type']] ?? 'jpg';
}
$filename = 'server_' . $server_id . '_' . time() . '.' . $extension;
$dir = __DIR__ . '/../assets/images/servers/';
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
$targetPath = $dir . $filename;
$relativeUrl = 'assets/images/servers/' . $filename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
// Optionally fetch old icon to delete it if it's local
$stmt = db()->prepare("SELECT icon_url FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
if ($server && !empty($server['icon_url']) && strpos($server['icon_url'], 'assets/images/servers/') === 0) {
$oldFile = __DIR__ . '/../' . $server['icon_url'];
if (file_exists($oldFile)) {
unlink($oldFile);
}
}
echo json_encode(['success' => true, 'url' => $relativeUrl]);
} else {
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'enregistrement du fichier']);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -2544,33 +2544,125 @@ document.addEventListener('DOMContentLoaded', () => {
// User Settings - Save logic removed and moved to index.php for reliability
const avatarSearchBtn = document.getElementById('search-avatar-btn');
const avatarRefreshBtn = document.getElementById('refresh-avatar-btn');
const avatarSearchQuery = document.getElementById('avatar-search-query');
const avatarResults = document.getElementById('avatar-results');
const avatarPreview = document.getElementById('settings-avatar-preview');
const avatarUrlInput = document.getElementById('settings-avatar-url');
const avatarUploadInput = document.getElementById('avatar-upload-input');
avatarSearchBtn?.addEventListener('click', async () => {
const q = avatarSearchQuery.value.trim();
avatarUploadInput?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('avatar', file);
try {
avatarPreview.innerHTML = '<div class="spinner-border spinner-border-sm text-light" role="status"></div>';
const resp = await fetch('api/upload_avatar.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
avatarPreview.innerHTML = '';
if (data.success) {
avatarUrlInput.value = data.url;
avatarPreview.style.backgroundImage = `url('${data.url}')`;
} else {
alert(data.error || 'Erreur lors de l\'upload');
}
} catch (err) {
console.error(err);
avatarPreview.innerHTML = '';
alert('Erreur réseau lors de l\'upload');
}
});
const serverIconUploadInput = document.getElementById('server-icon-upload-input');
// serverIconPreview and serverIconUrlInput are already declared above
serverIconUploadInput?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('icon', file);
formData.append('server_id', window.activeServerId);
try {
serverIconPreview.innerHTML = '<div class="spinner-border spinner-border-sm text-light" role="status"></div>';
const resp = await fetch('api/upload_server_icon.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
serverIconPreview.innerHTML = '';
if (data.success) {
serverIconUrlInput.value = data.url;
serverIconPreview.style.backgroundImage = `url('${data.url}')`;
} else {
alert(data.error || 'Erreur lors de l\'upload');
}
} catch (err) {
console.error(err);
serverIconPreview.innerHTML = '';
alert('Erreur réseau lors de l\'upload');
}
});
let currentAvatarPage = 1;
async function fetchAvatars(q, page = 1) {
if (!q) return;
avatarResults.innerHTML = '<div class="text-muted small">Searching...</div>';
try {
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}`);
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}&page=${page}`);
const data = await resp.json();
avatarResults.innerHTML = '';
data.forEach(photo => {
const img = document.createElement('img');
img.src = photo.url;
img.className = 'avatar-pick';
img.style.width = '60px';
img.style.height = '60px';
img.style.cursor = 'pointer';
img.onclick = () => {
avatarUrlInput.value = photo.url;
avatarPreview.style.backgroundImage = `url('${photo.url}')`;
};
avatarResults.appendChild(img);
});
} catch (e) { console.error(e); }
if (data && Array.isArray(data)) {
data.forEach(photo => {
const img = document.createElement('img');
img.src = photo.url;
img.className = 'avatar-pick';
img.style.width = '100%';
img.style.height = 'auto';
img.style.aspectRatio = '1/1';
img.style.objectFit = 'cover';
img.style.borderRadius = '4px';
img.style.cursor = 'pointer';
img.onclick = () => {
avatarUrlInput.value = photo.url;
avatarPreview.style.backgroundImage = `url('${photo.url}')`;
};
avatarResults.appendChild(img);
});
} else {
avatarResults.innerHTML = '<div class="text-muted small">Aucun résultat trouvé.</div>';
}
} catch (e) {
console.error(e);
avatarResults.innerHTML = '<div class="text-danger small">Erreur lors de la récupération.</div>';
}
}
avatarSearchBtn?.addEventListener('click', () => {
currentAvatarPage = 1;
fetchAvatars(avatarSearchQuery.value.trim(), currentAvatarPage);
});
avatarRefreshBtn?.addEventListener('click', () => {
currentAvatarPage++;
fetchAvatars(avatarSearchQuery.value.trim() || 'avatar', currentAvatarPage);
});
avatarSearchQuery?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
currentAvatarPage = 1;
fetchAvatars(avatarSearchQuery.value.trim(), currentAvatarPage);
}
});
// Theme preview

View File

@ -264,6 +264,18 @@ if ($is_dm_view) {
}
}
// Always fetch tags if it's a forum channel
if ($channel_type === 'forum') {
$stmt_tags = db()->prepare("SELECT * FROM forum_tags WHERE channel_id = ? ORDER BY name ASC");
$stmt_tags->execute([$active_channel_id]);
$forum_tags = $stmt_tags->fetchAll();
$selected_tag_ids = [];
if (!empty($_GET['tags'])) {
$selected_tag_ids = array_filter(explode(',', $_GET['tags']), 'is_numeric');
}
}
if ($active_thread) {
// Thread messages already fetched above
} elseif ($channel_type === 'rules') {
@ -275,12 +287,13 @@ if ($is_dm_view) {
$stmt->execute([$active_channel_id]);
$autoroles = $stmt->fetchAll();
} elseif ($channel_type === 'forum') {
$filter_status = $_GET['status'] ?? 'all';
$status_where = "";
if ($filter_status === 'resolved') {
$status_where = " AND t.solution_message_id IS NOT NULL";
} elseif ($filter_status === 'unresolved') {
$status_where = " AND t.solution_message_id IS NULL";
$tag_where = "";
$query_params = [$active_server_id, $active_server_id, $active_channel_id];
if (!empty($selected_tag_ids)) {
$placeholders = implode(',', array_fill(0, count($selected_tag_ids), '?'));
$tag_where = " AND EXISTS (SELECT 1 FROM thread_tags tt WHERE tt.thread_id = t.id AND tt.tag_id IN ($placeholders))";
foreach ($selected_tag_ids as $tid) $query_params[] = $tid;
}
$stmt = db()->prepare("
@ -291,10 +304,10 @@ if ($is_dm_view) {
(SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags
FROM forum_threads t
JOIN users u ON t.user_id = u.id
WHERE t.channel_id = ? " . $status_where . "
WHERE t.channel_id = ? " . $tag_where . "
ORDER BY t.is_pinned DESC, t.last_activity_at DESC, t.created_at DESC
");
$stmt->execute([$active_server_id, $active_server_id, $active_channel_id]);
$stmt->execute($query_params);
$threads = $stmt->fetchAll();
} else {
// Fetch messages for normal chat channels
@ -924,20 +937,31 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
<?php endif; ?>
</div>
<?php elseif($channel_type === 'forum'): ?>
<?php elseif($channel_type === 'forum' && !$active_thread): ?>
<div class="forum-container p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-2">🏛️ <?php echo htmlspecialchars($current_channel_name); ?></h2>
<div class="btn-group btn-group-sm forum-filters">
<div class="btn-group btn-group-sm forum-filters flex-wrap gap-1">
<?php
$s_id = $active_server_id;
$c_id = $active_channel_id;
$curr_status = $_GET['status'] ?? 'all';
?>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=all" class="btn btn-outline-secondary <?php echo $curr_status === 'all' ? 'active' : ''; ?>">Tous</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=unresolved" class="btn btn-outline-secondary <?php echo $curr_status === 'unresolved' ? 'active' : ''; ?>">Non résolus</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=resolved" class="btn btn-outline-secondary <?php echo $curr_status === 'resolved' ? 'active' : ''; ?>">Résolus</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>" class="btn btn-outline-secondary <?php echo empty($selected_tag_ids) ? 'active' : ''; ?>">Tous</a>
<?php foreach($forum_tags as $tag):
$is_active = in_array($tag['id'], $selected_tag_ids);
if ($is_active) {
$new_tags = array_diff($selected_tag_ids, [$tag['id']]);
} else {
$new_tags = array_merge($selected_tag_ids, [$tag['id']]);
}
$tags_query = !empty($new_tags) ? '&tags=' . implode(',', $new_tags) : '';
$tag_url = "?server_id=$s_id&channel_id=$c_id$tags_query";
?>
<a href="<?php echo $tag_url; ?>" class="btn btn-outline-secondary <?php echo $is_active ? 'active' : ''; ?>" style="<?php echo $is_active ? "background-color: {$tag['color']}; border-color: {$tag['color']}; color: white;" : ""; ?>">
<?php echo htmlspecialchars($tag['name']); ?>
</a>
<?php endforeach; ?>
</div>
</div>
<div class="d-flex gap-2">
@ -954,7 +978,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="text-center text-muted mt-5">Pas encore de discussions. Commencez-en une !</div>
<?php endif; ?>
<?php foreach($threads as $thread): ?>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?>&thread_id=<?php echo $thread['id']; ?><?php echo isset($_GET['status']) ? '&status='.htmlspecialchars($_GET['status']) : ''; ?>" class="thread-item d-flex align-items-center p-3 mb-2 rounded bg-dark text-decoration-none text-white border-start border-4 <?php echo $thread['is_pinned'] ? 'border-primary' : 'border-secondary'; ?>">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?>&thread_id=<?php echo $thread['id']; ?><?php echo !empty($selected_tag_ids) ? '&tags='.implode(',', $selected_tag_ids) : ''; ?>" class="thread-item d-flex align-items-center p-3 mb-2 rounded bg-dark text-decoration-none text-white border-start border-4 <?php echo $thread['is_pinned'] ? 'border-primary' : 'border-secondary'; ?>">
<div class="thread-icon me-3">
<?php if($thread['is_pinned']): ?>
<i class="fa-solid fa-thumbtack text-primary"></i>
@ -997,6 +1021,23 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<?php else: ?>
<?php if ($active_thread): ?>
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center bg-dark bg-opacity-25 sticky-top" style="z-index: 10;">
<div>
<h4 class="mb-0">
<?php if($active_thread['is_pinned']): ?><i class="fa-solid fa-thumbtack text-primary me-2 small"></i><?php endif; ?>
<?php if($active_thread['is_locked']): ?><i class="fa-solid fa-lock text-warning me-2 small"></i><?php endif; ?>
<?php echo htmlspecialchars($active_thread['title']); ?>
</h4>
<div class="small text-muted mt-1">
Par <?php echo htmlspecialchars($active_thread['username']); ?> • Dans #<?php echo htmlspecialchars($current_channel_name); ?>
</div>
</div>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?><?php echo !empty($selected_tag_ids) ? '&tags='.implode(',', $selected_tag_ids) : ''; ?>" class="btn btn-outline-secondary btn-sm">
<i class="fa-solid fa-arrow-left me-1"></i> Retour au forum
</a>
</div>
<?php endif; ?>
<?php if(empty($messages)): ?>
<div style="text-align: center; color: var(--text-muted); margin-top: 40px;">
<h4>Bienvenue dans #<?php echo htmlspecialchars($current_channel_name); ?> !</h4>
@ -1239,6 +1280,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="col-md-3 text-center">
<div class="message-avatar mx-auto mb-2" id="settings-avatar-preview" style="width: 80px; height: 80px; <?php echo $user['avatar_url'] ? "background-image: url('{$user['avatar_url']}');" : ""; ?>"></div>
<input type="hidden" name="avatar_url" id="settings-avatar-url" value="<?php echo htmlspecialchars($user['avatar_url'] ?? ''); ?>">
<button type="button" class="btn btn-sm btn-outline-secondary w-100 mt-2" onclick="document.getElementById('avatar-upload-input').click()">
<i class="fas fa-upload me-1"></i> Importer
</button>
<input type="file" id="avatar-upload-input" style="display: none;" accept="image/*">
</div>
<div class="col-md-9">
<div class="mb-3">
@ -1253,8 +1298,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="input-group mb-2 shadow-sm">
<input type="text" id="avatar-search-query" class="form-control bg-dark border-0 text-white" placeholder="ex: chat, abstrait, gamer">
<button class="btn btn-primary px-3" type="button" id="search-avatar-btn">Rechercher</button>
<button class="btn btn-secondary px-3" type="button" id="refresh-avatar-btn" title="Rafraîchir les propositions"><i class="fas fa-sync-alt"></i></button>
</div>
<div id="avatar-results" class="d-flex flex-wrap gap-2 overflow-auto p-2 rounded settings-inner-bg" style="max-height: 180px;">
<div id="avatar-results" class="d-grid gap-2 overflow-auto p-2 rounded settings-inner-bg" style="max-height: 220px; grid-template-columns: repeat(13, 1fr);">
<div class="w-100 text-center text-muted py-3 small">Recherchez des images pour changer votre avatar.</div>
</div>
</div>
@ -1858,7 +1904,13 @@ document.addEventListener('DOMContentLoaded', () => {
?>
<div class="message-avatar mx-auto mb-2" id="server-icon-preview" style="width: 80px; height: 80px; <?php echo $active_icon ? "background-image: url('{$active_icon}');" : ""; ?>"></div>
<input type="hidden" name="icon_url" id="server-icon-url" value="<?php echo htmlspecialchars($active_icon); ?>">
<button type="button" class="btn btn-sm btn-outline-secondary" id="search-server-icon-btn">Changer l'icône</button>
<div class="d-flex gap-2 justify-content-center">
<button type="button" class="btn btn-sm btn-outline-secondary" id="search-server-icon-btn">Rechercher</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="document.getElementById('server-icon-upload-input').click()">
<i class="fas fa-upload me-1"></i> Importer
</button>
<input type="file" id="server-icon-upload-input" style="display: none;" accept="image/*">
</div>
</div>
<div class="mb-3">