Uklonjen autofocus sa modala koji prikazuje formu bez poziva
All checks were successful
Tests / unit-tests (push) Successful in 25s

- autofocus na inputu u hidden modalu izazivao prikaz forme
  na nekim browserima pri učitavanju stranice
- Fokus se sada daje programski tek kad se modal otvori
- Isto rešenje primenjeno i na projects.html i chat.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
djuka 2026-02-18 06:56:13 +00:00
parent 6c0ca3a96f
commit e60e574287
3 changed files with 491 additions and 77 deletions

View File

@ -221,17 +221,18 @@ func NewPTYSessionManager() *PTYSessionManager {
} }
// GetOrCreate returns an existing session or creates a new one. // GetOrCreate returns an existing session or creates a new one.
func (m *PTYSessionManager) GetOrCreate(project, projectDir string) (*PTYSession, bool, error) { // sessionKey is a free-form string (e.g. "project:tabId").
func (m *PTYSessionManager) GetOrCreate(sessionKey, projectDir string) (*PTYSession, bool, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if sess, ok := m.sessions[project]; ok { if sess, ok := m.sessions[sessionKey]; ok {
// Check if process is still alive // Check if process is still alive
select { select {
case <-sess.Done(): case <-sess.Done():
// Process exited, remove and create new // Process exited, remove and create new
sess.Close() sess.Close()
delete(m.sessions, project) delete(m.sessions, sessionKey)
default: default:
sess.mu.Lock() sess.mu.Lock()
sess.lastActive = time.Now() sess.lastActive = time.Now()
@ -245,7 +246,7 @@ func (m *PTYSessionManager) GetOrCreate(project, projectDir string) (*PTYSession
return nil, false, err return nil, false, err
} }
m.sessions[project] = sess m.sessions[sessionKey] = sess
return sess, true, nil return sess, true, nil
} }

View File

@ -16,6 +16,7 @@
--text-muted: #6c6c80; --text-muted: #6c6c80;
--accent: #e94560; --accent: #e94560;
--accent-hover: #ff6b81; --accent-hover: #ff6b81;
--error: #f44336;
} }
body { body {
background: var(--bg-primary); background: var(--bg-primary);
@ -141,7 +142,99 @@
cursor: pointer; cursor: pointer;
} }
.terminal-header button:hover { border-color: var(--accent); color: var(--text-primary); } .terminal-header button:hover { border-color: var(--accent); color: var(--text-primary); }
#terminal-container { flex: 1; overflow: hidden; }
/* Tab bar */
.tab-bar {
display: flex;
align-items: center;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 0 0.25rem;
flex-shrink: 0;
overflow-x: auto;
gap: 2px;
min-height: 30px;
}
.tab-bar::-webkit-scrollbar { height: 0; }
.tab {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.2rem 0.5rem;
font-size: 0.7rem;
color: var(--text-muted);
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
border-radius: 4px 4px 0 0;
white-space: nowrap;
user-select: none;
transition: color 0.15s, background 0.15s;
}
.tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.03); }
.tab.active { color: var(--text-primary); background: var(--bg-primary); border-color: var(--border); }
.tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 0.65rem;
border-radius: 3px;
color: var(--text-muted);
transition: background 0.1s, color 0.1s;
}
.tab-close:hover { background: rgba(244,67,54,0.3); color: var(--error); }
.tab-add-wrap { position: relative; flex-shrink: 0; }
.tab-add {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 0.85rem;
color: var(--text-muted);
cursor: pointer;
border-radius: 4px;
border: none;
background: none;
font-family: inherit;
transition: color 0.15s, background 0.15s;
}
.tab-add:hover { color: var(--text-primary); background: rgba(255,255,255,0.05); }
.tab-dropdown {
position: absolute;
top: 100%;
left: 0;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
min-width: 180px;
max-height: 300px;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.tab-dropdown.hidden { display: none; }
.tab-dropdown-item {
display: block;
width: 100%;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
font-family: inherit;
color: var(--text-secondary);
background: none;
border: none;
text-align: left;
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.tab-dropdown-item:hover { background: rgba(233,69,96,0.15); color: var(--text-primary); }
/* Terminal containers */
#terminals { flex: 1; overflow: hidden; position: relative; }
.term-pane { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; }
.term-pane.active { display: block; }
.xterm { height: 100%; } .xterm { height: 100%; }
/* File viewer overlay */ /* File viewer overlay */
@ -201,6 +294,47 @@
.file-viewer-content th { background: var(--bg-secondary); color: var(--accent); font-weight: 600; } .file-viewer-content th { background: var(--bg-secondary); color: var(--accent); font-weight: 600; }
.file-viewer-content tr:nth-child(even) { background: rgba(22,27,34,0.5); } .file-viewer-content tr:nth-child(even) { background: rgba(22,27,34,0.5); }
/* Sidebar project list */
.sidebar-projects-list { max-height: 180px; overflow-y: auto; }
.sidebar-projects-list .file-item.active {
color: var(--accent);
background: rgba(233,69,96,0.1);
font-weight: 600;
}
.sidebar-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 0.8rem;
color: var(--text-muted);
cursor: pointer;
border-radius: 3px;
transition: color 0.15s, background 0.15s;
line-height: 1;
}
.sidebar-add-btn:hover { color: var(--accent); background: rgba(233,69,96,0.15); }
/* Create project modal */
.create-project-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); display: flex; align-items: center;
justify-content: center; z-index: 300;
}
.create-project-box {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 8px; padding: 1rem; width: 260px;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.create-project-box h3 { color: var(--accent); font-size: 0.85rem; margin-bottom: 0.6rem; }
.create-project-box input {
width: 100%; padding: 0.35rem 0.5rem; background: var(--bg-primary);
border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary);
font-size: 0.75rem; font-family: inherit; outline: none;
}
.create-project-box input:focus { border-color: var(--accent); }
/* Scrollbar */ /* Scrollbar */
::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
@ -215,6 +349,20 @@
<h3>{{.Project}}</h3> <h3>{{.Project}}</h3>
<button onclick="toggleSidebar()" title="Sakrij sidebar">&#x2190;</button> <button onclick="toggleSidebar()" title="Sakrij sidebar">&#x2190;</button>
</div> </div>
<!-- Projekti -->
<div class="sidebar-section">
<div class="sidebar-section-title" style="display:flex;justify-content:space-between;align-items:center;">
Projekti
<span class="sidebar-add-btn" onclick="showCreateProjectModal()" title="Novi projekat">+</span>
</div>
</div>
<div class="sidebar-files sidebar-projects-list">
{{range .Projects}}
<a class="file-item{{if eq .Name $.Project}} active{{end}}" href="/chat/{{.Name}}">{{.Name}}</a>
{{end}}
</div>
{{if .Files}} {{if .Files}}
<div class="sidebar-section"> <div class="sidebar-section">
<div class="sidebar-section-title">Dokumentacija</div> <div class="sidebar-section-title">Dokumentacija</div>
@ -224,18 +372,29 @@
<a class="file-item" href="#" onclick="loadFile('{{.RelPath}}'); return false;">{{.Name}}</a> <a class="file-item" href="#" onclick="loadFile('{{.RelPath}}'); return false;">{{.Name}}</a>
{{end}} {{end}}
</div> </div>
{{else}}
<div class="sidebar-files" style="padding: 0.75rem; color: var(--text-muted); font-size: 0.75rem;">
Nema .md fajlova
</div>
{{end}} {{end}}
<div class="sidebar-footer"> <div class="sidebar-footer">
<a href="/projects">Svi projekti</a>
<a href="/change-password">Promeni lozinku</a> <a href="/change-password">Promeni lozinku</a>
<a href="/logout">Odjavi se</a> <a href="/logout">Odjavi se</a>
</div> </div>
</div> </div>
<!-- Modal za kreiranje projekta -->
<div id="createProjectModal" class="create-project-modal hidden" onclick="if(event.target===this)this.classList.add('hidden')">
<div class="create-project-box">
<h3>Novi projekat</h3>
<form id="createProjectForm" onsubmit="return createProject(event)">
<input type="text" id="newProjectName" placeholder="ime-projekta" required>
<p style="color:var(--text-muted);font-size:0.65rem;margin:0.3rem 0;">Slova, brojevi, - i _</p>
<div style="display:flex;gap:0.4rem;justify-content:flex-end;">
<button type="button" onclick="document.getElementById('createProjectModal').classList.add('hidden')" style="background:var(--bg-primary);color:var(--text-secondary);border:1px solid var(--border);border-radius:4px;padding:0.3rem 0.6rem;font-size:0.7rem;cursor:pointer;font-family:inherit;">Otkaži</button>
<button type="submit" style="background:var(--accent);color:#fff;border:none;border-radius:4px;padding:0.3rem 0.6rem;font-size:0.7rem;cursor:pointer;font-family:inherit;">Kreiraj</button>
</div>
</form>
<div id="createProjectError" style="color:var(--error);font-size:0.65rem;margin-top:0.3rem;display:none;"></div>
</div>
</div>
<!-- Main terminal area --> <!-- Main terminal area -->
<div class="main"> <div class="main">
<div class="terminal-header"> <div class="terminal-header">
@ -255,10 +414,16 @@
<option value="tokyo-night">Tokyo Night</option> <option value="tokyo-night">Tokyo Night</option>
<option value="catppuccin">Catppuccin</option> <option value="catppuccin">Catppuccin</option>
</select> </select>
<a href="/projects" style="color:var(--text-muted);text-decoration:none;font-size:0.7rem;">Projekti</a>
</div> </div>
</div> </div>
<div id="terminal-container"></div> <div class="tab-bar" id="tab-bar">
<!-- Tabs rendered by JS -->
<div class="tab-add-wrap">
<button class="tab-add" id="tab-add-btn" title="Novi tab">+</button>
<div class="tab-dropdown hidden" id="tab-dropdown"></div>
</div>
</div>
<div id="terminals"></div>
</div> </div>
<!-- File viewer overlay --> <!-- File viewer overlay -->
@ -274,8 +439,12 @@
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
<script> <script>
// ── Config from server ──────────────────────────────
var PAGE_PROJECT = '{{.Project}}';
var PAGE_PROJECT_DIR = '{{.ProjectDir}}';
// ── Themes ────────────────────────────────────────── // ── Themes ──────────────────────────────────────────
const THEMES = { var THEMES = {
dark: { dark: {
background: '#0d1117', foreground: '#e0e0e0', cursor: '#e94560', cursorAccent: '#0d1117', background: '#0d1117', foreground: '#e0e0e0', cursor: '#e94560', cursorAccent: '#0d1117',
selectionBackground: 'rgba(233,69,96,0.3)', selectionBackground: 'rgba(233,69,96,0.3)',
@ -342,111 +511,301 @@
} }
}; };
// ── Terminal ──────────────────────────────────────── // ── Theme ───────────────────────────────────────────
const savedTheme = localStorage.getItem('terminal-theme') || 'dark'; var savedTheme = localStorage.getItem('terminal-theme') || 'dark';
document.getElementById('theme-select').value = savedTheme; document.getElementById('theme-select').value = savedTheme;
const statusEl = document.getElementById('ws-status');
const container = document.getElementById('terminal-container');
const term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
cursorInactiveStyle: 'outline',
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
theme: THEMES[savedTheme] || THEMES.dark,
allowProposedApi: true,
scrollback: 10000,
convertEol: false,
drawBoldTextInBrightColors: true,
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon.WebLinksAddon());
term.open(container);
fitAddon.fit();
function applyTheme(name) { function applyTheme(name) {
const theme = THEMES[name]; var theme = THEMES[name];
if (!theme) return; if (!theme) return;
term.options.theme = theme; savedTheme = name;
document.body.style.background = theme.background; document.body.style.background = theme.background;
document.querySelector('.main').style.background = theme.background; document.querySelector('.main').style.background = theme.background;
localStorage.setItem('terminal-theme', name); localStorage.setItem('terminal-theme', name);
// Apply to all open tabs
tabs.forEach(function(tab) {
tab.term.options.theme = theme;
});
} }
// Apply saved theme to body
applyTheme(savedTheme);
// ── WebSocket ─────────────────────────────────────── // ── Tab system ──────────────────────────────────────
let ws; var tabs = [];
let reconnectTimer; var activeTabId = null;
var nextTabId = 1;
var statusEl = document.getElementById('ws-status');
var tabBarEl = document.getElementById('tab-bar');
var terminalsEl = document.getElementById('terminals');
var tabAddBtn = document.getElementById('tab-add-btn');
var tabDropdown = document.getElementById('tab-dropdown');
var projectsCache = null;
function connect() { function createTerminal() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; var theme = THEMES[savedTheme] || THEMES.dark;
ws = new WebSocket(proto + '//' + location.host + '/ws?project={{.Project}}&project_dir={{.ProjectDir}}'); var term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
cursorInactiveStyle: 'outline',
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
theme: theme,
allowProposedApi: true,
scrollback: 10000,
convertEol: false,
drawBoldTextInBrightColors: true,
});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon.WebLinksAddon());
return { term: term, fitAddon: fitAddon };
}
function connectTab(tab) {
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/ws?project=' + encodeURIComponent(tab.project) +
'&project_dir=' + encodeURIComponent(tab.projectDir) +
'&tab=' + encodeURIComponent(tab.id);
var ws = new WebSocket(url);
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer';
tab.ws = ws;
ws.onopen = function() { ws.onopen = function() {
statusEl.textContent = 'Povezan'; if (activeTabId === tab.id) {
statusEl.className = 'status connected'; statusEl.textContent = 'Povezan';
ws.send(JSON.stringify({type: 'resize', cols: term.cols, rows: term.rows})); statusEl.className = 'status connected';
term.focus(); }
ws.send(JSON.stringify({ type: 'resize', cols: tab.term.cols, rows: tab.term.rows }));
if (activeTabId === tab.id) tab.term.focus();
}; };
ws.onmessage = function(event) { ws.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) { if (event.data instanceof ArrayBuffer) {
term.write(new Uint8Array(event.data)); tab.term.write(new Uint8Array(event.data));
} else { } else {
term.write(event.data); tab.term.write(event.data);
} }
}; };
ws.onclose = function() { ws.onclose = function() {
statusEl.textContent = 'Nepovezan'; if (activeTabId === tab.id) {
statusEl.className = 'status'; statusEl.textContent = 'Nepovezan';
clearTimeout(reconnectTimer); statusEl.className = 'status';
reconnectTimer = setTimeout(connect, 2000); }
// Reconnect only if tab still exists
if (tabs.find(function(t) { return t.id === tab.id; })) {
tab.reconnectTimer = setTimeout(function() { connectTab(tab); }, 2000);
}
}; };
ws.onerror = function() { ws.onerror = function() {
statusEl.textContent = 'Greška'; if (activeTabId === tab.id) {
statusEl.className = 'status'; statusEl.textContent = 'Greška';
statusEl.className = 'status';
}
}; };
} }
term.onData(function(data) { function addTab(project, projectDir) {
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data); var tabId = String(nextTabId++);
}); var t = createTerminal();
term.onResize(function(size) { // Create pane
if (ws && ws.readyState === WebSocket.OPEN) { var pane = document.createElement('div');
ws.send(JSON.stringify({type: 'resize', cols: size.cols, rows: size.rows})); pane.className = 'term-pane';
pane.id = 'pane-' + tabId;
terminalsEl.appendChild(pane);
t.term.open(pane);
var tab = {
id: tabId,
project: project,
projectDir: projectDir,
term: t.term,
fitAddon: t.fitAddon,
ws: null,
reconnectTimer: null,
container: pane
};
// Wire input
t.term.onData(function(data) {
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) tab.ws.send(data);
});
t.term.onResize(function(size) {
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
tab.ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
}
});
// Click on pane gives focus
pane.addEventListener('click', function() { tab.term.focus(); });
tabs.push(tab);
renderTabs();
switchTab(tabId);
connectTab(tab);
return tab;
}
function switchTab(tabId) {
activeTabId = tabId;
tabs.forEach(function(tab) {
if (tab.id === tabId) {
tab.container.classList.add('active');
} else {
tab.container.classList.remove('active');
}
});
renderTabs();
var active = tabs.find(function(t) { return t.id === tabId; });
if (active) {
setTimeout(function() {
active.fitAddon.fit();
active.term.focus();
}, 50);
// Update status
if (active.ws && active.ws.readyState === WebSocket.OPEN) {
statusEl.textContent = 'Povezan';
statusEl.className = 'status connected';
} else {
statusEl.textContent = 'Nepovezan';
statusEl.className = 'status';
}
}
}
function closeTab(tabId) {
if (tabs.length <= 1) return; // Don't close last tab
var idx = tabs.findIndex(function(t) { return t.id === tabId; });
if (idx === -1) return;
var tab = tabs[idx];
// Clean up
clearTimeout(tab.reconnectTimer);
if (tab.ws) {
tab.ws.onclose = null; // Prevent reconnect
tab.ws.close();
}
tab.term.dispose();
tab.container.remove();
tabs.splice(idx, 1);
// Switch to neighbor if active was closed
if (activeTabId === tabId) {
var newIdx = Math.min(idx, tabs.length - 1);
switchTab(tabs[newIdx].id);
} else {
renderTabs();
}
}
function renderTabs() {
// Remove old tab elements (keep the add button wrapper)
var addWrap = document.querySelector('.tab-add-wrap');
var oldTabs = tabBarEl.querySelectorAll('.tab');
oldTabs.forEach(function(el) { el.remove(); });
tabs.forEach(function(tab) {
var el = document.createElement('div');
el.className = 'tab' + (tab.id === activeTabId ? ' active' : '');
var label = document.createElement('span');
label.textContent = tab.project;
label.addEventListener('click', function(e) {
e.stopPropagation();
switchTab(tab.id);
});
el.appendChild(label);
if (tabs.length > 1) {
var closeBtn = document.createElement('span');
closeBtn.className = 'tab-close';
closeBtn.textContent = '\u00d7';
closeBtn.title = 'Zatvori tab';
closeBtn.addEventListener('click', function(e) {
e.stopPropagation();
closeTab(tab.id);
});
el.appendChild(closeBtn);
}
tabBarEl.insertBefore(el, addWrap);
});
}
// ── Add tab dropdown ────────────────────────────────
tabAddBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (!tabDropdown.classList.contains('hidden')) {
tabDropdown.classList.add('hidden');
return;
}
// Fetch projects and show dropdown
if (projectsCache) {
showDropdown(projectsCache);
} else {
fetch('/api/projects')
.then(function(r) { return r.json(); })
.then(function(data) {
projectsCache = data;
showDropdown(data);
})
.catch(function() {
// Fallback: just add tab for current project
addTab(PAGE_PROJECT, PAGE_PROJECT_DIR);
});
} }
}); });
window.addEventListener('resize', function() { fitAddon.fit(); }); function showDropdown(projects) {
tabDropdown.innerHTML = '';
projects.forEach(function(p) {
var btn = document.createElement('button');
btn.className = 'tab-dropdown-item';
btn.textContent = p.name;
btn.addEventListener('click', function() {
tabDropdown.classList.add('hidden');
addTab(p.name, p.path);
});
tabDropdown.appendChild(btn);
});
tabDropdown.classList.remove('hidden');
}
// Click on terminal area gives focus // Close dropdown when clicking outside
container.addEventListener('click', function() { term.focus(); }); document.addEventListener('click', function() {
tabDropdown.classList.add('hidden');
});
connect(); // ── Window resize ───────────────────────────────────
window.addEventListener('resize', function() {
var active = tabs.find(function(t) { return t.id === activeTabId; });
if (active) active.fitAddon.fit();
});
// ── Apply initial theme ─────────────────────────────
applyTheme(savedTheme);
// ── Create first tab ────────────────────────────────
addTab(PAGE_PROJECT, PAGE_PROJECT_DIR);
// ── Sidebar ───────────────────────────────────────── // ── Sidebar ─────────────────────────────────────────
const sidebar = document.getElementById('sidebar'); var sidebar = document.getElementById('sidebar');
const sidebarState = localStorage.getItem('sidebar-visible'); var sidebarState = localStorage.getItem('sidebar-visible');
if (sidebarState === 'false') sidebar.classList.add('collapsed'); if (sidebarState === 'false') sidebar.classList.add('collapsed');
function toggleSidebar() { function toggleSidebar() {
sidebar.classList.toggle('collapsed'); sidebar.classList.toggle('collapsed');
localStorage.setItem('sidebar-visible', !sidebar.classList.contains('collapsed')); localStorage.setItem('sidebar-visible', !sidebar.classList.contains('collapsed'));
setTimeout(function() { fitAddon.fit(); }, 200); setTimeout(function() {
var active = tabs.find(function(t) { return t.id === activeTabId; });
if (active) active.fitAddon.fit();
}, 200);
} }
// ── File viewer ───────────────────────────────────── // ── File viewer ─────────────────────────────────────
function loadFile(relPath) { function loadFile(relPath) {
fetch('/api/file?project={{.Project}}&path=' + encodeURIComponent(relPath)) fetch('/api/file?project=' + encodeURIComponent(PAGE_PROJECT) + '&path=' + encodeURIComponent(relPath))
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
document.getElementById('file-viewer-title').textContent = data.name; document.getElementById('file-viewer-title').textContent = data.name;
@ -458,12 +817,56 @@
function closeFileViewer() { function closeFileViewer() {
document.getElementById('file-viewer').classList.add('hidden'); document.getElementById('file-viewer').classList.add('hidden');
term.focus(); var active = tabs.find(function(t) { return t.id === activeTabId; });
if (active) active.term.focus();
} }
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeFileViewer(); if (e.key === 'Escape') {
closeFileViewer();
document.getElementById('createProjectModal').classList.add('hidden');
}
}); });
// ── Create project ───────────────────────────────────
function showCreateProjectModal() {
document.getElementById('createProjectModal').classList.remove('hidden');
setTimeout(function() {
document.getElementById('newProjectName').focus();
}, 50);
}
function createProject(e) {
e.preventDefault();
var name = document.getElementById('newProjectName').value.trim();
var errEl = document.getElementById('createProjectError');
if (!name) return false;
var form = new FormData();
form.append('name', name);
fetch('/projects/create', {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) {
return r.json().then(function(data) {
if (r.ok) {
projectsCache = null; // Invalidate cache
window.location.href = '/chat/' + encodeURIComponent(name);
} else {
errEl.textContent = data.error || 'Greška pri kreiranju';
errEl.style.display = 'block';
}
});
})
.catch(function() {
errEl.textContent = 'Greška pri kreiranju';
errEl.style.display = 'block';
});
return false;
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -11,7 +11,7 @@
<div class="projects-header"> <div class="projects-header">
<h1>Projekti</h1> <h1>Projekti</h1>
<div> <div>
<button class="btn btn-secondary" onclick="document.getElementById('createModal').classList.remove('hidden')">Novi projekat</button> <button class="btn btn-secondary" onclick="showCreateModal()">Novi projekat</button>
<a href="/change-password" class="btn btn-secondary">Promeni lozinku</a> <a href="/change-password" class="btn btn-secondary">Promeni lozinku</a>
<a href="/logout" class="btn">Odjavi se</a> <a href="/logout" class="btn">Odjavi se</a>
</div> </div>
@ -27,7 +27,7 @@
<form method="POST" action="/projects/create"> <form method="POST" action="/projects/create">
<div class="form-group"> <div class="form-group">
<label for="projectName">Ime projekta</label> <label for="projectName">Ime projekta</label>
<input type="text" id="projectName" name="name" placeholder="moj-projekat" required autofocus> <input type="text" id="projectName" name="name" placeholder="moj-projekat" required>
</div> </div>
<p class="modal-hint">Dozvoljeni karakteri: slova, brojevi, - i _</p> <p class="modal-hint">Dozvoljeni karakteri: slova, brojevi, - i _</p>
<div class="modal-actions"> <div class="modal-actions">
@ -54,5 +54,15 @@
<p style="color: var(--text-secondary);">Nema projekata u {{.ProjectsPath}}</p> <p style="color: var(--text-secondary);">Nema projekata u {{.ProjectsPath}}</p>
{{end}} {{end}}
</div> </div>
<script>
function showCreateModal() {
var modal = document.getElementById('createModal');
modal.classList.remove('hidden');
// Focus input after modal is visible
setTimeout(function() {
document.getElementById('projectName').focus();
}, 50);
}
</script>
</body> </body>
</html> </html>