Ready for release: Includes Auto-cron, Installer, and Schema
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# .env
|
||||
MYSQL_ROOT_PASSWORD=someweirdrootpassword
|
||||
MYSQL_DATABASE=rss_db
|
||||
MYSQL_USER=rss_user
|
||||
MYSQL_PASSWORD=secretpassword123
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# --- Security ---
|
||||
# Contains database passwords and secrets
|
||||
.env
|
||||
# Legacy config file (if you still use it locally)
|
||||
src/config.php
|
||||
|
||||
# --- Dependencies ---
|
||||
# Composer downloads these automatically during build
|
||||
src/vendor/
|
||||
|
||||
# --- Application Data ---
|
||||
# RSS cache folder (generated dynamically)
|
||||
src/cache/
|
||||
# Cron job logs
|
||||
src/cron_output.log
|
||||
|
||||
# --- System Files ---
|
||||
# Mac OS metadata
|
||||
.DS_Store
|
||||
# Windows thumbnail cache
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# --- IDE Settings (Optional but recommended) ---
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM php:8.3-apache
|
||||
|
||||
# 1. Install system dependencies (Git, Unzip, Cron)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
cron \
|
||||
&& docker-php-ext-install zip pdo pdo_mysql
|
||||
|
||||
# 2. Install Composer globally
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# 3. Enable Apache mod_rewrite
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# 4. Copy the entrypoint script and make it executable
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# 5. Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# 6. Set Entrypoint
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
# 7. Default command
|
||||
CMD ["apache2-foreground"]
|
||||
24
config.php
Normal file
24
config.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Read variables from Docker Environment
|
||||
$host = getenv('DB_HOST') ?: 'db';
|
||||
$db = getenv('DB_NAME') ?: 'rss_db';
|
||||
$user = getenv('DB_USER') ?: 'root';
|
||||
$pass = getenv('DB_PASS'); // This comes from docker-compose
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass, $options);
|
||||
} catch (\PDOException $e) {
|
||||
// Show a cleaner error if DB connection fails
|
||||
die("Database Connection Failed. Check your .env file and container status.");
|
||||
}
|
||||
?>
|
||||
42
docker-compose.yml .yml
Normal file
42
docker-compose.yml .yml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./src:/var/www/html
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- rss-network
|
||||
# Pass the variables to PHP
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_NAME=${MYSQL_DATABASE}
|
||||
- DB_USER=${MYSQL_USER}
|
||||
- DB_PASS=${MYSQL_PASSWORD}
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: rss_mysql
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
# MAGIC LINE: This auto-runs your SQL on first launch!
|
||||
- ./schema.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- rss-network
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
rss-network:
|
||||
driver: bridge
|
||||
24
docker-entrypoint.sh
Normal file
24
docker-entrypoint.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 1. Install SimplePie if vendor folder is missing
|
||||
if [ ! -d "/var/www/html/vendor" ]; then
|
||||
echo "Vendor folder not found. Installing SimplePie..."
|
||||
composer require simplepie/simplepie
|
||||
fi
|
||||
|
||||
# 2. Ensure Cache folder exists and is writable
|
||||
if [ ! -d "/var/www/html/cache" ]; then
|
||||
mkdir -p /var/www/html/cache
|
||||
chmod -R 777 /var/www/html/cache
|
||||
fi
|
||||
|
||||
# 3. Setup Cron Job (Runs every 15 minutes)
|
||||
# We overwrite the file to ensure no duplicates
|
||||
echo "*/15 * * * * /usr/local/bin/php /var/www/html/fetch.php >> /var/www/html/cron_output.log 2>&1" > /etc/cron.d/rss-cron
|
||||
chmod 0644 /etc/cron.d/rss-cron
|
||||
crontab /etc/cron.d/rss-cron
|
||||
service cron start
|
||||
|
||||
# 4. Start Apache
|
||||
exec "$@"
|
||||
51
fetch.php
Normal file
51
fetch.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
require 'vendor/autoload.php'; // Or manual include if not using composer
|
||||
|
||||
$feed = new SimplePie();
|
||||
$feed->set_cache_location(__DIR__ . '/cache');
|
||||
|
||||
// 1. Get feeds
|
||||
$stmt = $pdo->query("SELECT id, rss_url FROM feeds");
|
||||
$feeds_list = $stmt->fetchAll();
|
||||
|
||||
foreach ($feeds_list as $row) {
|
||||
$feed->set_feed_url($row['rss_url']);
|
||||
$feed->init();
|
||||
$feed->handle_content_type();
|
||||
|
||||
foreach ($feed->get_items() as $item) {
|
||||
// FIX: Decode HTML entities so "AT&T" becomes "AT&T"
|
||||
$title = html_entity_decode($item->get_title(), ENT_QUOTES | ENT_HTML5);
|
||||
$desc = html_entity_decode($item->get_description(), ENT_QUOTES | ENT_HTML5);
|
||||
|
||||
$link = $item->get_permalink();
|
||||
$date = $item->get_date('Y-m-d H:i:s');
|
||||
if (!$date) { $date = date('Y-m-d H:i:s'); }
|
||||
|
||||
$sql = "INSERT IGNORE INTO items (feed_id, title, link, description, pub_date)
|
||||
VALUES (?, ?, ?, ?, ?)";
|
||||
$stmtInsert = $pdo->prepare($sql);
|
||||
$stmtInsert->execute([$row['id'], $title, $link, $desc, $date]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- CLEANUP ROUTINE ---
|
||||
// Delete articles older than 30 days, BUT KEEP items marked as 'is_saved'
|
||||
$days_to_keep = 30;
|
||||
$sql_cleanup = "DELETE FROM items
|
||||
WHERE pub_date < DATE_SUB(NOW(), INTERVAL ? DAY)
|
||||
AND is_saved = 0";
|
||||
|
||||
$stmtCleanup = $pdo->prepare($sql_cleanup);
|
||||
$stmtCleanup->execute([$days_to_keep]);
|
||||
|
||||
// -----------------------
|
||||
|
||||
// 2. Redirect back to dashboard immediately
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
|
||||
?>
|
||||
377
index.php
Normal file
377
index.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
require 'config.php';
|
||||
|
||||
// --- ACTION HANDLERS ---
|
||||
|
||||
// 1. Add Feed
|
||||
if (isset($_POST['add_feed'])) {
|
||||
$stmt = $pdo->prepare("INSERT INTO feeds (site_name, rss_url, group_id) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$_POST['site_name'], $_POST['rss_url'], $_POST['group_id']]);
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Add Group
|
||||
if (isset($_POST['add_group'])) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `groups` (name) VALUES (?)");
|
||||
$stmt->execute([$_POST['group_name']]);
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Delete Feed
|
||||
if (isset($_POST['delete_feed'])) {
|
||||
$stmt = $pdo->prepare("DELETE FROM feeds WHERE id = ?");
|
||||
$stmt->execute([$_POST['feed_id_to_delete']]);
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Edit Feed
|
||||
if (isset($_POST['edit_feed'])) {
|
||||
$stmt = $pdo->prepare("UPDATE feeds SET site_name = ?, rss_url = ?, group_id = ? WHERE id = ?");
|
||||
$stmt->execute([
|
||||
$_POST['edit_site_name'],
|
||||
$_POST['edit_rss_url'],
|
||||
$_POST['edit_group_id'],
|
||||
$_POST['edit_feed_id']
|
||||
]);
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5. Mark as Read (Updated with Undo Memory)
|
||||
if (isset($_GET['mark_read'])) {
|
||||
$id = $_GET['mark_read'];
|
||||
|
||||
// Update DB
|
||||
$stmt = $pdo->prepare("UPDATE items SET is_read = 1, is_saved = 0 WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
// SAVE ID TO SESSION FOR UNDO
|
||||
$_SESSION['undo_entry_id'] = $id;
|
||||
|
||||
// Redirect logic (Same as before)
|
||||
$params = [];
|
||||
if (isset($_GET['view'])) $params[] = "view=" . $_GET['view'];
|
||||
if (isset($_GET['group_id'])) $params[] = "group_id=" . $_GET['group_id'];
|
||||
$redirect = count($params) > 0 ? "?" . implode("&", $params) : "index.php";
|
||||
header("Location: $redirect");
|
||||
exit;
|
||||
}
|
||||
// 6. Save/Unsave Logic
|
||||
if (isset($_GET['save_item'])) {
|
||||
$stmt = $pdo->prepare("UPDATE items SET is_saved = 1 WHERE id = ?");
|
||||
$stmt->execute([$_GET['save_item']]);
|
||||
$params = [];
|
||||
if (isset($_GET['view'])) $params[] = "view=" . $_GET['view'];
|
||||
if (isset($_GET['group_id'])) $params[] = "group_id=" . $_GET['group_id'];
|
||||
$redirect = count($params) > 0 ? "?" . implode("&", $params) : "index.php";
|
||||
header("Location: $redirect");
|
||||
exit;
|
||||
}
|
||||
if (isset($_GET['unsave_item'])) {
|
||||
$stmt = $pdo->prepare("UPDATE items SET is_saved = 0 WHERE id = ?");
|
||||
$stmt->execute([$_GET['unsave_item']]);
|
||||
$params = [];
|
||||
$params[] = "view=" . ($_GET['view'] ?? 'saved');
|
||||
if (isset($_GET['group_id'])) $params[] = "group_id=" . $_GET['group_id'];
|
||||
$redirect = "?" . implode("&", $params);
|
||||
header("Location: $redirect");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 7. NEW: Undo Handler
|
||||
if (isset($_GET['undo_action'])) {
|
||||
if (isset($_SESSION['undo_entry_id'])) {
|
||||
// Revert the last item to Unread (is_read = 0)
|
||||
$stmt = $pdo->prepare("UPDATE items SET is_read = 0 WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['undo_entry_id']]);
|
||||
|
||||
// Clear the session so the button disappears
|
||||
unset($_SESSION['undo_entry_id']);
|
||||
}
|
||||
|
||||
// Redirect logic to keep you on the same page
|
||||
$params = [];
|
||||
if (isset($_GET['view'])) $params[] = "view=" . $_GET['view'];
|
||||
if (isset($_GET['group_id'])) $params[] = "group_id=" . $_GET['group_id'];
|
||||
$redirect = count($params) > 0 ? "?" . implode("&", $params) : "index.php";
|
||||
header("Location: $redirect");
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- DATA FETCHING ---
|
||||
$groups = $pdo->query("SELECT * FROM `groups`")->fetchAll();
|
||||
|
||||
// Fetch all feeds for the management modal
|
||||
$all_feeds = $pdo->query("SELECT feeds.*, groups.name as group_name FROM feeds LEFT JOIN `groups` ON feeds.group_id = groups.id ORDER BY site_name ASC")->fetchAll();
|
||||
|
||||
$view_mode = $_GET['view'] ?? 'unread';
|
||||
$filter_group_id = $_GET['group_id'] ?? null;
|
||||
|
||||
// Build SQL
|
||||
$sql = "SELECT items.id AS item_id, items.title, items.link, items.description, items.pub_date, feeds.site_name
|
||||
FROM items JOIN feeds ON items.feed_id = feeds.id ";
|
||||
$params = [];
|
||||
|
||||
if ($view_mode === 'saved') {
|
||||
$sql .= "WHERE is_saved = 1 ";
|
||||
} else {
|
||||
$sql .= "WHERE is_read = 0 AND is_saved = 0 ";
|
||||
}
|
||||
|
||||
if ($filter_group_id) {
|
||||
$sql .= "AND feeds.group_id = ? ";
|
||||
$params[] = $filter_group_id;
|
||||
}
|
||||
$sql .= "ORDER BY pub_date DESC LIMIT 50";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$items = $stmt->fetchAll();
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>RSS Catcher</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar { height: 100vh; overflow-y: auto; position: fixed; width: 25%; }
|
||||
.main-content { margin-left: 25%; width: 75%; }
|
||||
.nav-link.active { background-color: #0d6efd; color: white !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-3 bg-white p-4 border-end sidebar">
|
||||
<h3 class="mb-4">RSS Catcher</h3>
|
||||
|
||||
<div class="list-group mb-4">
|
||||
<a href="index.php" class="list-group-item list-group-item-action <?= ($view_mode == 'unread' && !$filter_group_id) ? 'active' : '' ?>">
|
||||
📥 All Unread
|
||||
</a>
|
||||
<a href="?view=saved" class="list-group-item list-group-item-action <?= ($view_mode == 'saved') ? 'active' : '' ?>">
|
||||
⭐ Saved for Later
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted text-uppercase small">Filter by Group</h6>
|
||||
<div class="list-group mb-4">
|
||||
<?php foreach($groups as $g): ?>
|
||||
<a href="?group_id=<?= $g['id'] ?>"
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center <?= ($filter_group_id == $g['id']) ? 'active' : '' ?>">
|
||||
<?= htmlspecialchars($g['name']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#manageFeedsModal">
|
||||
🛠 Manage Feeds (Edit/Delete)
|
||||
</button>
|
||||
|
||||
<div class="accordion" id="adminControls">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#addControls">
|
||||
⚙️ Add New
|
||||
</button>
|
||||
</h2>
|
||||
<div id="addControls" class="accordion-collapse collapse" data-bs-parent="#adminControls">
|
||||
<div class="accordion-body">
|
||||
<form method="post" class="mb-3">
|
||||
<label class="form-label small">New Group</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="group_name" class="form-control" required>
|
||||
<button class="btn btn-secondary" name="add_group">+</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post">
|
||||
<label class="form-label small">New Feed</label>
|
||||
<input type="text" name="site_name" class="form-control form-control-sm mb-2" placeholder="Site Name" required>
|
||||
<input type="url" name="rss_url" class="form-control form-control-sm mb-2" placeholder="RSS URL" required>
|
||||
<select name="group_id" class="form-select form-select-sm mb-2">
|
||||
<?php foreach($groups as $g): ?>
|
||||
<option value="<?= $g['id'] ?>"><?= $g['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm w-100" name="add_feed">Add Feed</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="fetch.php" class="btn btn-success btn-sm">Force Refresh</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9 p-4 main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
|
||||
<h2>
|
||||
<?php
|
||||
$count_label = " <small class='text-muted'>(" . count($items) . ")</small>";
|
||||
if ($view_mode === 'saved') echo "⭐ Saved Articles" . $count_label;
|
||||
elseif ($filter_group_id) echo "📂 Group View" . $count_label;
|
||||
else echo "📥 Unread Articles" . $count_label;
|
||||
?>
|
||||
</h2>
|
||||
|
||||
<?php if (isset($_SESSION['undo_entry_id'])): ?>
|
||||
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-3">
|
||||
<span>✅ Article marked as read.</span>
|
||||
|
||||
<?php
|
||||
$undoParams = "undo_action=true";
|
||||
if (isset($_GET['view'])) $undoParams .= "&view=" . $_GET['view'];
|
||||
if (isset($_GET['group_id'])) $undoParams .= "&group_id=" . $_GET['group_id'];
|
||||
?>
|
||||
|
||||
<a href="?<?= $undoParams ?>" class="btn btn-sm btn-dark">↩ Undo</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (count($items) === 0): ?>
|
||||
<div class="alert alert-info">No articles found in this view.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<?php foreach($items as $item): ?>
|
||||
<div class="col-12 mb-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<a href="<?= $item['link'] ?>" target="_blank" class="text-decoration-none text-dark">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted small">
|
||||
<?= htmlspecialchars($item['site_name']) ?> | <?= date('M j, g:i a', strtotime($item['pub_date'])) ?>
|
||||
</h6>
|
||||
<p class="card-text text-secondary">
|
||||
<?= strip_tags(substr($item['description'], 0, 250)) ?>...
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<?php
|
||||
$contextParams = "&view=" . $view_mode;
|
||||
if ($filter_group_id) $contextParams .= "&group_id=" . $filter_group_id;
|
||||
?>
|
||||
<a href="?mark_read=<?= $item['item_id'] ?><?= $contextParams ?>" class="btn btn-sm btn-outline-secondary">Mark as Read</a>
|
||||
|
||||
<?php if($view_mode === 'saved'): ?>
|
||||
<a href="?unsave_item=<?= $item['item_id'] ?><?= $contextParams ?>" class="btn btn-sm btn-warning">Remove from Saved</a>
|
||||
<?php else: ?>
|
||||
<a href="?save_item=<?= $item['item_id'] ?><?= $contextParams ?>" class="btn btn-sm btn-outline-primary">⭐ Save for Later</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="manageFeedsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Manage Feeds</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Group</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($all_feeds as $f): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($f['site_name']) ?></td>
|
||||
<td><?= htmlspecialchars($f['group_name']) ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary"
|
||||
onclick="openEditModal(<?= $f['id'] ?>, '<?= addslashes($f['site_name']) ?>', '<?= addslashes($f['rss_url']) ?>', <?= $f['group_id'] ?>)">
|
||||
Edit
|
||||
</button>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('Delete this feed and ALL its articles?');">
|
||||
<input type="hidden" name="feed_id_to_delete" value="<?= $f['id'] ?>">
|
||||
<button class="btn btn-sm btn-danger" name="delete_feed">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="editFeedModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Feed</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="edit_feed_id" id="edit_feed_id">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Site Name</label>
|
||||
<input type="text" name="edit_site_name" id="edit_site_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>RSS URL</label>
|
||||
<input type="url" name="edit_rss_url" id="edit_rss_url" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Group</label>
|
||||
<select name="edit_group_id" id="edit_group_id" class="form-select">
|
||||
<?php foreach($groups as $g): ?>
|
||||
<option value="<?= $g['id'] ?>"><?= $g['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" name="edit_feed">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function openEditModal(id, name, url, groupId) {
|
||||
// Populate fields
|
||||
document.getElementById('edit_feed_id').value = id;
|
||||
document.getElementById('edit_site_name').value = name;
|
||||
document.getElementById('edit_rss_url').value = url;
|
||||
document.getElementById('edit_group_id').value = groupId;
|
||||
|
||||
// Open Modal
|
||||
var myModal = new bootstrap.Modal(document.getElementById('editFeedModal'));
|
||||
myModal.show();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
30
schema.sql
Normal file
30
schema.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
CREATE TABLE `groups` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `feeds` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`group_id` int(11) NOT NULL,
|
||||
`site_name` varchar(255) NOT NULL,
|
||||
`rss_url` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE `items` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`feed_id` int(11) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`link` varchar(500) NOT NULL,
|
||||
`description` TEXT,
|
||||
`pub_date` datetime DEFAULT NULL,
|
||||
`is_read` tinyint(1) DEFAULT 0,
|
||||
`is_saved` tinyint(1) DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_link` (`link`),
|
||||
FOREIGN KEY (`feed_id`) REFERENCES `feeds`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO `groups` (`name`) VALUES ('Tech News');
|
||||
Reference in New Issue
Block a user