432 lines
31 KiB
HTML
432 lines
31 KiB
HTML
{{define "title"}}{{if .IsNew}}Nouveau panel{{else}}Éditer — {{.Panel.Name}}{{end}}{{end}}
|
||
{{define "header"}}{{if .IsNew}}Créer un panel{{else}}Éditer {{.Panel.Name}}{{end}}{{end}}
|
||
{{define "content"}}
|
||
<form id="panelForm" method="POST" action="{{if .IsNew}}/panels/new{{else}}/panels/{{.Panel.ID}}/edit{{end}}">
|
||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||
|
||
{{if and .IsNew .Guilds}}
|
||
<div class="bg-discord-card rounded-lg border border-discord-border p-4 mb-4 flex items-center gap-4">
|
||
<label class="text-sm text-gray-300 flex-shrink-0">Serveur Discord :</label>
|
||
<select id="guildSelector" class="px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white text-sm focus:outline-none focus:border-indigo-500"
|
||
onchange="location.href='/panels/new?guild_id='+this.value">
|
||
{{range .Guilds}}
|
||
<option value="{{.ID}}" {{if eq .ID $.Panel.GuildID}}selected{{end}}>{{.Name}}</option>
|
||
{{end}}
|
||
</select>
|
||
<p class="text-xs text-gray-500">Les rôles et channels chargés correspondent au serveur sélectionné.</p>
|
||
</div>
|
||
{{end}}
|
||
|
||
<input type="hidden" name="guild_id" value="{{.Panel.GuildID}}">
|
||
|
||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||
<!-- Left: form -->
|
||
<div class="xl:col-span-2 space-y-6">
|
||
|
||
<!-- Section 1: Infos panel -->
|
||
<div class="bg-discord-card rounded-lg border border-discord-border p-6">
|
||
<h2 class="font-semibold text-white mb-4">Informations du panel</h2>
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<div class="sm:col-span-2">
|
||
<label class="block text-sm text-gray-300 mb-1">Nom interne <span class="text-red-400">*</span></label>
|
||
<input type="text" name="name" value="{{.Panel.Name}}" required {{if not .IsNew}}readonly{{end}}
|
||
class="w-full px-3 py-2 bg-gray-800 border {{if index .Errors "name"}}border-red-600{{else}}border-gray-600{{end}} rounded-lg text-white focus:outline-none focus:border-indigo-500 {{if not .IsNew}}opacity-60 cursor-not-allowed{{end}}">
|
||
{{if index .Errors "name"}}<p class="text-red-400 text-xs mt-1">{{index .Errors "name"}}</p>{{end}}
|
||
</div>
|
||
<div class="sm:col-span-2">
|
||
<label class="block text-sm text-gray-300 mb-1">Titre embed <span class="text-red-400">*</span></label>
|
||
<input type="text" name="embed_title" value="{{.Panel.EmbedTitle}}" required
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="w-full px-3 py-2 bg-gray-800 border {{if index .Errors "embed_title"}}border-red-600{{else}}border-gray-600{{end}} rounded-lg text-white focus:outline-none focus:border-indigo-500">
|
||
{{if index .Errors "embed_title"}}<p class="text-red-400 text-xs mt-1">{{index .Errors "embed_title"}}</p>{{end}}
|
||
</div>
|
||
<div class="sm:col-span-2">
|
||
<label class="block text-sm text-gray-300 mb-1">Description embed</label>
|
||
<textarea name="embed_description" rows="3"
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500 resize-none">{{.Panel.EmbedDescription.String}}</textarea>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-300 mb-1">Couleur embed</label>
|
||
<div class="flex items-center gap-3">
|
||
<input type="color" name="embed_color" value="{{.Panel.EmbedColor}}" id="embedColor"
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:200ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="h-9 w-16 rounded cursor-pointer bg-gray-800 border border-gray-600">
|
||
<span id="embedColorText" class="text-sm text-gray-400">{{.Panel.EmbedColor}}</span>
|
||
</div>
|
||
</div>
|
||
<div class="sm:col-span-2">
|
||
<label class="block text-sm text-gray-300 mb-1">Image URL (grande image en bas de l'embed)</label>
|
||
<input type="url" name="embed_image" value="{{.Panel.EmbedImage}}"
|
||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500"
|
||
placeholder="https://...">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-300 mb-1">Channel Discord</label>
|
||
{{if .TextChans}}
|
||
<select name="channel_id" class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500">
|
||
<option value="">— Sélectionner —</option>
|
||
{{range .TextChans}}
|
||
<option value="{{.ID}}" {{if eq .ID $.Panel.ChannelID.String}}selected{{end}}>#{{.Name}}</option>
|
||
{{end}}
|
||
</select>
|
||
{{else}}
|
||
<input type="text" name="channel_id" value="{{.Panel.ChannelID.String}}"
|
||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500"
|
||
placeholder="ID du channel cible">
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 2: Types de tickets -->
|
||
<div class="bg-discord-card rounded-lg border border-discord-border p-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="font-semibold text-white">Types de tickets</h2>
|
||
<button type="button" onclick="addType()"
|
||
class="px-3 py-1.5 text-sm bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg transition-colors">
|
||
+ Ajouter un type
|
||
</button>
|
||
</div>
|
||
<div id="typesList" class="space-y-4">
|
||
{{range $i, $t := .Panel.Types}}
|
||
{{template "type_block" dict "Type" $t "Index" $i "CSRFToken" $.CSRFToken "Roles" $.Roles "TextChans" $.TextChans "Categories" $.Categories}}
|
||
{{end}}
|
||
</div>
|
||
{{if eq (len .Panel.Types) 0}}
|
||
<p id="noTypesMsg" class="text-gray-500 text-sm text-center py-4">Aucun type. Cliquez sur "+ Ajouter un type".</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Right: preview + actions -->
|
||
<div class="space-y-6">
|
||
<div class="bg-discord-card rounded-lg border border-discord-border p-5">
|
||
<h3 class="font-semibold text-white mb-3">Aperçu</h3>
|
||
<div id="preview">
|
||
<div class="rounded-lg overflow-hidden border-l-4 bg-[#2b2d31] p-4" style="border-left-color:{{.Panel.EmbedColor}}">
|
||
<p class="font-semibold text-white mb-1">{{if .Panel.EmbedTitle}}{{.Panel.EmbedTitle}}{{else}}Titre du panel{{end}}</p>
|
||
{{if .Panel.EmbedDescription.Valid}}
|
||
<p class="text-gray-300 text-sm">{{.Panel.EmbedDescription.String}}</p>
|
||
{{end}}
|
||
{{if .Panel.Types}}
|
||
<div class="flex flex-wrap gap-2 mt-3">
|
||
{{range .Panel.Types}}
|
||
<span class="{{buttonClass .ButtonColor}} text-white text-sm px-3 py-1.5 rounded">{{.ButtonEmoji}} {{.ButtonLabel}}</span>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-discord-card rounded-lg border border-discord-border p-5 space-y-3">
|
||
<button type="submit" name="action" value="save"
|
||
class="w-full px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg transition-colors">
|
||
Sauvegarder
|
||
</button>
|
||
<button type="submit" name="action" value="save_send"
|
||
class="w-full px-4 py-2.5 bg-green-700 hover:bg-green-600 text-white font-medium rounded-lg transition-colors">
|
||
Sauvegarder et envoyer sur Discord
|
||
</button>
|
||
<a href="/panels" class="block text-center px-4 py-2.5 text-gray-400 hover:text-white border border-gray-700 hover:border-gray-500 rounded-lg transition-colors text-sm">
|
||
Annuler
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Emoji picker -->
|
||
<div id="emojiPicker" class="hidden fixed z-50 bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-72">
|
||
<div class="flex gap-1 p-2 border-b border-gray-700 overflow-x-auto">
|
||
<button type="button" onclick="showEmojiCat('smileys')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="smileys">😀</button>
|
||
<button type="button" onclick="showEmojiCat('gestures')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="gestures">👍</button>
|
||
<button type="button" onclick="showEmojiCat('activities')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="activities">🎮</button>
|
||
<button type="button" onclick="showEmojiCat('objects')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="objects">📝</button>
|
||
<button type="button" onclick="showEmojiCat('symbols')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="symbols">✅</button>
|
||
</div>
|
||
<div id="emojiGrid" class="p-2 grid grid-cols-8 gap-0.5 max-h-48 overflow-y-auto"></div>
|
||
</div>
|
||
|
||
<!-- Type block template for JS -->
|
||
<template id="typeTpl">
|
||
<div class="type-block border border-gray-700 rounded-lg p-4 space-y-3 cursor-move" data-index="__IDX__">
|
||
<div class="flex items-center justify-between mb-1">
|
||
<span class="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"/></svg>
|
||
Type <span class="type-num"></span>
|
||
</span>
|
||
<button type="button" onclick="removeType(this)" class="text-red-400 hover:text-red-300 text-sm">Supprimer</button>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div><label class="block text-xs text-gray-400 mb-1">Nom interne</label>
|
||
<input type="text" name="types[__IDX__][name]" required class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500" placeholder="ex: support"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Label bouton</label>
|
||
<input type="text" name="types[__IDX__][button_label]"
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Couleur bouton</label>
|
||
<select name="types[__IDX__][button_color]"
|
||
hx-post="/panels/preview" hx-trigger="change" hx-include="#panelForm" hx-target="#preview"
|
||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="primary">🔵 Bleu (primary)</option>
|
||
<option value="secondary">⚪ Gris (secondary)</option>
|
||
<option value="success">🟢 Vert (success)</option>
|
||
<option value="danger">🔴 Rouge (danger)</option>
|
||
</select></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Emoji bouton</label>
|
||
<div class="flex gap-1">
|
||
<input type="text" name="types[__IDX__][button_emoji]" readonly
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:200ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="flex-1 px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none cursor-pointer" placeholder="—" onclick="openEmojiPicker(this)">
|
||
<button type="button" onclick="openEmojiPicker(this.previousElementSibling)" class="px-2 py-1.5 bg-gray-700 hover:bg-gray-600 border border-gray-600 rounded text-sm">😀</button>
|
||
</div></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Titre embed ticket</label>
|
||
<input type="text" name="types[__IDX__][embed_title]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Couleur embed</label>
|
||
<input type="color" name="types[__IDX__][embed_color]" value="#5865f2" class="h-9 w-full rounded cursor-pointer bg-gray-800 border border-gray-600"></div>
|
||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Texte embed bienvenue</label>
|
||
<textarea name="types[__IDX__][embed_text]" rows="2" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500 resize-none"></textarea></div>
|
||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Image URL (optionnel)</label>
|
||
<input type="url" name="types[__IDX__][image_url]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500" placeholder="https://..."></div>
|
||
__ROLES_HTML__
|
||
__CATEGORIES_HTML__
|
||
__TEXTCHANS_HTML__
|
||
<div><label class="block text-xs text-gray-400 mb-1">Règle de fermeture</label>
|
||
<select name="types[__IDX__][close_rule]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="staff_only">Staff uniquement</option>
|
||
<option value="author_if_unclaimed">Auteur si non claimé</option>
|
||
</select></div>
|
||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Reup claim (minutes, 0 = global)</label>
|
||
<input type="number" name="types[__IDX__][claim_reup_minutes]" value="0" min="0" max="1440"
|
||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div class="flex items-center gap-2">
|
||
<input type="hidden" name="types[__IDX__][claim_mode]" value="0">
|
||
<input type="checkbox" name="types[__IDX__][claim_mode]" value="1" class="rounded" id="cm__IDX__">
|
||
<label for="cm__IDX__" class="text-xs text-gray-400">Système de claim activé</label></div>
|
||
<div class="flex items-center gap-2">
|
||
<input type="hidden" name="types[__IDX__][modal_enabled]" value="1">
|
||
<input type="checkbox" name="types[__IDX__][modal_enabled]" value="1" checked class="rounded" id="me__IDX__">
|
||
<label for="me__IDX__" class="text-xs text-gray-400">Modal activé</label></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||
<script>
|
||
let typeCount = {{len .Panel.Types}};
|
||
|
||
// Emoji data
|
||
const emojiData = {
|
||
smileys: ['😀','😃','😄','😁','😆','😅','😂','🤣','😊','😇','🙂','😉','😌','😍','🥰','😘','😗','😙','😚','😋','😛','😜','🤪','😝','🤑','🤗','🤭','🤫','🤔','🤐','🤨','😐','😑','😶','😏','😒','🙄','😬','🤥','😌','😔','😪','🤤','😴','😷','🤒','🤕','🤢','🤧','🥵','🥶','🥴','😵','🤯','🤠','🥳','😎','🤓','🧐','😕','😟','🙁','☹️','😮','😯','😲','😳','🥺','😦','😧','😨','😰','😥','😢','😭','😱','😖','😣','😞','😓','😩','😫','🥱','😤','😡','😠','🤬','😈','👿','💀','☠️','💩','🤡','👹','👺','👻','👽','👾','🤖'],
|
||
gestures: ['👋','🤚','🖐','✋','🖖','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','🖕','👇','☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✍️','💅','🤳','💪','🦾','🦿','🦵','🦶','👂','🦻','👃','🫀','🫁','🧠','🦷','🦴','👀','👁','👅','👄'],
|
||
activities: ['⚽','🏀','🏈','⚾','🥎','🎾','🏐','🏉','🥏','🎱','🪀','🏓','🏸','🏒','🥍','🏏','🪃','🥅','⛳','🪁','🏹','🎣','🤿','🥊','🥋','🎽','🛹','🛷','⛸','🥌','🎿','⛷','🏂','🏋','🤼','🤸','⛹','🤺','🤾','🏌','🏇','🧘','🏄','🏊','🤽','🚣','🧗','🚵','🚴','🎖','🏆','🥇','🥈','🥉','🏅','🎪','🤹','🎭','🎨','🎬','🎤','🎧','🎼','🎵','🎶','🎷','🪗','🎸','🎹','🎺','🎻','🥁','🪘','🎙','🎚','🎛','📻','🎮','🕹','🎲','🎯','🎳','🎰','🎴','🀄','🎭','🎠','🎡','🎢','🎪'],
|
||
objects: ['📱','💻','🖥','🖨','⌨️','🖱','🖲','💽','💾','💿','📀','📷','📸','📹','🎥','📽','🎞','📞','☎️','📟','📠','📺','📻','🧭','⏱','⏲','⏰','🕰','⌚','⏳','📡','🔋','🔌','💡','🔦','🕯','🗑','🛢','💰','💴','💵','💶','💷','💸','💳','🪙','💹','📈','📉','📊','📋','📌','📍','✂️','🗃','🗂','🗄','🗑','🔧','🔨','⚒','🛠','⛏','🔩','🪛','🔑','🗝','🔐','🔏','🔒','🔓','🚪','🪑','🛋','🚽','🪠','🚿','🛁','🛒','🚬','⚰️','🪦','⚱️','🧿','💎','🔮','🧸','🪆','🖼','🪅','🎊','🎉','🎁','🎀','🎗','🎟','🎫','🎖','📝','📓','📔','📒','📕','📗','📘','📙','📚','📖','🔖','🏷','💉','🩸','💊','🩹','🩺','🔬','🔭','🩻','🧬','🦠','🧫','🧪','🌡','🧹','🧺','🧻','🪣','🧼','🫧','🪥','🧽','🧯','🛒'],
|
||
symbols: ['✅','❌','❎','🔰','⛔','🚫','📵','🔕','🚷','🚯','🚳','🚱','🔞','📵','🔇','🔈','🔉','🔊','📢','📣','🔔','🔕','🔋','🪫','🔌','💡','🔦','🕯','🪔','🧯','💰','💱','💲','♾','🔁','🔂','▶️','⏩','⏭','⏯','◀️','⏪','⏮','🔀','🔃','🎵','🎶','➕','➖','✖️','➗','🟰','♾','💲','💱','🏧','🔱','⚜️','🔰','♻️','✅','🆗','✔️','❎','🆘','❓','❔','❕','❗','⁉️','‼️','🔅','🔆','🔱','⚜️','💠','🌀','⭕','✖️','❌','✖','➰','➿','〽️','✳️','✴️','❇️','💯','☑️','🔘','🔲','🔳','▪️','▫️','◾','◽','◼️','◻️','🟥','🟧','🟨','🟩','🟦','🟪','⬛','⬜','🟫','🔶','🔷','🔸','🔹','🔺','🔻','💠','🔘','🔳','🔲']
|
||
};
|
||
|
||
let currentEmojiInput = null;
|
||
let currentCat = 'smileys';
|
||
|
||
function openEmojiPicker(input) {
|
||
currentEmojiInput = input;
|
||
const picker = document.getElementById('emojiPicker');
|
||
const rect = input.getBoundingClientRect();
|
||
picker.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
||
picker.style.left = rect.left + 'px';
|
||
picker.classList.remove('hidden');
|
||
showEmojiCat(currentCat);
|
||
}
|
||
|
||
function showEmojiCat(cat) {
|
||
currentCat = cat;
|
||
const grid = document.getElementById('emojiGrid');
|
||
grid.innerHTML = emojiData[cat].map(e =>
|
||
`<button type="button" onclick="pickEmoji('${e}')" class="text-xl p-1 hover:bg-gray-700 rounded transition-colors" title="${e}">${e}</button>`
|
||
).join('');
|
||
}
|
||
|
||
function pickEmoji(e) {
|
||
if (currentEmojiInput) {
|
||
currentEmojiInput.value = e;
|
||
currentEmojiInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||
}
|
||
document.getElementById('emojiPicker').classList.add('hidden');
|
||
}
|
||
|
||
document.addEventListener('click', function(ev) {
|
||
const picker = document.getElementById('emojiPicker');
|
||
if (!picker.classList.contains('hidden') && !picker.contains(ev.target) &&
|
||
!ev.target.closest('[onclick*="openEmojiPicker"]') && !ev.target.closest('[onclick*="EmojiPicker"]')) {
|
||
picker.classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
// Sync color picker
|
||
document.getElementById('embedColor').addEventListener('input', function() {
|
||
document.getElementById('embedColorText').textContent = this.value;
|
||
});
|
||
|
||
// Roles HTML for JS-added type blocks
|
||
const rolesHtml = `{{range .Roles}}<option value="{{.ID}}">{{.Name}}</option>{{end}}`;
|
||
const categoriesHtml = `{{range .Categories}}<option value="{{.ID}}">📁 {{.Name}}</option>{{end}}`;
|
||
const textChansHtml = `{{range .TextChans}}<option value="{{.ID}}"># {{.Name}}</option>{{end}}`;
|
||
|
||
function makeSelect(name, label, options, value = '') {
|
||
if (!options) return `<div><label class="block text-xs text-gray-400 mb-1">${label}</label><input type="text" name="${name}" value="${value}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>`;
|
||
return `<div><label class="block text-xs text-gray-400 mb-1">${label}</label><select name="${name}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"><option value="">— Sélectionner —</option>${options}</select></div>`;
|
||
}
|
||
|
||
// SortableJS
|
||
new Sortable(document.getElementById('typesList'), {
|
||
animation: 150, handle: '.type-block',
|
||
onEnd: function() {
|
||
renumberTypes();
|
||
{{if not .IsNew}}
|
||
const ids = [...document.querySelectorAll('.type-block')].map(el => el.dataset.typeId).filter(Boolean);
|
||
if (ids.length) {
|
||
fetch('/panels/{{.Panel.ID}}/types/reorder', {
|
||
method: 'POST',
|
||
headers: {'X-CSRF-Token': '{{.CSRFToken}}', 'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: ids.map(id => 'ids[]=' + id).join('&')
|
||
});
|
||
}
|
||
{{end}}
|
||
}
|
||
});
|
||
|
||
function addType() {
|
||
document.getElementById('noTypesMsg')?.remove();
|
||
let html = document.getElementById('typeTpl').innerHTML.replaceAll('__IDX__', typeCount);
|
||
html = html.replace('__ROLES_HTML__', makeSelect(`types[${typeCount}][staff_role_id]`, 'Staff Role', rolesHtml));
|
||
html = html.replace('__CATEGORIES_HTML__', makeSelect(`types[${typeCount}][category_id]`, 'Catégorie', categoriesHtml));
|
||
html = html.replace('__TEXTCHANS_HTML__',
|
||
makeSelect(`types[${typeCount}][claim_channel_id]`, 'Claim Channel', textChansHtml) +
|
||
makeSelect(`types[${typeCount}][log_channel_id]`, 'Log Channel', textChansHtml));
|
||
const div = document.createElement('div');
|
||
div.innerHTML = html;
|
||
document.getElementById('typesList').appendChild(div.firstElementChild);
|
||
typeCount++;
|
||
renumberTypes();
|
||
htmx.process(document.getElementById('typesList'));
|
||
}
|
||
|
||
function removeType(btn) {
|
||
btn.closest('.type-block').remove();
|
||
renumberTypes();
|
||
}
|
||
|
||
function renumberTypes() {
|
||
document.querySelectorAll('.type-num').forEach((el, i) => el.textContent = i + 1);
|
||
}
|
||
</script>
|
||
{{end}}
|
||
|
||
{{define "type_block"}}
|
||
<div class="type-block border border-gray-700 rounded-lg p-4 space-y-3 cursor-move" data-index="{{.Index}}" data-type-id="{{.Type.ID}}">
|
||
<div class="flex items-center justify-between mb-1">
|
||
<span class="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"/></svg>
|
||
Type <span class="type-num">{{add .Index 1}}</span>
|
||
</span>
|
||
<button type="button" onclick="removeType(this)" class="text-red-400 hover:text-red-300 text-sm">Supprimer</button>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div><label class="block text-xs text-gray-400 mb-1">Nom interne</label>
|
||
<input type="text" name="types[{{.Index}}][name]" value="{{.Type.Name}}" required class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Label bouton</label>
|
||
<input type="text" name="types[{{.Index}}][button_label]" value="{{.Type.ButtonLabel}}"
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Couleur bouton</label>
|
||
<select name="types[{{.Index}}][button_color]"
|
||
hx-post="/panels/preview" hx-trigger="change" hx-include="#panelForm" hx-target="#preview"
|
||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="primary" {{if eq .Type.ButtonColor "primary"}}selected{{end}}>🔵 Bleu (primary)</option>
|
||
<option value="secondary" {{if eq .Type.ButtonColor "secondary"}}selected{{end}}>⚪ Gris (secondary)</option>
|
||
<option value="success" {{if eq .Type.ButtonColor "success"}}selected{{end}}>🟢 Vert (success)</option>
|
||
<option value="danger" {{if eq .Type.ButtonColor "danger"}}selected{{end}}>🔴 Rouge (danger)</option>
|
||
</select></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Emoji bouton</label>
|
||
<div class="flex gap-1">
|
||
<input type="text" name="types[{{.Index}}][button_emoji]" value="{{.Type.ButtonEmoji}}" readonly
|
||
hx-post="/panels/preview" hx-trigger="input changed delay:200ms" hx-include="#panelForm" hx-target="#preview"
|
||
class="flex-1 px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm cursor-pointer" placeholder="—" onclick="openEmojiPicker(this)">
|
||
<button type="button" onclick="openEmojiPicker(this.previousElementSibling)" class="px-2 py-1.5 bg-gray-700 hover:bg-gray-600 border border-gray-600 rounded text-sm">😀</button>
|
||
</div></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Titre embed ticket</label>
|
||
<input type="text" name="types[{{.Index}}][embed_title]" value="{{.Type.EmbedTitle}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Couleur embed</label>
|
||
<input type="color" name="types[{{.Index}}][embed_color]" value="{{.Type.EmbedColor}}" class="h-9 w-full rounded cursor-pointer bg-gray-800 border border-gray-600"></div>
|
||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Texte embed bienvenue</label>
|
||
<textarea name="types[{{.Index}}][embed_text]" rows="2" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500 resize-none">{{.Type.EmbedText}}</textarea></div>
|
||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Image URL (optionnel)</label>
|
||
<input type="url" name="types[{{.Index}}][image_url]" value="{{.Type.ImageURL}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500" placeholder="https://..."></div>
|
||
<div>
|
||
<label class="block text-xs text-gray-400 mb-1">Staff Role</label>
|
||
{{if .Roles}}
|
||
<select name="types[{{.Index}}][staff_role_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="">— Sélectionner —</option>
|
||
{{range .Roles}}<option value="{{.ID}}" {{if eq .ID $.Type.StaffRoleID}}selected{{end}}>{{.Name}}</option>{{end}}
|
||
</select>
|
||
{{else}}
|
||
<input type="text" name="types[{{.Index}}][staff_role_id]" value="{{.Type.StaffRoleID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
{{end}}
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-gray-400 mb-1">Catégorie</label>
|
||
{{if .Categories}}
|
||
<select name="types[{{.Index}}][category_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="">— Sélectionner —</option>
|
||
{{range .Categories}}<option value="{{.ID}}" {{if eq .ID $.Type.CategoryID}}selected{{end}}>📁 {{.Name}}</option>{{end}}
|
||
</select>
|
||
{{else}}
|
||
<input type="text" name="types[{{.Index}}][category_id]" value="{{.Type.CategoryID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
{{end}}
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-gray-400 mb-1">Claim Channel</label>
|
||
{{if .TextChans}}
|
||
<select name="types[{{.Index}}][claim_channel_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="">— Sélectionner —</option>
|
||
{{range .TextChans}}<option value="{{.ID}}" {{if eq .ID $.Type.ClaimChannelID}}selected{{end}}># {{.Name}}</option>{{end}}
|
||
</select>
|
||
{{else}}
|
||
<input type="text" name="types[{{.Index}}][claim_channel_id]" value="{{.Type.ClaimChannelID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
{{end}}
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-gray-400 mb-1">Log Channel</label>
|
||
{{if .TextChans}}
|
||
<select name="types[{{.Index}}][log_channel_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="">— Sélectionner —</option>
|
||
{{range .TextChans}}<option value="{{.ID}}" {{if eq .ID $.Type.LogChannelID}}selected{{end}}># {{.Name}}</option>{{end}}
|
||
</select>
|
||
{{else}}
|
||
<input type="text" name="types[{{.Index}}][log_channel_id]" value="{{.Type.LogChannelID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
{{end}}
|
||
</div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Max tickets / user</label>
|
||
<input type="number" name="types[{{.Index}}][max_per_user]" value="{{.Type.MaxPerUser}}" min="1" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div><label class="block text-xs text-gray-400 mb-1">Règle de close</label>
|
||
<select name="types[{{.Index}}][close_rule]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||
<option value="staff_only" {{if eq .Type.CloseRule "staff_only"}}selected{{end}}>Staff uniquement</option>
|
||
<option value="author_if_unclaimed" {{if eq .Type.CloseRule "author_if_unclaimed"}}selected{{end}}>Auteur si non claimé</option>
|
||
</select></div>
|
||
<div class="flex items-center gap-2 pt-1">
|
||
<input type="hidden" name="types[{{.Index}}][claim_mode]" value="0">
|
||
<input type="checkbox" name="types[{{.Index}}][claim_mode]" value="1" {{if .Type.ClaimMode}}checked{{end}} class="rounded" id="cm{{.Index}}">
|
||
<label for="cm{{.Index}}" class="text-xs text-gray-400">Système de claim activé</label></div>
|
||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Reup claim (minutes, 0 = global)</label>
|
||
<input type="number" name="types[{{.Index}}][claim_reup_minutes]" value="{{.Type.ClaimReupMinutes}}" min="0" max="1440"
|
||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||
<div class="flex items-center gap-2 pt-1">
|
||
<input type="hidden" name="types[{{.Index}}][modal_enabled]" value="0">
|
||
<input type="checkbox" name="types[{{.Index}}][modal_enabled]" value="1" {{if .Type.ModalEnabled}}checked{{end}} class="rounded" id="me{{.Index}}">
|
||
<label for="me{{.Index}}" class="text-xs text-gray-400">Modal activé</label></div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|