usability enhancements
This commit is contained in:
@@ -20,10 +20,11 @@
|
||||
<link rel="canonical" href="/" id="canonical-link">
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com">
|
||||
<title>ClawFort — AI News</title>
|
||||
<title>ClawFort AI News</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Noto+Sans+Tamil:wght@400;500;600;700&family=Noto+Sans+Malayalam:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
@@ -36,7 +37,9 @@
|
||||
700: '#4263eb', 900: '#1e2a5e', 950: '#0f172a'
|
||||
}
|
||||
},
|
||||
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'Noto Sans Tamil', 'Noto Sans Malayalam', 'system-ui', 'sans-serif']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +211,45 @@
|
||||
border-color: #5c7cfa;
|
||||
box-shadow: 0 0 0 1px #5c7cfa inset;
|
||||
}
|
||||
html[data-lang='ta'] .news-card-summary,
|
||||
html[data-lang='ml'] .news-card-summary,
|
||||
html[data-lang='ta'] .hero-summary,
|
||||
html[data-lang='ml'] .hero-summary,
|
||||
html[data-lang='ta'] .modal-body-text,
|
||||
html[data-lang='ml'] .modal-body-text {
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.78;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.share-icon-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
background: rgba(92, 124, 250, 0.14);
|
||||
color: #dbeafe;
|
||||
transition: background 180ms ease;
|
||||
}
|
||||
.share-icon-btn:hover { background: rgba(92, 124, 250, 0.25); }
|
||||
.footer-link { text-decoration: underline; text-underline-offset: 2px; }
|
||||
.footer-link:hover { color: #dbeafe; }
|
||||
.contact-hint {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
max-width: 280px;
|
||||
background: rgba(15, 23, 42, 0.96);
|
||||
color: #e2e8f0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 8px 22px rgba(2, 6, 23, 0.45);
|
||||
}
|
||||
*:focus-visible {
|
||||
outline: 2px solid #5c7cfa;
|
||||
outline-offset: 2px;
|
||||
@@ -291,7 +333,7 @@
|
||||
<p class="text-base sm:text-lg max-w-3xl line-clamp-3 mb-4 hero-summary" x-text="item.summary"></p>
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm hero-meta">
|
||||
<button class="px-3 py-1.5 rounded-md bg-cf-500/20 text-cf-300 hover:bg-cf-500/30 transition-colors"
|
||||
@click="trackEvent('hero-cta-click', { article_id: item.id }); window.dispatchEvent(new CustomEvent('open-summary', { detail: item }))">
|
||||
@click="trackEvent('hero-cta-click', { article_id: item.id, article_title: item.headline }); window.dispatchEvent(new CustomEvent('open-summary', { detail: item }))">
|
||||
Read TL;DR
|
||||
</button>
|
||||
<a :href="item.source_url" target="_blank" rel="noopener"
|
||||
@@ -300,6 +342,9 @@
|
||||
x-show="item.source_url">
|
||||
Via: <span x-text="extractDomain(item.source_url)" class="underline underline-offset-2"></span>
|
||||
</a>
|
||||
<a :href="articlePermalink(item)" class="hover:text-cf-300 transition-colors underline underline-offset-2">
|
||||
Permalink
|
||||
</a>
|
||||
<span x-show="item.image_credit" x-text="'Image: ' + item.image_credit"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,9 +409,12 @@
|
||||
@click.stop="trackEvent('source-link-click')"
|
||||
x-show="item.source_url"
|
||||
x-text="extractDomain(item.source_url)"></a>
|
||||
<span x-text="timeAgo(item.published_at)"></span>
|
||||
<div class="flex items-center gap-2">
|
||||
<a :href="articlePermalink(item)" class="hover:text-cf-300 underline underline-offset-2">Link</a>
|
||||
<span x-text="timeAgo(item.published_at)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="openSummary(item)"
|
||||
<button @click="trackEvent('feed-cta-click', { article_id: item.id, article_title: item.headline }); openSummary(item)"
|
||||
class="w-full text-center text-xs font-semibold rounded-md px-3 py-2 bg-cf-500/15 hover:bg-cf-500/25 transition-colors news-card-btn">
|
||||
Read TL;DR
|
||||
</button>
|
||||
@@ -423,6 +471,21 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm uppercase tracking-wide font-semibold mb-2 modal-section-title">Share</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<a :href="shareLink('x', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on X" title="Share on X">
|
||||
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M18.9 2H22l-6.8 7.8L23 22h-6.2l-4.9-6.4L6.2 22H3l7.3-8.4L1 2h6.3l4.4 5.8L18.9 2z"/></svg>
|
||||
</a>
|
||||
<a :href="shareLink('whatsapp', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on WhatsApp" title="Share on WhatsApp">
|
||||
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M20.5 3.5A11.8 11.8 0 0 0 12.1 0C5.5 0 .1 5.3.1 11.9c0 2.1.5 4.1 1.6 5.9L0 24l6.4-1.7a12 12 0 0 0 5.7 1.4h.1c6.6 0 11.9-5.3 11.9-11.9 0-3.2-1.3-6.2-3.6-8.3zM12.2 21.7h-.1a9.9 9.9 0 0 1-5-1.4l-.4-.2-3.8 1 1-3.7-.2-.4a9.8 9.8 0 0 1-1.5-5.2c0-5.4 4.4-9.8 9.9-9.8 2.6 0 5.1 1 6.9 2.9a9.7 9.7 0 0 1 2.8 6.9c0 5.4-4.4 9.9-9.8 9.9zm5.4-7.3c-.3-.1-1.8-.9-2.1-1-.3-.1-.5-.1-.7.2-.2.3-.8 1-1 1.2-.2.2-.3.2-.6.1-1.6-.8-2.6-1.5-3.7-3.3-.3-.5.3-.5 1-1.8.1-.2.1-.4 0-.6l-1-2.4c-.2-.6-.5-.5-.7-.5h-.6c-.2 0-.6.1-.9.4-.3.3-1.2 1.1-1.2 2.8 0 1.6 1.2 3.2 1.4 3.4.2.2 2.3 3.6 5.6 5 .8.3 1.4.5 1.9.7.8.3 1.5.2 2 .1.6-.1 1.8-.8 2-1.5.2-.7.2-1.4.1-1.5-.1-.1-.3-.2-.6-.3z"/></svg>
|
||||
</a>
|
||||
<a :href="shareLink('linkedin', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on LinkedIn" title="Share on LinkedIn">
|
||||
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M4.98 3.5a2.5 2.5 0 1 1 0 5.001A2.5 2.5 0 0 1 4.98 3.5zM3 9h4v12H3zM10 9h3.8v1.7h.1c.5-1 1.8-2.1 3.7-2.1 4 0 4.7 2.6 4.7 6V21h-4v-5.4c0-1.3 0-2.9-1.8-2.9s-2.1 1.4-2.1 2.8V21h-4V9z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs modal-powered">Powered by Perplexity</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,15 +508,20 @@
|
||||
<div class="fixed left-0 right-0 pointer-events-none" style="top:75%" x-data x-intersect:enter="trackDepth(75)"></div>
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-white/5 py-8 text-center text-sm text-gray-500">
|
||||
<footer x-data="footerEnhancements()" x-init="init()" class="border-t border-white/5 py-8 text-center text-sm text-gray-500">
|
||||
<div class="max-w-7xl mx-auto px-4 space-y-2">
|
||||
<p>Powered by <a href="https://www.perplexity.ai" target="_blank" rel="noopener" class="text-cf-400 hover:text-cf-300 transition-colors">Perplexity</a></p>
|
||||
<p class="space-x-3">
|
||||
<a href="/terms" class="underline hover:text-gray-300">Terms of Use</a>
|
||||
<a href="/attribution" class="underline hover:text-gray-300">Attribution</a>
|
||||
<a href="/terms" class="footer-link">Terms of Use</a>
|
||||
<a href="/attribution" class="footer-link">Attribution</a>
|
||||
<button type="button" class="footer-link" @click="scrollTop()">Back to Top</button>
|
||||
<a x-show="githubUrl" :href="githubUrl" target="_blank" rel="noopener" class="footer-link">GitHub</a>
|
||||
<a x-show="contactEmail" :href="'mailto:' + contactEmail" class="footer-link"
|
||||
@mouseenter="showHint($event)" @mousemove="moveHint($event)" @mouseleave="hideHint()">Email me</a>
|
||||
</p>
|
||||
<p>© <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p>
|
||||
</div>
|
||||
<div x-show="hintVisible" x-cloak class="contact-hint" :style="`left:${hintX}px; top:${hintY}px`" x-text="hintText"></div>
|
||||
</footer>
|
||||
|
||||
<div id="cookie-consent-banner" class="hidden fixed bottom-4 left-1/2 -translate-x-1/2 w-[95%] max-w-3xl z-50 rounded-lg border border-white/15 bg-slate-900/95 backdrop-blur p-4 transition-opacity duration-700 opacity-100">
|
||||
@@ -506,7 +574,47 @@ function toAbsoluteUrl(url) {
|
||||
|
||||
function articlePermalink(item) {
|
||||
if (!item?.id) return toAbsoluteUrl('/');
|
||||
return toAbsoluteUrl(`/#news-${item.id}`);
|
||||
return toAbsoluteUrl(`/?article=${item.id}`);
|
||||
}
|
||||
|
||||
function getPermalinkArticleId() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const article = params.get('article');
|
||||
if (article && /^\d+$/.test(article)) return Number(article);
|
||||
const match = (window.location.hash || '').match(/^#news-(\d+)$/);
|
||||
if (match) return Number(match[1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function setPermalinkArticleId(articleId) {
|
||||
if (!articleId) return;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('article', String(articleId));
|
||||
url.hash = '';
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
function clearPermalinkArticleId() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('article');
|
||||
if ((url.hash || '').startsWith('#news-')) url.hash = '';
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
function shareLink(provider, item) {
|
||||
const permalink = articlePermalink(item);
|
||||
const encodedUrl = encodeURIComponent(permalink);
|
||||
const encodedTitle = encodeURIComponent(item?.headline || 'ClawFort AI News');
|
||||
if (provider === 'x') {
|
||||
return `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
|
||||
}
|
||||
if (provider === 'whatsapp') {
|
||||
return `https://wa.me/?text=${encodedTitle}%20${encodedUrl}`;
|
||||
}
|
||||
if (provider === 'linkedin') {
|
||||
return `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
|
||||
}
|
||||
return permalink;
|
||||
}
|
||||
|
||||
function setMetaContent(selector, content, attribute = 'content') {
|
||||
@@ -519,13 +627,13 @@ function syncSeoMeta(item) {
|
||||
const canonical = toAbsoluteUrl(window.location.pathname || '/');
|
||||
setMetaContent('#canonical-link', canonical, 'href');
|
||||
setMetaContent('#meta-og-url', canonical);
|
||||
document.title = 'ClawFort AI News';
|
||||
|
||||
if (!item) return;
|
||||
const title = `${item.headline} | ClawFort`;
|
||||
const title = 'ClawFort AI News';
|
||||
const description = item.summary || 'Latest AI news updates from ClawFort.';
|
||||
const image = toAbsoluteUrl(preferredImage(item));
|
||||
|
||||
document.title = title;
|
||||
setMetaContent('meta[name="description"]', description);
|
||||
setMetaContent('meta[property="og:title"]', title);
|
||||
setMetaContent('meta[property="og:description"]', description);
|
||||
@@ -741,6 +849,7 @@ function getPreferredLanguage() {
|
||||
function setPreferredLanguage(language) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
window._selectedLanguage = normalized;
|
||||
document.documentElement.setAttribute('data-lang', normalized);
|
||||
safeSetStorage('clawfort_language', normalized);
|
||||
setCookie('clawfort_language', normalized);
|
||||
const select = document.getElementById('language-select');
|
||||
@@ -756,10 +865,103 @@ window._selectedLanguage = getPreferredLanguage();
|
||||
window._themeChoice = getPreferredTheme();
|
||||
window.__heroNewsItem = null;
|
||||
window.__feedNewsItems = [];
|
||||
document.documentElement.setAttribute('data-lang', window._selectedLanguage);
|
||||
applyTheme(window._themeChoice);
|
||||
syncSeoMeta(null);
|
||||
syncStructuredData();
|
||||
|
||||
const CONTACT_HINTS = [
|
||||
'If you have feedback, I would love to hear it.',
|
||||
'Found a bug? Send a note and I will take a look.',
|
||||
'Have an idea for ClawFort? Reach out anytime.',
|
||||
'Suggestions are always welcome here.',
|
||||
'Want a feature? Let me know by email.',
|
||||
'Your feedback helps improve this project.',
|
||||
'Questions, suggestions, or praise - all welcome.',
|
||||
'Noticed something odd? Please report it.',
|
||||
'Tell me what would make this site better.',
|
||||
'Happy to hear your thoughts on UX and content.',
|
||||
'If something feels off, I am listening.',
|
||||
'Feature requests are open - send yours.',
|
||||
'If you care about this project, drop a line.',
|
||||
'Spotted a typo? I appreciate quick heads-up notes.',
|
||||
'Your perspective can shape the next update.',
|
||||
'If this helped you, tell me what to improve next.',
|
||||
'Ideas are fuel. Share one when you can.',
|
||||
'Small feedback can create big improvements.',
|
||||
'If a page confused you, let me know why.',
|
||||
'Product feedback is always in scope.',
|
||||
'Have performance concerns? I want to hear them.',
|
||||
'Your suggestions are part of the roadmap.',
|
||||
'See something broken? I can fix it faster with details.',
|
||||
'If a feature is missing, say the word.',
|
||||
'Tell me how ClawFort could serve you better.',
|
||||
'Think the UI can be clearer? Share your take.',
|
||||
'Want better readability options? Send feedback.',
|
||||
'If translation quality feels off, please report it.',
|
||||
'Any accessibility concern is worth sharing.',
|
||||
'If a link fails, I would appreciate the report.',
|
||||
'Help make this news feed sharper with feedback.',
|
||||
'Your comments help prioritize updates.',
|
||||
'Share your favorite improvement idea.',
|
||||
'If you have a workflow pain point, mention it.',
|
||||
'Even one sentence of feedback helps.',
|
||||
'Tell me which feature you use most.',
|
||||
'If sharing feels clunky, let me know.',
|
||||
'Want custom views? I am open to suggestions.',
|
||||
'If something is hard to discover, please say so.',
|
||||
'I appreciate concrete suggestions and examples.',
|
||||
'If the mobile layout needs work, send details.',
|
||||
'A quick email can spark the next fix.',
|
||||
'Thanks for helping improve this project.',
|
||||
'If this tool saves you time, tell me what else you need.',
|
||||
'I welcome constructive feedback.',
|
||||
'Have a better idea? I am curious.',
|
||||
'If you found a regression, please report it.',
|
||||
'Your use case matters - share it.',
|
||||
'If something should be simpler, I want to know.',
|
||||
'Feedback is always appreciated. Thank you.',
|
||||
];
|
||||
|
||||
function footerEnhancements() {
|
||||
return {
|
||||
githubUrl: '',
|
||||
contactEmail: '',
|
||||
hintVisible: false,
|
||||
hintText: '',
|
||||
hintX: 16,
|
||||
hintY: 16,
|
||||
async init() {
|
||||
try {
|
||||
const resp = await fetch('/config');
|
||||
if (!resp.ok) return;
|
||||
const cfg = await resp.json();
|
||||
this.githubUrl = cfg.github_repo_url || '';
|
||||
this.contactEmail = cfg.contact_email || '';
|
||||
} catch {}
|
||||
},
|
||||
scrollTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
randomHint() {
|
||||
return CONTACT_HINTS[Math.floor(Math.random() * CONTACT_HINTS.length)];
|
||||
},
|
||||
showHint(event) {
|
||||
this.hintText = this.randomHint();
|
||||
this.hintVisible = true;
|
||||
this.moveHint(event);
|
||||
},
|
||||
moveHint(event) {
|
||||
if (!event) return;
|
||||
this.hintX = Math.min(event.clientX + 16, window.innerWidth - 290);
|
||||
this.hintY = Math.min(event.clientY + 18, window.innerHeight - 80);
|
||||
},
|
||||
hideHint() {
|
||||
this.hintVisible = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
const choice = getPreferredTheme();
|
||||
if (choice === 'system') {
|
||||
@@ -834,6 +1036,7 @@ function newsFeed() {
|
||||
async init() {
|
||||
await this.waitForHero();
|
||||
await this.loadMore();
|
||||
await this.openFromPermalink();
|
||||
this.initialLoading = false;
|
||||
this.setupObserver();
|
||||
window.addEventListener('language-changed', async () => {
|
||||
@@ -845,6 +1048,7 @@ function newsFeed() {
|
||||
window.__feedNewsItems = [];
|
||||
await this.waitForHero();
|
||||
await this.loadMore();
|
||||
await this.openFromPermalink();
|
||||
this.initialLoading = false;
|
||||
});
|
||||
|
||||
@@ -919,23 +1123,52 @@ function newsFeed() {
|
||||
this.observer.observe(this.$refs.sentinel);
|
||||
},
|
||||
|
||||
async openFromPermalink() {
|
||||
const articleId = getPermalinkArticleId();
|
||||
if (!articleId) return;
|
||||
|
||||
if (window.__heroNewsItem?.id === articleId) {
|
||||
this.openSummary(window.__heroNewsItem);
|
||||
return;
|
||||
}
|
||||
|
||||
let item = this.items.find(i => i.id === articleId);
|
||||
let attempts = 0;
|
||||
while (!item && this.hasMore && attempts < 5) {
|
||||
await this.loadMore();
|
||||
item = this.items.find(i => i.id === articleId);
|
||||
attempts += 1;
|
||||
}
|
||||
|
||||
if (item) this.openSummary(item);
|
||||
},
|
||||
|
||||
openSummary(item) {
|
||||
this.modalItem = item;
|
||||
this.modalOpen = true;
|
||||
this.modalImageLoading = true;
|
||||
this.modalTldrLoading = true;
|
||||
setPermalinkArticleId(item?.id);
|
||||
setTimeout(() => {
|
||||
if (this.modalOpen) this.modalTldrLoading = false;
|
||||
}, 250);
|
||||
trackEvent('summary-modal-open', { article_id: item.id });
|
||||
trackEvent('summary-modal-open', {
|
||||
article_id: item.id,
|
||||
article_title: item.headline || null,
|
||||
});
|
||||
},
|
||||
|
||||
closeSummary() {
|
||||
const id = this.modalItem ? this.modalItem.id : null;
|
||||
const title = this.modalItem ? this.modalItem.headline : null;
|
||||
this.modalOpen = false;
|
||||
this.modalItem = null;
|
||||
this.modalTldrLoading = true;
|
||||
trackEvent('summary-modal-close', { article_id: id });
|
||||
clearPermalinkArticleId();
|
||||
trackEvent('summary-modal-close', {
|
||||
article_id: id,
|
||||
article_title: title,
|
||||
});
|
||||
},
|
||||
|
||||
trackSummarySource(item) {
|
||||
|
||||
Reference in New Issue
Block a user