aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 20:39:19 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 20:39:19 -0800
commit408f88e4d26048396a01bf82e36eb133db3feb24 (patch)
treedd4f06756c775a2955025baf7b370784e34a0af4
parent60cc6f00f681f09c2dbf8aa61b20570370e67efb (diff)
downloadneko-408f88e4d26048396a01bf82e36eb133db3feb24.tar.gz
neko-408f88e4d26048396a01bf82e36eb133db3feb24.tar.bz2
neko-408f88e4d26048396a01bf82e36eb133db3feb24.zip
feat: add vanilla JS frontend prototype (NK-2xsgef)
-rw-r--r--vanilla/app.js95
-rw-r--r--vanilla/index.html34
-rw-r--r--vanilla/style.css123
-rw-r--r--web/web.go11
4 files changed, 263 insertions, 0 deletions
diff --git a/vanilla/app.js b/vanilla/app.js
new file mode 100644
index 0000000..d1b2337
--- /dev/null
+++ b/vanilla/app.js
@@ -0,0 +1,95 @@
+document.addEventListener('DOMContentLoaded', () => {
+ fetchFeeds();
+ fetchItems(); // Default to fetching recent items
+});
+
+async function fetchFeeds() {
+ try {
+ const response = await fetch('/api/feed/');
+ if (!response.ok) throw new Error('Failed to fetch feeds');
+ const feeds = await response.json();
+ renderFeeds(feeds);
+ } catch (err) {
+ console.error(err);
+ document.getElementById('feeds-nav').innerHTML = '<div class="error">Error loading feeds</div>';
+ }
+}
+
+async function fetchItems(feedId = null) {
+ const listEl = document.getElementById('entries-list');
+ listEl.innerHTML = '<div class="loading">Loading items...</div>';
+
+ let url = '/api/stream/';
+ if (feedId) {
+ url += `?feed_id=${feedId}`;
+ }
+
+ try {
+ const response = await fetch(url);
+ if (!response.ok) throw new Error('Failed to fetch items');
+ const items = await response.json();
+ renderItems(items);
+ } catch (err) {
+ console.error(err);
+ listEl.innerHTML = '<div class="error">Error loading items</div>';
+ }
+}
+
+function renderFeeds(feeds) {
+ const nav = document.getElementById('feeds-nav');
+ nav.innerHTML = '';
+
+ const allLink = document.createElement('div');
+ allLink.className = 'feed-item';
+ allLink.textContent = 'All Items';
+ allLink.onclick = () => {
+ document.getElementById('feed-title').textContent = 'All Items';
+ fetchItems();
+ };
+ nav.appendChild(allLink);
+
+ feeds.forEach(feed => {
+ const div = document.createElement('div');
+ div.className = 'feed-item';
+ div.textContent = feed.title || feed.url;
+ div.title = feed.url;
+ div.onclick = () => {
+ document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+ div.classList.add('active');
+ document.getElementById('feed-title').textContent = feed.title;
+ fetchItems(feed.id);
+ };
+ nav.appendChild(div);
+ });
+}
+
+function renderItems(items) {
+ const list = document.getElementById('entries-list');
+ list.innerHTML = '';
+
+ if (items.length === 0) {
+ list.innerHTML = '<div class="empty">No items found.</div>';
+ return;
+ }
+
+ items.forEach(item => {
+ const article = document.createElement('article');
+ article.className = 'entry';
+
+ const date = new Date(item.published_at || item.created_at).toLocaleString();
+
+ article.innerHTML = `
+ <header class="entry-header">
+ <a href="${item.url}" class="entry-title" target="_blank">${item.title}</a>
+ <div class="entry-meta">
+ ${item.feed ? `<span class="feed-name">${item.feed.title}</span> • ` : ''}
+ <span class="date">${date}</span>
+ </div>
+ </header>
+ <div class="entry-content">
+ ${item.description || ''}
+ </div>
+ `;
+ list.appendChild(article);
+ });
+}
diff --git a/vanilla/index.html b/vanilla/index.html
new file mode 100644
index 0000000..98d505b
--- /dev/null
+++ b/vanilla/index.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Neko Reader (Vanilla)</title>
+ <link rel="stylesheet" href="style.css">
+ <style>
+ /* Minimal reset for now, proper styles in style.css */
+ body, html { margin: 0; padding: 0; height: 100%; font-family: sans-serif; }
+ </style>
+</head>
+<body>
+ <div id="app">
+ <aside id="sidebar">
+ <header>
+ <h1>🐱 Neko</h1>
+ </header>
+ <nav id="feeds-nav">
+ <div class="loading">Loading feeds...</div>
+ </nav>
+ </aside>
+ <main id="main">
+ <header id="main-header">
+ <h2 id="feed-title">All Items</h2>
+ </header>
+ <div id="entries-list">
+ <div class="loading">Loading items...</div>
+ </div>
+ </main>
+ </div>
+ <script src="app.js"></script>
+</body>
+</html>
diff --git a/vanilla/style.css b/vanilla/style.css
new file mode 100644
index 0000000..f83011f
--- /dev/null
+++ b/vanilla/style.css
@@ -0,0 +1,123 @@
+:root {
+ --bg-color: #f6f6f6;
+ --sidebar-bg: #eaeaea;
+ --item-bg: #fff;
+ --text-color: #222;
+ --link-color: #0000EE;
+ /* Standard blue link */
+ --border-color: #ddd;
+ --selected-bg: #e8f0fe;
+}
+
+body {
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ overflow: hidden;
+ /* App container handles scrolling */
+}
+
+#app {
+ display: flex;
+ height: 100vh;
+}
+
+#sidebar {
+ width: 250px;
+ background-color: var(--sidebar-bg);
+ border-right: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+}
+
+#sidebar header {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+#sidebar h1 {
+ margin: 0;
+ font-size: 1.2rem;
+}
+
+#feeds-nav {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0.5rem;
+}
+
+.feed-item {
+ padding: 0.5rem;
+ cursor: pointer;
+ border-radius: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.feed-item:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.feed-item.active {
+ font-weight: bold;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+#main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background-color: #fff;
+}
+
+#main-header {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color);
+ background-color: #fcfcfc;
+}
+
+#main-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+#entries-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1rem;
+}
+
+.entry {
+ background-color: var(--item-bg);
+ border-bottom: 1px solid var(--border-color);
+ padding: 1rem 0;
+}
+
+.entry-header {
+ margin-bottom: 0.5rem;
+}
+
+.entry-title {
+ font-size: 1.2rem;
+ font-weight: bold;
+ color: var(--link-color);
+ text-decoration: none;
+ display: block;
+ margin-bottom: 0.25rem;
+}
+
+.entry-meta {
+ font-size: 0.85rem;
+ color: #666;
+}
+
+.entry-content {
+ line-height: 1.6;
+ max-width: 800px;
+}
+
+.entry-content img {
+ max-width: 100%;
+ height: auto;
+} \ No newline at end of file
diff --git a/web/web.go b/web/web.go
index 1a0270e..c9ff5e0 100644
--- a/web/web.go
+++ b/web/web.go
@@ -30,6 +30,7 @@ var gzPool = sync.Pool{
var (
staticBox *rice.Box
frontendBox *rice.Box
+ vanillaBox *rice.Box
)
func init() {
@@ -43,6 +44,11 @@ func init() {
if err != nil {
log.Printf("Warning: Could not find frontendBox at ../frontend/dist: %v", err)
}
+
+ vanillaBox, err = rice.FindBox("../vanilla")
+ if err != nil {
+ log.Printf("Warning: Could not find vanillaBox at ../vanilla: %v", err)
+ }
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
@@ -236,6 +242,11 @@ func NewRouter() http.Handler {
apiRouter := api.NewRouter()
mux.Handle("/api/", GzipMiddleware(http.StripPrefix("/api", AuthWrapHandler(apiRouter))))
+ // Vanilla JS Prototype
+ if vanillaBox != nil {
+ mux.Handle("/vanilla/", GzipMiddleware(http.StripPrefix("/vanilla/", http.FileServer(vanillaBox.HTTPBox()))))
+ }
+
// Legacy routes for backward compatibility
mux.HandleFunc("/stream/", AuthWrap(api.HandleStream))
mux.HandleFunc("/item/", AuthWrap(api.HandleItem))