Upgrade of BloomFeed v0.1
@@ -0,0 +1,15 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,74 @@
|
||||
APP_NAME=BloomFeed
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost:7081
|
||||
|
||||
APP_LOCALE=fr
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=fr_FR
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=bloomfeed
|
||||
DB_USERNAME=bloomfeed
|
||||
DB_PASSWORD=
|
||||
|
||||
# Utilisé par docker-compose pour initialiser MariaDB (doit correspondre à DB_PASSWORD ci-dessus)
|
||||
DB_ROOT_PASSWORD=
|
||||
|
||||
# Catalogue central BloomFluxDB (lecture seule)
|
||||
BLOOMFLUX_DB_HOST=163.5.159.95
|
||||
BLOOMFLUX_DB_PORT=3306
|
||||
BLOOMFLUX_DB_DATABASE=bloomfluxdb
|
||||
BLOOMFLUX_DB_USERNAME=bloomflux_reader
|
||||
BLOOMFLUX_DB_PASSWORD=BloomFlux_Public_Read_2026
|
||||
BLOOMFLUX_DB_TIMEOUT=4
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
@@ -0,0 +1,8 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
@@ -0,0 +1,30 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.claude
|
||||
/.codex
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/fonts-manifest.dev.json
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
composer.phar
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
@@ -21,6 +21,8 @@ n'a qu'un seul compte propriétaire, créé une seule fois au premier lancement.
|
||||
- Lecture approfondie intégrée : extraction du contenu principal (via
|
||||
[Readability](https://github.com/fivefilters/readability.php)) converti en Markdown, sans
|
||||
quitter BloomFeed.
|
||||
- Catalogue de flux façon "market" alimenté par **BloomFluxDB** (base centrale en lecture seule),
|
||||
avec cache local de secours si le serveur catalogue est injoignable.
|
||||
- Récupération périodique des flux via une tâche planifiée.
|
||||
- Thème clair/sombre.
|
||||
- Inscription fermée après la création du compte propriétaire — pas de SaaS ouvert.
|
||||
@@ -31,9 +33,9 @@ n'a qu'un seul compte propriétaire, créé une seule fois au premier lancement.
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Mode sombre | Connexion |
|
||||
| Catalogue de flux | Mode sombre |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Stack technique
|
||||
|
||||
@@ -63,7 +65,7 @@ planificateur, MariaDB). Voir la page **[/self-hosting](/self-hosting)** une foi
|
||||
l'application lancée, ou directement :
|
||||
|
||||
```bash
|
||||
git clone https://git.eldorianet.work/esteban/RSS-Eldoria-Network bloomfeed
|
||||
git clone https://git.eldorianet.work/esteban/BloomFeed-RSS bloomfeed
|
||||
cd bloomfeed
|
||||
bash scriptsite.sh
|
||||
```
|
||||
@@ -78,6 +80,23 @@ Pour désinstaller proprement (avec confirmation avant toute suppression de donn
|
||||
bash uninstall.sh
|
||||
```
|
||||
|
||||
## BloomFluxDB (catalogue central)
|
||||
|
||||
Les instances BloomFeed consultent en lecture seule une base MariaDB centrale contenant une
|
||||
liste de flux pré-sélectionnés, affichée dans l'onglet **Catalogue**. La liste est mise en cache
|
||||
localement : si le serveur catalogue est injoignable, la dernière copie connue est servie, et
|
||||
elle se rafraîchit automatiquement à la prochaine consultation réussie.
|
||||
|
||||
Le script de création (base, table, utilisateur `bloomflux_reader` en `SELECT` uniquement, et
|
||||
liste de flux initiale) est dans [docs/bloomfluxdb-setup.sql](docs/bloomfluxdb-setup.sql) — à
|
||||
exécuter sur le serveur catalogue :
|
||||
|
||||
```bash
|
||||
mysql -u root -p < docs/bloomfluxdb-setup.sql
|
||||
```
|
||||
|
||||
La connexion se configure dans le `.env` de chaque instance via les variables `BLOOMFLUX_DB_*`.
|
||||
|
||||
## Licence
|
||||
|
||||
MIT.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\FluxCatalogService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CatalogController extends Controller
|
||||
{
|
||||
public function index(Request $request, FluxCatalogService $catalog): View
|
||||
{
|
||||
$result = $catalog->getCatalog();
|
||||
|
||||
$subscribedUrls = $request->user()->feeds()->pluck('url')->all();
|
||||
|
||||
return view('catalog', [
|
||||
'feeds' => collect($result['feeds'])->groupBy('category'),
|
||||
'fromCache' => $result['fromCache'],
|
||||
'updatedAt' => $result['updatedAt'],
|
||||
'subscribedUrls' => $subscribedUrls,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class FluxCatalogService
|
||||
{
|
||||
private const CACHE_KEY = 'bloomflux.catalog';
|
||||
|
||||
private const CACHE_TIME_KEY = 'bloomflux.catalog_updated_at';
|
||||
|
||||
/**
|
||||
* Retourne le catalogue BloomFluxDB, en le rafraîchissant depuis le serveur
|
||||
* distant quand il est joignable, sinon en servant la dernière copie en cache.
|
||||
*
|
||||
* @return array{feeds: list<array{title: string, url: string, category: string, lang: string, description: ?string}>, fromCache: bool, updatedAt: ?Carbon}
|
||||
*/
|
||||
public function getCatalog(): array
|
||||
{
|
||||
try {
|
||||
$rows = DB::connection('bloomflux')
|
||||
->table('feeds')
|
||||
->where('is_active', 1)
|
||||
->orderBy('category')
|
||||
->orderBy('title')
|
||||
->get(['title', 'url', 'category', 'lang', 'description'])
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
Cache::forever(self::CACHE_KEY, $rows);
|
||||
Cache::forever(self::CACHE_TIME_KEY, now()->toIso8601String());
|
||||
|
||||
return ['feeds' => $rows, 'fromCache' => false, 'updatedAt' => now()];
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('BloomFluxDB unreachable, serving cached catalog', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$cached = Cache::get(self::CACHE_KEY, []);
|
||||
$updatedAt = Cache::get(self::CACHE_TIME_KEY);
|
||||
|
||||
return [
|
||||
'feeds' => $cached,
|
||||
'fromCache' => true,
|
||||
'updatedAt' => $updatedAt ? Carbon::parse($updatedAt) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,24 @@ return [
|
||||
]) : [],
|
||||
],
|
||||
|
||||
// Catalogue central BloomFluxDB (lecture seule, distant) — timeout court
|
||||
// pour ne jamais bloquer la page si le serveur catalogue est injoignable.
|
||||
'bloomflux' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('BLOOMFLUX_DB_HOST', '163.5.159.95'),
|
||||
'port' => env('BLOOMFLUX_DB_PORT', '3306'),
|
||||
'database' => env('BLOOMFLUX_DB_DATABASE', 'bloomfluxdb'),
|
||||
'username' => env('BLOOMFLUX_DB_USERNAME', 'bloomflux_reader'),
|
||||
'password' => env('BLOOMFLUX_DB_PASSWORD', 'BloomFlux_Public_Read_2026'),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => true,
|
||||
'options' => extension_loaded('pdo_mysql') ? [
|
||||
\PDO::ATTR_TIMEOUT => (int) env('BLOOMFLUX_DB_TIMEOUT', 4),
|
||||
] : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
-- ============================================================
|
||||
-- BloomFluxDB — catalogue central de flux RSS pour les instances BloomFeed
|
||||
-- À exécuter sur le serveur catalogue (163.5.159.95), en root MariaDB/MySQL :
|
||||
-- mysql -u root -p < bloomfluxdb-setup.sql
|
||||
--
|
||||
-- Prérequis réseau : MariaDB doit écouter sur l'extérieur
|
||||
-- (bind-address = 0.0.0.0 dans /etc/mysql/mariadb.conf.d/50-server.cnf,
|
||||
-- puis systemctl restart mariadb) et le port 3306 doit être ouvert.
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS bloomfluxdb
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE bloomfluxdb;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feeds (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
url VARCHAR(2048) NOT NULL,
|
||||
category VARCHAR(100) NOT NULL DEFAULT 'Divers',
|
||||
lang CHAR(2) NOT NULL DEFAULT 'fr',
|
||||
description VARCHAR(500) NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_feeds_url (url(500)),
|
||||
KEY idx_feeds_category (category),
|
||||
KEY idx_feeds_active (is_active)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Utilisateur en LECTURE SEULE pour les instances BloomFeed ('%' = accessible
|
||||
-- depuis n'importe quelle IP). Changez le mot de passe si vous voulez restreindre,
|
||||
-- et reportez-le dans le .env des instances (BLOOMFLUX_DB_PASSWORD).
|
||||
-- ------------------------------------------------------------
|
||||
CREATE USER IF NOT EXISTS 'bloomflux_reader'@'%' IDENTIFIED BY 'BloomFlux_Public_Read_2026';
|
||||
GRANT SELECT ON bloomfluxdb.feeds TO 'bloomflux_reader'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Liste de flux pré-faite (extensible à volonté : INSERT depuis root/localhost)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT IGNORE INTO feeds (title, url, category, lang, description) VALUES
|
||||
-- Actualités générales
|
||||
('Le Monde', 'https://www.lemonde.fr/rss/une.xml', 'Actualités', 'fr', 'La une du journal Le Monde.'),
|
||||
('France Info', 'https://www.franceinfo.fr/titres.rss', 'Actualités', 'fr', 'Les titres de France Info en continu.'),
|
||||
('Courrier International', 'https://www.courrierinternational.com/feed/all/rss.xml', 'Actualités', 'fr', 'L''actualité internationale vue par la presse étrangère.'),
|
||||
('BBC News', 'https://feeds.bbci.co.uk/news/rss.xml', 'Actualités', 'en', 'Fil d''actualité mondial de la BBC.'),
|
||||
-- Tech & informatique
|
||||
('Korben', 'https://korben.info/feed', 'Tech', 'fr', 'Actu tech, astuces et bidouilles.'),
|
||||
('Numerama', 'https://www.numerama.com/feed/', 'Tech', 'fr', 'Société numérique, tech et sciences.'),
|
||||
('Next', 'https://next.ink/feed/', 'Tech', 'fr', 'Actualité informatique et numérique de fond.'),
|
||||
('Journal du hacker', 'https://www.journalduhacker.net/rss', 'Tech', 'fr', 'Liens tech partagés par la communauté francophone.'),
|
||||
('Ars Technica', 'https://feeds.arstechnica.com/arstechnica/index', 'Tech', 'en', 'Analyses tech approfondies.'),
|
||||
('Hacker News', 'https://news.ycombinator.com/rss', 'Tech', 'en', 'Les liens les plus discutés de la communauté HN.'),
|
||||
('The Verge', 'https://www.theverge.com/rss/index.xml', 'Tech', 'en', 'Tech, science et culture numérique.'),
|
||||
-- Linux & open source
|
||||
('Phoronix', 'https://www.phoronix.com/rss.php', 'Open source', 'en', 'Linux, matériel et open source.'),
|
||||
('LinuxFr', 'https://linuxfr.org/news.atom', 'Open source', 'fr', 'L''actualité du logiciel libre en français.'),
|
||||
('It''s FOSS', 'https://itsfoss.com/rss/', 'Open source', 'en', 'Tutoriels et actus Linux/open source.'),
|
||||
-- Sécurité
|
||||
('CERT-FR', 'https://www.cert.ssi.gouv.fr/feed/', 'Sécurité', 'fr', 'Alertes et avis de sécurité de l''ANSSI.'),
|
||||
('Krebs on Security', 'https://krebsonsecurity.com/feed/', 'Sécurité', 'en', 'Investigations cybersécurité de Brian Krebs.'),
|
||||
('Zataz', 'https://www.zataz.com/feed/', 'Sécurité', 'fr', 'Actualité cybersécurité et fuites de données.'),
|
||||
-- Développement
|
||||
('Dev.to', 'https://dev.to/feed', 'Développement', 'en', 'Articles de la communauté dev.to.'),
|
||||
('GitHub Blog', 'https://github.blog/feed/', 'Développement', 'en', 'Nouveautés et ingénierie GitHub.'),
|
||||
('Laravel News', 'https://feed.laravel-news.com/', 'Développement', 'en', 'L''écosystème Laravel au quotidien.'),
|
||||
-- Sciences
|
||||
('Futura Sciences', 'https://www.futura-sciences.com/rss/actualites.xml', 'Sciences', 'fr', 'Actualités scientifiques tous domaines.'),
|
||||
('CNRS Le Journal', 'https://lejournal.cnrs.fr/rss', 'Sciences', 'fr', 'Le journal de la recherche française.'),
|
||||
-- Jeux vidéo
|
||||
('Gamekult', 'https://www.gamekult.com/feed.xml', 'Jeux vidéo', 'fr', 'Actualité et critiques jeux vidéo.'),
|
||||
('Rock Paper Shotgun', 'https://www.rockpapershotgun.com/feed', 'Jeux vidéo', 'en', 'Jeux PC, tests et actus.');
|
||||
|
||||
-- Vérification rapide
|
||||
SELECT category, COUNT(*) AS nb FROM feeds GROUP BY category ORDER BY category;
|
||||
|
After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 751 KiB |
|
Before Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 322 KiB |
@@ -998,6 +998,148 @@ h1, h2, h3 {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
/* ---------- Catalogue (market) ---------- */
|
||||
|
||||
.market-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cache-notice {
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--accent-soft-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px 16px;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--accent-strong);
|
||||
animation: fade-up 350ms var(--ease-out) both;
|
||||
}
|
||||
|
||||
.market-search {
|
||||
font-size: var(--font-base);
|
||||
padding: 13px 18px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.market-chips {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background: var(--surface);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 6px 15px;
|
||||
font-size: var(--font-xs);
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast), transform var(--t-fast);
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: var(--gradient-accent);
|
||||
border-color: transparent;
|
||||
color: var(--accent-contrast);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.chip-static {
|
||||
cursor: default;
|
||||
background: var(--accent-soft);
|
||||
border-color: transparent;
|
||||
color: var(--accent-strong);
|
||||
font-size: 10px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.chip-static:hover {
|
||||
transform: none;
|
||||
color: var(--accent-strong);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.market-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.market-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
animation: fade-up 450ms var(--ease-out) both;
|
||||
transition: transform var(--t-base), box-shadow var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
|
||||
.market-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--accent-soft-strong);
|
||||
}
|
||||
|
||||
.market-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.market-card-title {
|
||||
font-weight: 700;
|
||||
font-size: var(--font-base);
|
||||
}
|
||||
|
||||
.market-card-desc {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.market-card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.market-lang {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-faint);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.market-added {
|
||||
color: var(--green);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
||||
@@ -31,4 +31,49 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Catalogue : recherche + filtre par catégorie côté client
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var grid = document.getElementById('market-grid');
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
var search = document.getElementById('market-search');
|
||||
var chips = document.getElementById('market-chips');
|
||||
var emptyNotice = document.getElementById('market-empty');
|
||||
var activeCategory = '';
|
||||
|
||||
function applyFilters() {
|
||||
var query = (search.value || '').toLowerCase().trim();
|
||||
var visible = 0;
|
||||
|
||||
grid.querySelectorAll('.market-card').forEach(function (card) {
|
||||
var matchesCategory = !activeCategory || card.getAttribute('data-category') === activeCategory;
|
||||
var matchesQuery = !query || card.getAttribute('data-search').indexOf(query) !== -1;
|
||||
var show = matchesCategory && matchesQuery;
|
||||
card.hidden = !show;
|
||||
if (show) {
|
||||
visible++;
|
||||
}
|
||||
});
|
||||
|
||||
emptyNotice.hidden = visible > 0;
|
||||
}
|
||||
|
||||
search.addEventListener('input', applyFilters);
|
||||
|
||||
chips.addEventListener('click', function (event) {
|
||||
var chip = event.target.closest('.chip');
|
||||
if (!chip) {
|
||||
return;
|
||||
}
|
||||
chips.querySelectorAll('.chip').forEach(function (c) {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
chip.classList.add('active');
|
||||
activeCategory = chip.getAttribute('data-category');
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
@extends('layouts.app', ['title' => 'Catalogue — BloomFeed'])
|
||||
|
||||
@section('content')
|
||||
<div class="main-column" style="width:100%;">
|
||||
@if (session('status'))
|
||||
<div class="form-status">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="market-header">
|
||||
<div>
|
||||
<h2 style="margin:0;">Catalogue de flux</h2>
|
||||
<p class="text-muted text-sm" style="margin:4px 0 0;">
|
||||
Une sélection de flux RSS maintenue par BloomFluxDB — ajoutez-les à votre collection en un clic.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($fromCache)
|
||||
<div class="cache-notice">
|
||||
⚠ Serveur catalogue injoignable — liste servie depuis le cache local
|
||||
@if ($updatedAt)
|
||||
(mise à jour {{ $updatedAt->diffForHumans() }})
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($feeds->isEmpty())
|
||||
<div class="card empty-state">
|
||||
Le catalogue est vide pour le moment.
|
||||
@if ($fromCache)
|
||||
Il sera récupéré automatiquement dès que le serveur catalogue sera joignable.
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<input type="search" id="market-search" class="form-input market-search" placeholder="Rechercher un flux… (nom, description, catégorie)">
|
||||
|
||||
<div class="market-chips" id="market-chips">
|
||||
<button type="button" class="chip active" data-category="">Tout</button>
|
||||
@foreach ($feeds->keys() as $category)
|
||||
<button type="button" class="chip" data-category="{{ $category }}">{{ $category }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="market-grid" id="market-grid">
|
||||
@foreach ($feeds as $category => $items)
|
||||
@foreach ($items as $item)
|
||||
<div class="market-card" data-category="{{ $category }}" data-search="{{ Str::lower($item['title'].' '.$item['description'].' '.$category) }}">
|
||||
<div class="market-card-head">
|
||||
<span class="market-card-title">{{ $item['title'] }}</span>
|
||||
<span class="chip chip-static">{{ $category }}</span>
|
||||
</div>
|
||||
<p class="market-card-desc">{{ $item['description'] ?: $item['url'] }}</p>
|
||||
<div class="market-card-foot">
|
||||
<span class="market-lang">{{ strtoupper($item['lang']) }}</span>
|
||||
@if (in_array($item['url'], $subscribedUrls))
|
||||
<span class="market-added">✓ Ajouté</span>
|
||||
@else
|
||||
<form method="POST" action="{{ route('feeds.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="url" value="{{ $item['url'] }}">
|
||||
<input type="hidden" name="title" value="{{ $item['title'] }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm">+ Ajouter</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="card empty-state" id="market-empty" hidden>Aucun flux ne correspond à votre recherche.</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<footer class="site-footer">
|
||||
<span>BloomFeed — projet open-source, auto-hébergé.</span>
|
||||
<a href="https://git.eldorianet.work/esteban/RSS-Eldoria-Network" target="_blank" rel="noopener">Code source</a>
|
||||
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">Code source</a>
|
||||
</footer>
|
||||
|
||||
<script src="{{ asset('js/theme-toggle.js') }}" defer></script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</a>
|
||||
<div class="navbar-links">
|
||||
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">Articles</a>
|
||||
<a href="{{ route('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">Catalogue</a>
|
||||
<a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">Mes flux</a>
|
||||
<button type="button" class="theme-toggle" data-theme-toggle title="Changer de thème">◐</button>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="card mt-2">
|
||||
<h2>Code source</h2>
|
||||
<p>
|
||||
<a href="https://git.eldorianet.work/esteban/RSS-Eldoria-Network" target="_blank" rel="noopener">git.eldorianet.work/esteban/RSS-Eldoria-Network</a>
|
||||
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">git.eldorianet.work/esteban/BloomFeed-RSS</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Http\Controllers\ArticleController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\CatalogController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\FeedController;
|
||||
use App\Http\Controllers\PageController;
|
||||
@@ -22,6 +23,8 @@ Route::middleware('auth')->group(function () {
|
||||
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
Route::get('/catalog', [CatalogController::class, 'index'])->name('catalog');
|
||||
|
||||
Route::get('/feeds', [FeedController::class, 'index'])->name('feeds.index');
|
||||
Route::post('/feeds', [FeedController::class, 'store'])->name('feeds.store');
|
||||
Route::delete('/feeds/{feed}', [FeedController::class, 'destroy'])->name('feeds.destroy');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://git.eldorianet.work/esteban/RSS-Eldoria-Network"
|
||||
REPO_URL="https://git.eldorianet.work/esteban/BloomFeed-RSS"
|
||||
APP_DIR="/opt/bloomfeed"
|
||||
HOST_PORT="7081"
|
||||
|
||||
|
||||