Add contact page/includes and refine header action/logo styling
This commit is contained in:
parent
613d7e15a3
commit
be9da9fe86
7 changed files with 404 additions and 231 deletions
31
_footer.php
Normal file
31
_footer.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<footer>Bubulescu.Org</footer>
|
||||
<script>
|
||||
(function(){
|
||||
const btn=document.getElementById('push-toggle');
|
||||
if(!btn || !('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return;
|
||||
function b64uToUint8Array(s){ const pad='='.repeat((4-s.length%4)%4); const b64=(s+pad).replace(/-/g,'+').replace(/_/g,'/'); const raw=atob(b64); return Uint8Array.from([...raw].map(c=>c.charCodeAt(0))); }
|
||||
async function state(reg){ const sub=await reg.pushManager.getSubscription(); btn.classList.toggle('active', !!sub); btn.title=sub?'Notifications on':'Notifications'; btn.hidden=false; return sub; }
|
||||
navigator.serviceWorker.register('sw.js').then(async reg=>{
|
||||
const cfg=await fetch('push_config.php',{cache:'no-store'}).then(r=>r.json()).catch(()=>({enabled:false}));
|
||||
if(!cfg.enabled || !cfg.publicKey) return;
|
||||
await state(reg);
|
||||
btn.addEventListener('click', async ()=>{
|
||||
btn.disabled=true;
|
||||
try{
|
||||
let sub=await reg.pushManager.getSubscription();
|
||||
if(sub){
|
||||
return;
|
||||
}else{
|
||||
const perm=await Notification.requestPermission();
|
||||
if(perm!=='granted') return;
|
||||
sub=await reg.pushManager.subscribe({userVisibleOnly:true,applicationServerKey:b64uToUint8Array(cfg.publicKey)});
|
||||
await fetch('push_subscribe.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(sub)});
|
||||
}
|
||||
await state(reg);
|
||||
}catch(e){ alert(e.message || 'Could not update notifications'); }
|
||||
finally{ await state(reg); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="js/map_lightbox.js?v=20260531b" defer></script>
|
||||
183
_footer_admin.php
Normal file
183
_footer_admin.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<footer class="site-footer admin-footer"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div id="job-status" class="job-status"<?= empty($runningJobs) ? ' hidden' : '' ?>><?php foreach($runningJobs as $job): ?><span class="job-dot"></span><span><?=h(str_replace('_', ' ', $job['name']))?></span><?php if(!empty($job['started_at'])): ?><span class="job-age">(<?=h(format_job_age($job['started_at']))?>)</span><?php endif; ?><?php endforeach; ?></div><div class="admin-right"><a class="logout-link" href="logout.php">Log off</a></div></footer>
|
||||
<script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script>
|
||||
<script>
|
||||
(function(){
|
||||
function attachDescribeButtons(){
|
||||
document.querySelectorAll('.admin-video-row').forEach(function(row){
|
||||
if (row.querySelector('.describe-button')) return;
|
||||
var actions = row.querySelector('.admin-actions');
|
||||
if (!actions) return;
|
||||
var job = row.dataset.job || '';
|
||||
var articleId = row.dataset.id || '';
|
||||
if (!job && !articleId) return;
|
||||
var describe = document.createElement('a');
|
||||
describe.className = 'button describe-button';
|
||||
describe.href = '#';
|
||||
describe.dataset.job = job;
|
||||
describe.dataset.id = articleId;
|
||||
describe.textContent = 'Describe';
|
||||
var first = actions.querySelector('.button');
|
||||
if (first) actions.insertBefore(describe, first);
|
||||
else actions.appendChild(describe);
|
||||
});
|
||||
}
|
||||
|
||||
function showModal(job, articleId, description, teaser, quoteDa, rawJson){
|
||||
var existing = document.getElementById('mvlog-gemini-modal');
|
||||
if (existing) existing.remove();
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'mvlog-gemini-modal';
|
||||
overlay.style.cssText = 'position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = 'background:white;color:#111;padding:20px;max-width:900px;max-height:80vh;overflow:auto;border-radius:6px;font-family:inherit;';
|
||||
var h = document.createElement('h3');
|
||||
h.textContent = 'Generated description';
|
||||
var teaserBox = null;
|
||||
if (teaser) {
|
||||
teaserBox = document.createElement('div');
|
||||
teaserBox.style.cssText = 'white-space:pre-wrap;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:10px;background:#fff7e8;border-radius:4px;';
|
||||
teaserBox.textContent = teaser;
|
||||
}
|
||||
var quoteBox = null;
|
||||
if (quoteDa) {
|
||||
quoteBox = document.createElement('div');
|
||||
quoteBox.style.cssText = 'white-space:pre-wrap;font-style:italic;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:10px;background:#f4f0ff;border-radius:4px;';
|
||||
quoteBox.textContent = '“' + quoteDa + '”';
|
||||
}
|
||||
var pre = document.createElement('div');
|
||||
pre.style.cssText = 'white-space:pre-wrap;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:12px;background:#f8f8f8;border-radius:4px;';
|
||||
pre.textContent = description || '';
|
||||
var btnBar = document.createElement('div');
|
||||
btnBar.style.cssText = 'margin-top:12px;text-align:right;';
|
||||
var useBtn = document.createElement('button');
|
||||
useBtn.className = 'button';
|
||||
useBtn.textContent = 'Use in form';
|
||||
var cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'button';
|
||||
cancelBtn.textContent = 'Close';
|
||||
cancelBtn.style.marginLeft = '8px';
|
||||
btnBar.appendChild(useBtn);
|
||||
btnBar.appendChild(cancelBtn);
|
||||
box.appendChild(h);
|
||||
if (teaserBox) box.appendChild(teaserBox);
|
||||
if (quoteBox) box.appendChild(quoteBox);
|
||||
box.appendChild(pre);
|
||||
box.appendChild(btnBar);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
cancelBtn.addEventListener('click', function(){ overlay.remove(); });
|
||||
useBtn.addEventListener('click', function(){
|
||||
var editForm = document.getElementById('edit-form');
|
||||
if (editForm){
|
||||
var idInput = editForm.querySelector('input[name="id"]');
|
||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||
var sameById = !!(articleId && idInput && idInput.value === articleId);
|
||||
var sameByDir = !!(job && dirInput && dirInput.value === job);
|
||||
if (sameById || sameByDir){
|
||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||
if (textarea){
|
||||
textarea.value = description || '';
|
||||
var teaserInput = editForm.querySelector('input[name="teaser"]');
|
||||
if (teaserInput && teaser) teaserInput.value = teaser;
|
||||
var quoteInput = editForm.querySelector('input[name="quote_da"]');
|
||||
if (quoteInput && quoteDa) quoteInput.value = quoteDa;
|
||||
showToast('Inserted generated description' + (teaser || quoteDa ? ' and extras' : '') + ' into form. Click Save changes to apply.', true);
|
||||
overlay.remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
var storageKey = 'mvlog_gemini_description_' + (articleId || job);
|
||||
try { localStorage.setItem(storageKey, JSON.stringify({description: description || '', teaser: teaser || '', quote_da: quoteDa || ''})); } catch(e){}
|
||||
var href = 'new.php?tab=edit';
|
||||
if (articleId) href += '&id=' + encodeURIComponent(articleId);
|
||||
if (job) href += '&edit=' + encodeURIComponent(job);
|
||||
window.location.href = href;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e){
|
||||
var el = (e.target && e.target.closest && e.target.closest('.describe-button')) || (e.target && e.target.classList && e.target.classList.contains && e.target.classList.contains('describe-button') ? e.target : null);
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
var job = el.dataset.job || '';
|
||||
var articleId = el.dataset.id || '';
|
||||
if (!job && !articleId) return;
|
||||
el.classList.add('disabled');
|
||||
var oldText = el.textContent;
|
||||
el.textContent = 'Generating...';
|
||||
fetch('lib/generate_data_sync.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body: 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16'
|
||||
}).then(function(r){ return r.json().catch(()=>({})); })
|
||||
.then(function(data){
|
||||
if (data && data.status === 'ok'){
|
||||
var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || '';
|
||||
var teaser = data.teaser || '';
|
||||
var rawFirst = (Array.isArray(data.raw_json) && data.raw_json.length) ? data.raw_json[0] : data.raw_json;
|
||||
var quoteDa = data.quote_da || (rawFirst && rawFirst.quote_da) || '';
|
||||
showModal(job, articleId, desc, teaser, quoteDa, data.raw_json || null);
|
||||
} else {
|
||||
var msg = (data && data.message) ? data.message : 'No response';
|
||||
if (data && data.log) {
|
||||
try {
|
||||
var log = String(data.log);
|
||||
var lines = log.split('\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });
|
||||
var keywords = ['quota','rate limit','rate-limit','429','503','overloaded','high demand','busy','unavailable','error','failed','retry'];
|
||||
var found = lines.find(function(l){ var ll=l.toLowerCase(); return keywords.some(function(k){ return ll.indexOf(k) !== -1; }); });
|
||||
if (found) msg += ' — ' + found;
|
||||
else if (lines.length) msg += ' — ' + lines[0].slice(0,200);
|
||||
} catch(e){}
|
||||
}
|
||||
showToast('Error: ' + msg, false, (data && data.log) ? (typeof data.log === 'string' ? data.log : JSON.stringify(data.log)) : null);
|
||||
}
|
||||
}).catch(function(err){
|
||||
showToast('Request failed: ' + err, false);
|
||||
}).finally(function(){
|
||||
el.classList.remove('disabled');
|
||||
el.textContent = oldText;
|
||||
});
|
||||
});
|
||||
|
||||
attachDescribeButtons();
|
||||
var list = document.querySelector('.admin-list.video-list');
|
||||
if (list) new MutationObserver(function(){ attachDescribeButtons(); }).observe(list, {childList:true, subtree:true});
|
||||
|
||||
// On edit page load: if a generated description is in localStorage, insert it into the form.
|
||||
(function(){
|
||||
var editForm = document.getElementById('edit-form');
|
||||
if (!editForm) return;
|
||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||
var idInput = editForm.querySelector('input[name="id"]');
|
||||
if (!dirInput && !idInput) return;
|
||||
var job = dirInput ? dirInput.value : '';
|
||||
var articleId = idInput ? idInput.value : '';
|
||||
try {
|
||||
var key = 'mvlog_gemini_description_' + (articleId || job);
|
||||
var raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
var payload = null;
|
||||
try { payload = JSON.parse(raw); } catch(e) { payload = {description: raw, teaser: '', quote_da: ''}; }
|
||||
var desc = (payload && typeof payload === 'object') ? String(payload.description || '') : String(raw || '');
|
||||
var teaser = (payload && typeof payload === 'object') ? String(payload.teaser || '') : '';
|
||||
var quoteDa = (payload && typeof payload === 'object') ? String(payload.quote_da || '') : '';
|
||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||
if (textarea) {
|
||||
textarea.value = desc;
|
||||
var teaserInput = editForm.querySelector('input[name="teaser"]');
|
||||
if (teaserInput && teaser) teaserInput.value = teaser;
|
||||
var quoteInput = editForm.querySelector('input[name="quote_da"]');
|
||||
if (quoteInput && quoteDa) quoteInput.value = quoteDa;
|
||||
showToast('Inserted generated description' + (teaser || quoteDa ? ' and extras' : '') + ' into form. Click Save changes to apply.', true);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch(e){}
|
||||
})();
|
||||
|
||||
})();
|
||||
</script>
|
||||
<script src="js/map_lightbox.js?v=20260531b" defer></script>
|
||||
3
_header.php
Normal file
3
_header.php
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
// Shared header for public pages (index.php, contact.php, etc.)
|
||||
?><header class="site-header admin-header"><div class="brand-wrap"><a class="header-logo" href="index.php" aria-label="MVLog home"><img src="assets/img/moto_travel.png" alt=""></a><a class="brand" href="index.php"><h1><?=h($siteHeaderTitle)?></h1><p>journal of an<br>unreliable narrator.</p></a></div><div class="header-actions"><button id="push-toggle" class="push-toggle" type="button" hidden aria-label="Notifications" title="Notifications"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22a2.7 2.7 0 0 0 2.6-2h-5.2A2.7 2.7 0 0 0 12 22Zm7-6V11a7 7 0 0 0-5-6.7V3a2 2 0 0 0-4 0v1.3A7 7 0 0 0 5 11v5l-2 2v1h18v-1l-2-2Z"/></svg></button><a class="rss-link" href="feed.php" aria-label="RSS feed" title="RSS feed"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.2 17.8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM2.2 8.7v3.1a10 10 0 0 1 10 10h3.1A13.1 13.1 0 0 0 2.2 8.7Zm0-5.5v3.1a15.5 15.5 0 0 1 15.5 15.5h3.1A18.6 18.6 0 0 0 2.2 3.2Z"/></svg></a><a class="contact-link" href="contact.php" aria-label="Contact" title="Contact"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg></a></div></header>
|
||||
89
contact.php
Normal file
89
contact.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
$config = require __DIR__ . "/config.php";
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
|
||||
|
||||
$msg = '';
|
||||
$err = '';
|
||||
$form_name = '';
|
||||
$form_email = '';
|
||||
$form_message = '';
|
||||
|
||||
// ##################################################################
|
||||
// ## IMPORTANT: Replace this with your actual email address. ##
|
||||
// ##################################################################
|
||||
$to_email = 'your-real-email@example.com';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$form_name = trim((string)($_POST['name'] ?? ''));
|
||||
$form_email = trim((string)($_POST['email'] ?? ''));
|
||||
$form_message = trim((string)($_POST['message'] ?? ''));
|
||||
$honeypot = trim((string)($_POST['website'] ?? ''));
|
||||
|
||||
if ($honeypot !== '') {
|
||||
// This was likely a bot, fail silently but look like a success.
|
||||
$msg = "Thank you for your message!";
|
||||
} elseif ($form_name === '' || $form_email === '' || $form_message === '') {
|
||||
$err = "Please fill out all required fields.";
|
||||
} elseif (!filter_var($form_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$err = "Please provide a valid email address.";
|
||||
} else {
|
||||
$subject = "MVLog Contact Form Submission from " . $form_name;
|
||||
$body = "Name: " . $form_name . "\n";
|
||||
$body .= "Email: " . $form_email . "\n\n";
|
||||
$body .= "Message:\n" . str_replace(["\r\n", "\r"], "\n", $form_message);
|
||||
|
||||
$headers = "From: " . $form_name . " <" . $form_email . ">\r\n";
|
||||
$headers .= "Reply-To: " . $form_email . "\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . phpversion();
|
||||
|
||||
if (mail($to_email, $subject, $body, $headers)) {
|
||||
$msg = "Thank you for your message! I will get back to you shortly.";
|
||||
$form_name = $form_email = $form_message = ''; // Clear form on success
|
||||
} else {
|
||||
$err = "Sorry, there was an error sending your message. Please try again later.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$headerTitles = [ 'Moto Vlog', 'Multiple Voices Log', 'Multi-Version Log', 'MultiVerse Log', 'Miles & Views Log', 'Moving Vistas Log', 'Motorcycle Voyage Log', 'Mapped Vagabond Log', 'Mountain Valley Log', 'Miles Versus Logic', 'Motorcycle Voyage Log', 'Mapped Vagabond Log', 'Multiple Viewpoints Log', 'Multiple Versions of Life', 'Motion, Velocity & Luck', 'Miles, Views & Life', 'Many Voices of Life', 'Many Views of Life', 'Modern Vagabond Log', 'Motorcycle Wanderlust Log', 'Mapped Wanderings Log', 'Miles Beyond Reason', 'Miles Beyond Reality', 'Motion, Velocity & Lunacy', 'Miles, Wind & Luck', 'Maps, Villages & Legends', 'Multi-Vision Log', 'Multi-Vista Log', 'Mutable View Log', 'Mistakes, Victories & Lessons', 'Mostly Verified Legends', 'Mostly Vague Logistics', 'Marginally Viable Leadership', 'Mileage Versus Luck', 'Mechanical Violence Log', 'Moderately Violent Leisure', 'Mistakes Validated Later', ];
|
||||
$siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>MVL - Contact</title>
|
||||
<meta property="og:title" content="MVL">
|
||||
<meta property="og:description" content="journal of an unreliable narrator.">
|
||||
<meta property="og:image" content="https://bubulescu.org/assets/img/moto_travel.png">
|
||||
<meta property="og:url" content="https://bubulescu.org">
|
||||
<link rel="icon" type="image/png" href="assets/img/moto_travel.png">
|
||||
<link rel="stylesheet" href="style.css?v=20260531k">
|
||||
</head>
|
||||
<body>
|
||||
<?php include __DIR__ . '/_header.php'; ?>
|
||||
<main>
|
||||
<section class="contact-form-section">
|
||||
<h2>Contact</h2>
|
||||
|
||||
<?php if($msg): ?><div class="ok"><?=h($msg)?></div><?php endif; ?>
|
||||
<?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||
|
||||
<form method="post" action="contact.php" id="contact-form">
|
||||
<label>Name*<input type="text" name="name" value="<?=h($form_name)?>" required></label>
|
||||
<label>Email*<input type="email" name="email" value="<?=h($form_email)?>" required></label>
|
||||
<label>Message*<textarea name="message" required><?=h($form_message)?></textarea></label>
|
||||
|
||||
<!-- Honeypot field for spam bots -->
|
||||
<div style="position: absolute; left: -5000px;" aria-hidden="true">
|
||||
<label>Don't fill this out if you're human: <input type="text" name="website" tabindex="-1" autocomplete="off"></label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Send Message</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<?php include __DIR__ . '/_footer.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
57
index.php
57
index.php
|
|
@ -249,14 +249,22 @@ $headerTitles = [
|
|||
'Mountain Valley Log',
|
||||
'Miles Versus Logic',
|
||||
'Motorcycle Voyage Log',
|
||||
'Mapped Vagabond Log',
|
||||
'Multiple Viewpoints Log',
|
||||
'Multiple Versions of Life',
|
||||
'Motion, Velocity & Luck',
|
||||
'Miles, Views & Life',
|
||||
'Many Voices of Life',
|
||||
'Many Views of Life',
|
||||
'Modern Vagabond Log',
|
||||
'Motorcycle Wanderlust Log',
|
||||
'Mapped Wanderings Log',
|
||||
'Miles Beyond Reason',
|
||||
'Miles Beyond Reality',
|
||||
'Motion, Velocity & Lunacy',
|
||||
'Miles, Wind & Luck',
|
||||
'Maps, Villages & Legends',
|
||||
'Multi-Vision Log',
|
||||
'Multi-Vista Log',
|
||||
'Mutable View Log',
|
||||
'Mistakes, Victories & Lessons',
|
||||
|
|
@ -276,20 +284,13 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>MVL</title>
|
||||
|
||||
<meta property="og:title" content="MVL — journal of an unreliable narrator.">
|
||||
<meta property="og:description" content="Motorcycle journeys, videos and stories..">
|
||||
<meta property="og:image" content="http://bubulescu.org/assets/img/moto_travel.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:title" content="MVL">
|
||||
<meta property="og:description" content="journal of an unreliable narrator.">
|
||||
<meta property="og:image" content="https://bubulescu.org/assets/img/moto_travel.png">
|
||||
<meta property="og:url" content="https://bubulescu.org">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="MVL">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" title="MVLog RSS" href="feed.php">
|
||||
<link rel="icon" type="image/png" href="assets/img/moto_travel.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="style.css?v=20260531k">
|
||||
<style>
|
||||
.filter-form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.55rem;align-items:center;margin:0 0 .4rem}
|
||||
.filter-input-wrap{position:relative;min-width:0}
|
||||
|
|
@ -325,7 +326,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header admin-header"><div class="brand-wrap"><a class="header-logo" href="index.php" aria-label="MVLog home"><img src="assets/img/moto_travel.png" alt=""></a><a class="brand" href="index.php"><h1><?=h($siteHeaderTitle)?></h1><p>journal of an<br>unreliable narrator.</p></a></div><button id="push-toggle" class="push-toggle" type="button" hidden aria-label="Notifications" title="Notifications"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22a2.7 2.7 0 0 0 2.6-2h-5.2A2.7 2.7 0 0 0 12 22Zm7-6V11a7 7 0 0 0-5-6.7V3a2 2 0 0 0-4 0v1.3A7 7 0 0 0 5 11v5l-2 2v1h18v-1l-2-2Z"/></svg></button><a class="rss-link" href="feed.php" aria-label="RSS feed" title="RSS feed"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.2 17.8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM2.2 8.7v3.1a10 10 0 0 1 10 10h3.1A13.1 13.1 0 0 0 2.2 8.7Zm0-5.5v3.1a15.5 15.5 0 0 1 15.5 15.5h3.1A18.6 18.6 0 0 0 2.2 3.2Z"/></svg></a></header>
|
||||
<?php include __DIR__ . '/_header.php'; ?>
|
||||
<main>
|
||||
<section id="videos">
|
||||
<form method="get" class="filter-form">
|
||||
|
|
@ -399,36 +400,6 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>Bubulescu.Org</footer>
|
||||
<script>
|
||||
(function(){
|
||||
const btn=document.getElementById('push-toggle');
|
||||
if(!btn || !('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return;
|
||||
function b64uToUint8Array(s){ const pad='='.repeat((4-s.length%4)%4); const b64=(s+pad).replace(/-/g,'+').replace(/_/g,'/'); const raw=atob(b64); return Uint8Array.from([...raw].map(c=>c.charCodeAt(0))); }
|
||||
async function state(reg){ const sub=await reg.pushManager.getSubscription(); btn.classList.toggle('active', !!sub); btn.title=sub?'Notifications on':'Notifications'; btn.hidden=false; return sub; }
|
||||
navigator.serviceWorker.register('sw.js').then(async reg=>{
|
||||
const cfg=await fetch('push_config.php',{cache:'no-store'}).then(r=>r.json()).catch(()=>({enabled:false}));
|
||||
if(!cfg.enabled || !cfg.publicKey) return;
|
||||
await state(reg);
|
||||
btn.addEventListener('click', async ()=>{
|
||||
btn.disabled=true;
|
||||
try{
|
||||
let sub=await reg.pushManager.getSubscription();
|
||||
if(sub){
|
||||
return;
|
||||
}else{
|
||||
const perm=await Notification.requestPermission();
|
||||
if(perm!=='granted') return;
|
||||
sub=await reg.pushManager.subscribe({userVisibleOnly:true,applicationServerKey:b64uToUint8Array(cfg.publicKey)});
|
||||
await fetch('push_subscribe.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(sub)});
|
||||
}
|
||||
await state(reg);
|
||||
}catch(e){ alert(e.message || 'Could not update notifications'); }
|
||||
finally{ await state(reg); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="js/map_lightbox.js?v=20260531b" defer></script>
|
||||
<?php include __DIR__ . '/_footer.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
190
new.php
190
new.php
|
|
@ -1040,8 +1040,8 @@ $editDir = null; $editStatus = ''; $editRunning = false; $editData = ['title'=>'
|
|||
if ($editName !== '' || $editId !== '') { try { $editDir = resolve_input_dir_from_request($config['uploads_dir'], $editId, $editName, $articleIndex); $editStatus = input_dir_status($editDir); $editRunning = input_dir_running($editDir); if (!$editRunning) { $editData = read_data($editDir); $editFiles = media_sort_files($editDir, list_files($editDir)); } } catch (Throwable $e) { $err = $e->getMessage(); } }
|
||||
$runningJobs = active_worker_jobs($config);
|
||||
?>
|
||||
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>MVL Admin</title><link rel="icon" type="image/png" href="assets/img/moto_travel.png"><link rel="stylesheet" href="style.css"><style>.admin-search-form{display:flex;flex-wrap:nowrap;align-items:center;gap:.55rem;margin:0 0 .6rem;overflow-x:auto;padding-bottom:2px}.admin-search-wrap{position:relative;min-width:180px;flex:1 1 auto}.admin-search-wrap input{margin:0;padding-right:2.15rem}.admin-search-clear{position:absolute;right:.45rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:#C46A3A;color:#111315;text-decoration:none;font-size:1rem;line-height:1;font-weight:700;box-shadow:0 1px 4px #0007;transition:opacity .15s ease,transform .15s ease}.admin-search-clear:hover{background:#d97b48;color:#111315;transform:translateY(-50%) scale(1.05)}.admin-search-clear.is-empty{opacity:.38;pointer-events:none}.admin-search-submit{display:inline-grid;place-items:center;width:2.35rem;height:2.35rem;padding:0;border-radius:.5rem;background:#C46A3A;color:#111315;flex:0 0 auto}.admin-search-submit .icon{font-size:1.02rem;line-height:1;transform:translateY(.01em)}.admin-search-filters{display:flex;flex-wrap:nowrap;gap:.7rem;align-items:center;flex:0 0 auto;white-space:nowrap}.admin-filter-item{display:inline-flex;align-items:center;gap:.35rem;margin:0;white-space:nowrap;font-size:.92rem}.admin-filter-item input[type=checkbox]{width:.9rem;height:.9rem;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.description-teaser{margin:.2rem 0 .45rem;color:#eadfca}.description-quote{margin:.2rem 0 .45rem;font-style:italic;font-size:.82rem;color:#d4d7dd}</style></head><body>
|
||||
<header class="site-header admin-header"><div class="brand-wrap"><a class="header-logo" href="index.php" aria-label="MVLog home"><img src="assets/img/moto_travel.png" alt=""></a><a class="brand" href="index.php"><h1 style="font-size:1.15rem">MVLog <span class="admin-word">Admin</span></h1><p>journal of an<br>unreliable narrator.</p></a></div></header>
|
||||
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>MVL Admin</title><link rel="icon" type="image/png" href="assets/img/moto_travel.png"><link rel="stylesheet" href="style.css?v=20260531k"><style>.admin-search-form{display:flex;flex-wrap:nowrap;align-items:center;gap:.55rem;margin:0 0 .6rem;overflow-x:auto;padding-bottom:2px}.admin-search-wrap{position:relative;min-width:180px;flex:1 1 auto}.admin-search-wrap input{margin:0;padding-right:2.15rem}.admin-search-clear{position:absolute;right:.45rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:#C46A3A;color:#111315;text-decoration:none;font-size:1rem;line-height:1;font-weight:700;box-shadow:0 1px 4px #0007;transition:opacity .15s ease,transform .15s ease}.admin-search-clear:hover{background:#d97b48;color:#111315;transform:translateY(-50%) scale(1.05)}.admin-search-clear.is-empty{opacity:.38;pointer-events:none}.admin-search-submit{display:inline-grid;place-items:center;width:2.35rem;height:2.35rem;padding:0;border-radius:.5rem;background:#C46A3A;color:#111315;flex:0 0 auto}.admin-search-submit .icon{font-size:1.02rem;line-height:1;transform:translateY(.01em)}.admin-search-filters{display:flex;flex-wrap:nowrap;gap:.7rem;align-items:center;flex:0 0 auto;white-space:nowrap}.admin-filter-item{display:inline-flex;align-items:center;gap:.35rem;margin:0;white-space:nowrap;font-size:.92rem}.admin-filter-item input[type=checkbox]{width:.9rem;height:.9rem;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.description-teaser{margin:.2rem 0 .45rem;color:#eadfca}.description-quote{margin:.2rem 0 .45rem;font-style:italic;font-size:.82rem;color:#d4d7dd}</style></head><body>
|
||||
<header class="site-header admin-header"><div class="brand-wrap"><a class="header-logo" href="index.php" aria-label="MVLog home"><img src="assets/img/moto_travel.png" alt=""></a><a class="brand" href="index.php"><h1 style="font-size:1.15rem">MVLog <span class="admin-word">Admin</span></h1><p>journal of an<br>unreliable narrator.</p></a></div><div class="header-actions"><a class="contact-link" href="contact.php" aria-label="Contact" title="Contact"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg></a></div></header>
|
||||
<main>
|
||||
<?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||
|
||||
|
|
@ -1553,187 +1553,5 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
});
|
||||
</script></main>
|
||||
<footer>Input dirs are saved under <code>in-dir/</code>.</footer>
|
||||
<footer class="site-footer admin-footer"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div id="job-status" class="job-status"<?= empty($runningJobs) ? ' hidden' : '' ?>><?php foreach($runningJobs as $job): ?><span class="job-dot"></span><span><?=h(str_replace('_', ' ', $job['name']))?></span><?php if(!empty($job['started_at'])): ?><span class="job-age">(<?=h(format_job_age($job['started_at']))?>)</span><?php endif; ?><?php endforeach; ?></div><div class="admin-right"><a class="logout-link" href="logout.php">Log off</a></div></footer>
|
||||
<script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script>
|
||||
<script>
|
||||
(function(){
|
||||
function attachDescribeButtons(){
|
||||
document.querySelectorAll('.admin-video-row').forEach(function(row){
|
||||
if (row.querySelector('.describe-button')) return;
|
||||
var actions = row.querySelector('.admin-actions');
|
||||
if (!actions) return;
|
||||
var job = row.dataset.job || '';
|
||||
var articleId = row.dataset.id || '';
|
||||
if (!job && !articleId) return;
|
||||
var describe = document.createElement('a');
|
||||
describe.className = 'button describe-button';
|
||||
describe.href = '#';
|
||||
describe.dataset.job = job;
|
||||
describe.dataset.id = articleId;
|
||||
describe.textContent = 'Describe';
|
||||
var first = actions.querySelector('.button');
|
||||
if (first) actions.insertBefore(describe, first);
|
||||
else actions.appendChild(describe);
|
||||
});
|
||||
}
|
||||
|
||||
function showModal(job, articleId, description, teaser, quoteDa, rawJson){
|
||||
var existing = document.getElementById('mvlog-gemini-modal');
|
||||
if (existing) existing.remove();
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'mvlog-gemini-modal';
|
||||
overlay.style.cssText = 'position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = 'background:white;color:#111;padding:20px;max-width:900px;max-height:80vh;overflow:auto;border-radius:6px;font-family:inherit;';
|
||||
var h = document.createElement('h3');
|
||||
h.textContent = 'Generated description';
|
||||
var quoteBox = null;
|
||||
if (quoteDa) {
|
||||
quoteBox = document.createElement('div');
|
||||
quoteBox.style.cssText = 'white-space:pre-wrap;font-style:italic;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:10px;background:#f4f0ff;border-radius:4px;';
|
||||
quoteBox.textContent = '“' + quoteDa + '”';
|
||||
}
|
||||
var pre = document.createElement('div');
|
||||
pre.style.cssText = 'white-space:pre-wrap;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:12px;background:#f8f8f8;border-radius:4px;';
|
||||
pre.textContent = description || '';
|
||||
var teaserBox = null;
|
||||
if (teaser) {
|
||||
teaserBox = document.createElement('div');
|
||||
teaserBox.style.cssText = 'white-space:pre-wrap;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:10px;background:#fff7e8;border-radius:4px;';
|
||||
teaserBox.textContent = teaser;
|
||||
}
|
||||
var btnBar = document.createElement('div');
|
||||
btnBar.style.cssText = 'margin-top:12px;text-align:right;';
|
||||
var useBtn = document.createElement('button');
|
||||
useBtn.className = 'button';
|
||||
useBtn.textContent = 'Use in form';
|
||||
var cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'button';
|
||||
cancelBtn.textContent = 'Close';
|
||||
cancelBtn.style.marginLeft = '8px';
|
||||
btnBar.appendChild(useBtn);
|
||||
btnBar.appendChild(cancelBtn);
|
||||
box.appendChild(h);
|
||||
if (teaserBox) box.appendChild(teaserBox);
|
||||
if (quoteBox) box.appendChild(quoteBox);
|
||||
box.appendChild(pre);
|
||||
box.appendChild(btnBar);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
cancelBtn.addEventListener('click', function(){ overlay.remove(); });
|
||||
useBtn.addEventListener('click', function(){
|
||||
var editForm = document.getElementById('edit-form');
|
||||
if (editForm){
|
||||
var idInput = editForm.querySelector('input[name="id"]');
|
||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||
var sameById = !!(articleId && idInput && idInput.value === articleId);
|
||||
var sameByDir = !!(job && dirInput && dirInput.value === job);
|
||||
if (sameById || sameByDir){
|
||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||
if (textarea){
|
||||
textarea.value = description || '';
|
||||
var teaserInput = editForm.querySelector('input[name="teaser"]');
|
||||
if (teaserInput && teaser) teaserInput.value = teaser;
|
||||
var quoteInput = editForm.querySelector('input[name="quote_da"]');
|
||||
if (quoteInput && quoteDa) quoteInput.value = quoteDa;
|
||||
showToast('Inserted generated description' + (teaser || quoteDa ? ' and extras' : '') + ' into form. Click Save changes to apply.', true);
|
||||
overlay.remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
var storageKey = 'mvlog_gemini_description_' + (articleId || job);
|
||||
try { localStorage.setItem(storageKey, JSON.stringify({description: description || '', teaser: teaser || '', quote_da: quoteDa || ''})); } catch(e){}
|
||||
var href = 'new.php?tab=edit';
|
||||
if (articleId) href += '&id=' + encodeURIComponent(articleId);
|
||||
if (job) href += '&edit=' + encodeURIComponent(job);
|
||||
window.location.href = href;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e){
|
||||
var el = (e.target && e.target.closest && e.target.closest('.describe-button')) || (e.target && e.target.classList && e.target.classList.contains && e.target.classList.contains('describe-button') ? e.target : null);
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
var job = el.dataset.job || '';
|
||||
var articleId = el.dataset.id || '';
|
||||
if (!job && !articleId) return;
|
||||
el.classList.add('disabled');
|
||||
var oldText = el.textContent;
|
||||
el.textContent = 'Generating...';
|
||||
fetch('lib/generate_data_sync.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body: 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16'
|
||||
}).then(function(r){ return r.json().catch(()=>({})); })
|
||||
.then(function(data){
|
||||
if (data && data.status === 'ok'){
|
||||
var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || '';
|
||||
var teaser = data.teaser || '';
|
||||
var rawFirst = (Array.isArray(data.raw_json) && data.raw_json.length) ? data.raw_json[0] : data.raw_json;
|
||||
var quoteDa = data.quote_da || (rawFirst && rawFirst.quote_da) || '';
|
||||
showModal(job, articleId, desc, teaser, quoteDa, data.raw_json || null);
|
||||
} else {
|
||||
var msg = (data && data.message) ? data.message : 'No response';
|
||||
if (data && data.log) {
|
||||
try {
|
||||
var log = String(data.log);
|
||||
var lines = log.split('\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });
|
||||
var keywords = ['quota','rate limit','rate-limit','429','503','overloaded','high demand','busy','unavailable','error','failed','retry'];
|
||||
var found = lines.find(function(l){ var ll=l.toLowerCase(); return keywords.some(function(k){ return ll.indexOf(k) !== -1; }); });
|
||||
if (found) msg += ' — ' + found;
|
||||
else if (lines.length) msg += ' — ' + lines[0].slice(0,200);
|
||||
} catch(e){}
|
||||
}
|
||||
showToast('Error: ' + msg, false, (data && data.log) ? (typeof data.log === 'string' ? data.log : JSON.stringify(data.log)) : null);
|
||||
}
|
||||
}).catch(function(err){
|
||||
showToast('Request failed: ' + err, false);
|
||||
}).finally(function(){
|
||||
el.classList.remove('disabled');
|
||||
el.textContent = oldText;
|
||||
});
|
||||
});
|
||||
|
||||
attachDescribeButtons();
|
||||
var list = document.querySelector('.admin-list.video-list');
|
||||
if (list) new MutationObserver(function(){ attachDescribeButtons(); }).observe(list, {childList:true, subtree:true});
|
||||
|
||||
// On edit page load: if a generated description is in localStorage, insert it into the form.
|
||||
(function(){
|
||||
var editForm = document.getElementById('edit-form');
|
||||
if (!editForm) return;
|
||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||
var idInput = editForm.querySelector('input[name="id"]');
|
||||
if (!dirInput && !idInput) return;
|
||||
var job = dirInput ? dirInput.value : '';
|
||||
var articleId = idInput ? idInput.value : '';
|
||||
try {
|
||||
var key = 'mvlog_gemini_description_' + (articleId || job);
|
||||
var raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
var payload = null;
|
||||
try { payload = JSON.parse(raw); } catch(e) { payload = {description: raw, teaser: '', quote_da: ''}; }
|
||||
var desc = (payload && typeof payload === 'object') ? String(payload.description || '') : String(raw || '');
|
||||
var teaser = (payload && typeof payload === 'object') ? String(payload.teaser || '') : '';
|
||||
var quoteDa = (payload && typeof payload === 'object') ? String(payload.quote_da || '') : '';
|
||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||
if (textarea) {
|
||||
textarea.value = desc;
|
||||
var teaserInput = editForm.querySelector('input[name="teaser"]');
|
||||
if (teaserInput && teaser) teaserInput.value = teaser;
|
||||
var quoteInput = editForm.querySelector('input[name="quote_da"]');
|
||||
if (quoteInput && quoteDa) quoteInput.value = quoteDa;
|
||||
showToast('Inserted generated description' + (teaser || quoteDa ? ' and extras' : '') + ' into form. Click Save changes to apply.', true);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch(e){}
|
||||
})();
|
||||
|
||||
})();
|
||||
</script>
|
||||
<script src="js/map_lightbox.js?v=20260531b" defer></script>
|
||||
</body></html>
|
||||
<?php include __DIR__ . '/_footer_admin.php'; ?>
|
||||
</body></html>
|
||||
|
|
|
|||
82
style.css
82
style.css
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue