Vision éditable — contenu chargé depuis NocoDB
- Nouvelle table NocoDB Vision (9 sections : trois-mots, phrase, synopsis, tags, listes, inspirations) - Routes API /api/vision GET + PATCH dans server.js - Frontend : contenteditable sur les champs texte, sauvegarde auto au blur - Tags, listes et inspirations : ajout/suppression/édition en place - Indicateur "Enregistré" discret en bas à droite - Variable NOCODB_VISION_TABLE_ID ajoutée Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+304
-52
@@ -245,6 +245,33 @@
|
||||
|
||||
.vision-quote { font-family: 'Playfair Display', serif; font-size: 17px; font-style: italic; color: var(--text-muted); line-height: 1.7; border-left: 3px solid var(--accent); padding-left: 16px; margin: 12px 0; }
|
||||
|
||||
/* ── Editable Vision ── */
|
||||
[contenteditable] { outline: none; border-radius: 6px; transition: background 0.15s, box-shadow 0.15s; cursor: text; }
|
||||
[contenteditable]:hover { background: rgba(138,112,96,0.06); }
|
||||
[contenteditable]:focus { background: rgba(138,112,96,0.08); box-shadow: 0 0 0 2px var(--accent); }
|
||||
.dark [contenteditable]:hover { background: rgba(196,168,130,0.08); }
|
||||
.dark [contenteditable]:focus { background: rgba(196,168,130,0.1); }
|
||||
[contenteditable].placeholder { color: var(--text-light); font-style: italic; }
|
||||
.vision-saving { position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: var(--bg); padding: 8px 18px; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; opacity: 0; transition: opacity 0.3s; z-index: 200; pointer-events: none; }
|
||||
.vision-saving.show { opacity: 1; }
|
||||
|
||||
.tag-remove { display: none; margin-left: 4px; cursor: pointer; font-size: 11px; opacity: 0.5; }
|
||||
.tag:hover .tag-remove { display: inline; }
|
||||
.tag-remove:hover { opacity: 1; }
|
||||
.tag-add { padding: 8px 16px; border-radius: 20px; font-size: 14px; border: 1px dashed var(--border); background: transparent; color: var(--text-hint); cursor: pointer; font-family: 'Lora', serif; }
|
||||
.tag-add:hover { background: var(--bg-alt); color: var(--text); }
|
||||
|
||||
.list-item-text { flex: 1; }
|
||||
.list-item-remove { display: none; cursor: pointer; color: var(--text-hint); font-size: 14px; margin-left: 8px; }
|
||||
.list-item:hover .list-item-remove { display: inline; }
|
||||
.list-item-remove:hover { color: #8b3030; }
|
||||
.list-add { padding: 6px 0; font-size: 14px; color: var(--text-hint); cursor: pointer; display: flex; align-items: center; gap: 6px; }
|
||||
.list-add:hover { color: var(--accent); }
|
||||
|
||||
.inspi-card-remove { position: absolute; top: 4px; right: 4px; display: none; background: rgba(139,48,48,0.8); color: #fff; border: none; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; cursor: pointer; align-items: center; justify-content: center; }
|
||||
.inspi-card:hover .inspi-card-remove { display: flex; }
|
||||
.inspi-card { position: relative; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
@@ -313,44 +340,25 @@
|
||||
|
||||
<div class="hero-banner">
|
||||
<div class="hero-overlay">
|
||||
<div class="three-words" style="color:rgba(255,255,255,0.5); font-style:italic;">Les trois mots qui définissent votre univers...</div>
|
||||
<div class="hero-sub">Image d'ambiance — cliquer pour remplacer</div>
|
||||
<div class="three-words" id="v-trois-mots" contenteditable="true"></div>
|
||||
<div class="hero-sub" id="v-hero-sub" contenteditable="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="v-card full">
|
||||
<h3>L'histoire en une phrase</h3>
|
||||
<div class="vision-phrase" style="color:var(--text-light); font-style:italic;">Cliquez pour écrire la phrase qui résume votre histoire...</div>
|
||||
<div class="vision-detail" style="color:var(--text-light); font-style:italic;">Modifiable à tout moment par les deux auteurs</div>
|
||||
<div class="vision-phrase" id="v-phrase" contenteditable="true"></div>
|
||||
<div class="vision-detail" id="v-phrase-detail" contenteditable="true"></div>
|
||||
</div>
|
||||
|
||||
<div class="v-card full">
|
||||
<h3>Synopsis</h3>
|
||||
<div class="vision-detail" style="color:var(--text-light); font-style:italic;">Développez ici le résumé de votre histoire en quelques paragraphes — les grandes lignes, les enjeux, l'arc principal...</div>
|
||||
<div class="vision-detail" style="color:var(--text-light); font-style:italic; margin-top:8px;">Peut évoluer au fil de l'écriture</div>
|
||||
<div class="vision-detail" id="v-synopsis" contenteditable="true" style="min-height:80px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="v-card">
|
||||
<h3>Ambiance, époque, rythme</h3>
|
||||
<div class="tag-group">
|
||||
<span class="tag ambiance">Ambiance sombre</span>
|
||||
<span class="tag epoque">Époque fictive</span>
|
||||
<span class="tag rythme">Rythme lent puis rapide</span>
|
||||
<span class="tag genre">Aventure</span>
|
||||
<span class="tag genre">Drame</span>
|
||||
<span class="tag ambiance">Suspense</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="v-card">
|
||||
<h3>Moodboard</h3>
|
||||
<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:10px;">
|
||||
<div style="aspect-ratio:1; border-radius:10px; background:linear-gradient(135deg, var(--accent), var(--brown)); position:relative; overflow:hidden;"><span style="position:absolute;bottom:8px;left:8px;font-size:12px;color:#fff;background:rgba(44,24,16,0.5);padding:3px 8px;border-radius:4px;">Ambiance nuit</span></div>
|
||||
<div style="aspect-ratio:1; border-radius:10px; background:linear-gradient(135deg, #a07850, #8a6840); position:relative; overflow:hidden;"><span style="position:absolute;bottom:8px;left:8px;font-size:12px;color:#fff;background:rgba(44,24,16,0.5);padding:3px 8px;border-radius:4px;">Architecture</span></div>
|
||||
<div style="aspect-ratio:1; border-radius:10px; background:linear-gradient(135deg, #7a8a5a, #5a6a40); position:relative; overflow:hidden;"><span style="position:absolute;bottom:8px;left:8px;font-size:12px;color:#fff;background:rgba(44,24,16,0.5);padding:3px 8px;border-radius:4px;">Forêt</span></div>
|
||||
<div style="aspect-ratio:1; border-radius:10px; background:linear-gradient(135deg, var(--terra), #a07050); position:relative; overflow:hidden;"><span style="position:absolute;bottom:8px;left:8px;font-size:12px;color:#fff;background:rgba(44,24,16,0.5);padding:3px 8px;border-radius:4px;">Costumes</span></div>
|
||||
<div style="aspect-ratio:1; border-radius:10px; border:2px dashed var(--border); display:flex; align-items:center; justify-content:center;"><span style="color:var(--text-hint); font-size:24px;">+</span></div>
|
||||
</div>
|
||||
<div class="tag-group" id="v-tags"></div>
|
||||
</div>
|
||||
|
||||
<div class="v-card">
|
||||
@@ -358,47 +366,24 @@
|
||||
<div class="two-cols">
|
||||
<div class="list-block">
|
||||
<h4 class="yes">On veut</h4>
|
||||
<div class="list-item yes"><span class="bullet"></span> Des personnages profonds</div>
|
||||
<div class="list-item yes"><span class="bullet"></span> Des retournements crédibles</div>
|
||||
<div class="list-item yes"><span class="bullet"></span> Un univers immersif</div>
|
||||
<div id="v-on-veut"></div>
|
||||
</div>
|
||||
<div class="list-block">
|
||||
<h4 class="no">On ne veut pas</h4>
|
||||
<div class="list-item no"><span class="bullet"></span> Un ton Young Adult</div>
|
||||
<div class="list-item no"><span class="bullet"></span> Des coïncidences faciles</div>
|
||||
<div class="list-item no"><span class="bullet"></span> De la violence gratuite</div>
|
||||
<div id="v-on-ne-veut-pas"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="v-card">
|
||||
<h3>Inspirations et références</h3>
|
||||
<div class="inspi-grid">
|
||||
<div class="inspi-card">
|
||||
<div class="inspi-icon">📖</div>
|
||||
<div class="inspi-title">Roman A</div>
|
||||
<div class="inspi-type">Livre</div>
|
||||
</div>
|
||||
<div class="inspi-card">
|
||||
<div class="inspi-icon">🎬</div>
|
||||
<div class="inspi-title">Film B</div>
|
||||
<div class="inspi-type">Film</div>
|
||||
</div>
|
||||
<div class="inspi-card">
|
||||
<div class="inspi-icon">🎨</div>
|
||||
<div class="inspi-title">Tableau C</div>
|
||||
<div class="inspi-type">Peinture</div>
|
||||
</div>
|
||||
<div class="inspi-card" style="border:1px dashed var(--border); background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center;">
|
||||
<div style="font-size:24px; color:var(--text-hint);">+</div>
|
||||
<div class="inspi-type">Ajouter</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inspi-grid" id="v-inspirations"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vision-saving" id="vision-saving">Enregistré</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -836,6 +821,273 @@ document.addEventListener('keydown', e => {
|
||||
});
|
||||
|
||||
loadData();
|
||||
|
||||
// ══════════════════════════════════════
|
||||
// ══ VISION — Editable sections ══
|
||||
// ══════════════════════════════════════
|
||||
|
||||
let visionData = {}; // section -> {Id, Contenu}
|
||||
|
||||
const PLACEHOLDERS = {
|
||||
'trois-mots': 'Les trois mots qui définissent votre univers...',
|
||||
'hero-sous-titre': 'Sous-titre ou accroche...',
|
||||
'phrase': 'Cliquez pour écrire la phrase qui résume votre histoire...',
|
||||
'phrase-detail': 'Un détail, une nuance, une précision...',
|
||||
'synopsis': 'Développez ici le résumé de votre histoire en quelques paragraphes...',
|
||||
};
|
||||
|
||||
const FIELD_MAP = {
|
||||
'trois-mots': 'v-trois-mots',
|
||||
'hero-sous-titre': 'v-hero-sub',
|
||||
'phrase': 'v-phrase',
|
||||
'phrase-detail': 'v-phrase-detail',
|
||||
'synopsis': 'v-synopsis',
|
||||
};
|
||||
|
||||
function showSaved() {
|
||||
const el = document.getElementById('vision-saving');
|
||||
el.classList.add('show');
|
||||
setTimeout(() => el.classList.remove('show'), 1200);
|
||||
}
|
||||
|
||||
async function saveVisionSection(section, contenu) {
|
||||
const row = visionData[section];
|
||||
if (!row) return;
|
||||
await fetch(`/api/vision/${row.Id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Contenu: contenu }),
|
||||
});
|
||||
row.Contenu = contenu;
|
||||
showSaved();
|
||||
}
|
||||
|
||||
function setupEditable(elId, section) {
|
||||
const el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
const placeholder = PLACEHOLDERS[section] || '';
|
||||
|
||||
function updatePlaceholder() {
|
||||
const text = el.textContent.trim();
|
||||
if (!text || text === placeholder) {
|
||||
el.classList.add('placeholder');
|
||||
if (!text) el.textContent = placeholder;
|
||||
} else {
|
||||
el.classList.remove('placeholder');
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener('focus', () => {
|
||||
if (el.classList.contains('placeholder')) {
|
||||
el.textContent = '';
|
||||
el.classList.remove('placeholder');
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener('blur', () => {
|
||||
const text = el.textContent.trim();
|
||||
if (text && text !== placeholder) {
|
||||
const row = visionData[section];
|
||||
if (row && row.Contenu !== text) {
|
||||
saveVisionSection(section, text);
|
||||
}
|
||||
} else {
|
||||
el.textContent = placeholder;
|
||||
el.classList.add('placeholder');
|
||||
const row = visionData[section];
|
||||
if (row && row.Contenu) {
|
||||
saveVisionSection(section, '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent Enter creating divs in single-line fields
|
||||
if (section !== 'synopsis') {
|
||||
el.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); el.blur(); }
|
||||
});
|
||||
}
|
||||
|
||||
updatePlaceholder();
|
||||
}
|
||||
|
||||
// ── Tags ──
|
||||
const TAG_TYPES = ['ambiance', 'epoque', 'rythme', 'genre', 'theme'];
|
||||
|
||||
function renderTags() {
|
||||
const container = document.getElementById('v-tags');
|
||||
const row = visionData['tags'];
|
||||
let tags = [];
|
||||
try { tags = JSON.parse(row?.Contenu || '[]'); } catch { tags = []; }
|
||||
|
||||
container.innerHTML = '';
|
||||
tags.forEach((t, i) => {
|
||||
const span = document.createElement('span');
|
||||
span.className = `tag ${t.type || 'ambiance'}`;
|
||||
span.innerHTML = `${esc(t.label)}<span class="tag-remove" onclick="removeTag(${i})">×</span>`;
|
||||
container.appendChild(span);
|
||||
});
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.className = 'tag-add';
|
||||
addBtn.textContent = '+ Ajouter';
|
||||
addBtn.onclick = () => {
|
||||
const label = prompt('Nouveau tag :');
|
||||
if (!label) return;
|
||||
const type = prompt('Type (ambiance, epoque, rythme, genre, theme) :', 'genre') || 'genre';
|
||||
tags.push({ label, type });
|
||||
saveVisionSection('tags', JSON.stringify(tags));
|
||||
renderTags();
|
||||
};
|
||||
container.appendChild(addBtn);
|
||||
}
|
||||
|
||||
function removeTag(index) {
|
||||
const row = visionData['tags'];
|
||||
let tags = [];
|
||||
try { tags = JSON.parse(row?.Contenu || '[]'); } catch { return; }
|
||||
tags.splice(index, 1);
|
||||
saveVisionSection('tags', JSON.stringify(tags));
|
||||
renderTags();
|
||||
}
|
||||
|
||||
// ── Lists (on-veut / on-ne-veut-pas) ──
|
||||
function renderList(section, containerId, cssClass) {
|
||||
const container = document.getElementById(containerId);
|
||||
const row = visionData[section];
|
||||
let items = [];
|
||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
||||
|
||||
container.innerHTML = '';
|
||||
items.forEach((text, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `list-item ${cssClass}`;
|
||||
div.innerHTML = `<span class="bullet"></span><span class="list-item-text" contenteditable="true">${esc(text)}</span><span class="list-item-remove" onclick="removeListItem('${section}','${containerId}','${cssClass}',${i})">×</span>`;
|
||||
const textEl = div.querySelector('.list-item-text');
|
||||
textEl.addEventListener('blur', () => {
|
||||
const newText = textEl.textContent.trim();
|
||||
if (newText && newText !== items[i]) {
|
||||
items[i] = newText;
|
||||
saveVisionSection(section, JSON.stringify(items));
|
||||
}
|
||||
});
|
||||
textEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); textEl.blur(); }
|
||||
});
|
||||
container.appendChild(div);
|
||||
});
|
||||
|
||||
const addEl = document.createElement('div');
|
||||
addEl.className = 'list-add';
|
||||
addEl.innerHTML = '<span>+</span> Ajouter';
|
||||
addEl.onclick = () => {
|
||||
const text = prompt('Nouvel élément :');
|
||||
if (!text) return;
|
||||
items.push(text);
|
||||
saveVisionSection(section, JSON.stringify(items));
|
||||
renderList(section, containerId, cssClass);
|
||||
};
|
||||
container.appendChild(addEl);
|
||||
}
|
||||
|
||||
function removeListItem(section, containerId, cssClass, index) {
|
||||
const row = visionData[section];
|
||||
let items = [];
|
||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; }
|
||||
items.splice(index, 1);
|
||||
saveVisionSection(section, JSON.stringify(items));
|
||||
renderList(section, containerId, cssClass);
|
||||
}
|
||||
|
||||
// ── Inspirations ──
|
||||
const INSPI_ICONS = { 'Livre': '\u{1F4D6}', 'Film': '\u{1F3AC}', 'Peinture': '\u{1F3A8}', 'Musique': '\u{1F3B5}', 'Série': '\u{1F4FA}', 'Jeu': '\u{1F3AE}', 'Autre': '\u{2728}' };
|
||||
|
||||
function renderInspirations() {
|
||||
const container = document.getElementById('v-inspirations');
|
||||
const row = visionData['inspirations'];
|
||||
let items = [];
|
||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
||||
|
||||
container.innerHTML = '';
|
||||
items.forEach((item, i) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'inspi-card';
|
||||
card.innerHTML = `
|
||||
<button class="inspi-card-remove" onclick="event.stopPropagation();removeInspiration(${i})">×</button>
|
||||
<div class="inspi-icon">${item.icon || INSPI_ICONS[item.type] || '\u{2728}'}</div>
|
||||
<div class="inspi-title" contenteditable="true">${esc(item.title)}</div>
|
||||
<div class="inspi-type">${esc(item.type)}</div>
|
||||
`;
|
||||
const titleEl = card.querySelector('.inspi-title');
|
||||
titleEl.addEventListener('blur', () => {
|
||||
const newTitle = titleEl.textContent.trim();
|
||||
if (newTitle && newTitle !== items[i].title) {
|
||||
items[i].title = newTitle;
|
||||
saveVisionSection('inspirations', JSON.stringify(items));
|
||||
}
|
||||
});
|
||||
titleEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); }
|
||||
});
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
const addCard = document.createElement('div');
|
||||
addCard.className = 'inspi-card';
|
||||
addCard.style.cssText = 'border:1px dashed var(--border); background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer;';
|
||||
addCard.innerHTML = '<div style="font-size:24px; color:var(--text-hint);">+</div><div class="inspi-type">Ajouter</div>';
|
||||
addCard.onclick = () => {
|
||||
const title = prompt('Titre de la référence :');
|
||||
if (!title) return;
|
||||
const type = prompt('Type (Livre, Film, Peinture, Musique, Série, Jeu, Autre) :', 'Livre') || 'Livre';
|
||||
items.push({ title, type, icon: INSPI_ICONS[type] || '\u{2728}' });
|
||||
saveVisionSection('inspirations', JSON.stringify(items));
|
||||
renderInspirations();
|
||||
};
|
||||
container.appendChild(addCard);
|
||||
}
|
||||
|
||||
function removeInspiration(index) {
|
||||
const row = visionData['inspirations'];
|
||||
let items = [];
|
||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; }
|
||||
items.splice(index, 1);
|
||||
saveVisionSection('inspirations', JSON.stringify(items));
|
||||
renderInspirations();
|
||||
}
|
||||
|
||||
// ── Load Vision data ──
|
||||
async function loadVision() {
|
||||
try {
|
||||
const res = await fetch('/api/vision');
|
||||
if (res.status === 401) return;
|
||||
const data = await res.json();
|
||||
const rows = data.list || [];
|
||||
|
||||
// Index by section
|
||||
visionData = {};
|
||||
rows.forEach(r => { visionData[r.Section] = r; });
|
||||
|
||||
// Populate text fields
|
||||
for (const [section, elId] of Object.entries(FIELD_MAP)) {
|
||||
const el = document.getElementById(elId);
|
||||
if (!el) continue;
|
||||
const contenu = visionData[section]?.Contenu || '';
|
||||
el.textContent = contenu || PLACEHOLDERS[section] || '';
|
||||
setupEditable(elId, section);
|
||||
}
|
||||
|
||||
// Render structured sections
|
||||
renderTags();
|
||||
renderList('on-veut', 'v-on-veut', 'yes');
|
||||
renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no');
|
||||
renderInspirations();
|
||||
} catch (e) {
|
||||
console.error('Vision load error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
loadVision();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -14,6 +14,7 @@ const NOCODB_URL = process.env.NOCODB_URL || 'https://nocodb.hub.delvarre.net';
|
||||
const NOCODB_TOKEN = process.env.NOCODB_TOKEN || '';
|
||||
const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID || '';
|
||||
const NOCODB_TABLE_ID = process.env.NOCODB_TABLE_ID || '';
|
||||
const NOCODB_VISION_TABLE_ID = process.env.NOCODB_VISION_TABLE_ID || '';
|
||||
|
||||
// Auth
|
||||
function makeToken(ts) {
|
||||
@@ -142,6 +143,36 @@ app.post('/api/reorder', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Vision API ──
|
||||
|
||||
// List all vision sections
|
||||
app.get('/api/vision', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VISION_TABLE_ID}?limit=50&sort=Ordre`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const data = await r.json();
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a vision section
|
||||
app.patch('/api/vision/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VISION_TABLE_ID}/${req.params.id}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
const data = await r.json();
|
||||
res.status(r.status).json(data);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Auth gate — redirect to login page if not authenticated
|
||||
app.get('/', (req, res) => {
|
||||
if (isAuth(req)) {
|
||||
|
||||
Reference in New Issue
Block a user