BloomFeed Upgrade v0.21

This commit is contained in:
Esteban
2026-07-04 03:07:54 +02:00
parent d73c25cbbb
commit 28ba35da13
36 changed files with 1981 additions and 274 deletions
+542 -45
View File
@@ -1,48 +1,545 @@
{
"version": 1,
"updated_at": "2026-07-03",
"version": 2,
"updated_at": "2026-07-04",
"feeds": [
{ "title": "Le Monde", "url": "https://www.lemonde.fr/rss/une.xml", "category": "Actualités", "lang": "fr", "description": "La une du journal Le Monde." },
{ "title": "France Info", "url": "https://www.franceinfo.fr/titres.rss", "category": "Actualités", "lang": "fr", "description": "Les titres de France Info en continu." },
{ "title": "Courrier International", "url": "https://www.courrierinternational.com/feed/all/rss.xml", "category": "Actualités", "lang": "fr", "description": "L'actualité internationale vue par la presse étrangère." },
{ "title": "BBC News", "url": "https://feeds.bbci.co.uk/news/rss.xml", "category": "Actualités", "lang": "en", "description": "Fil d'actualité mondial de la BBC." },
{ "title": "Korben", "url": "https://korben.info/feed", "category": "Tech", "lang": "fr", "description": "Actu tech, astuces et bidouilles." },
{ "title": "Numerama", "url": "https://www.numerama.com/feed/", "category": "Tech", "lang": "fr", "description": "Société numérique, tech et sciences." },
{ "title": "Next", "url": "https://next.ink/feed/", "category": "Tech", "lang": "fr", "description": "Actualité informatique et numérique de fond." },
{ "title": "Journal du hacker", "url": "https://www.journalduhacker.net/rss", "category": "Tech", "lang": "fr", "description": "Liens tech partagés par la communauté francophone." },
{ "title": "Ars Technica", "url": "https://feeds.arstechnica.com/arstechnica/index", "category": "Tech", "lang": "en", "description": "Analyses tech approfondies." },
{ "title": "Hacker News", "url": "https://news.ycombinator.com/rss", "category": "Tech", "lang": "en", "description": "Les liens les plus discutés de la communauté HN." },
{ "title": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "category": "Tech", "lang": "en", "description": "Tech, science et culture numérique." },
{ "title": "Dev.to", "url": "https://dev.to/feed", "category": "Développement", "lang": "en", "description": "Articles de la communauté dev.to." },
{ "title": "GitHub Blog", "url": "https://github.blog/feed/", "category": "Développement", "lang": "en", "description": "Nouveautés et ingénierie GitHub." },
{ "title": "Laravel News", "url": "https://feed.laravel-news.com/", "category": "Développement", "lang": "en", "description": "L'écosystème Laravel au quotidien." },
{ "title": "Stack Overflow Blog", "url": "https://stackoverflow.blog/feed/", "category": "Développement", "lang": "en", "description": "Le blog officiel de Stack Overflow." },
{ "title": "freeCodeCamp", "url": "https://www.freecodecamp.org/news/rss/", "category": "Développement", "lang": "en", "description": "Tutoriels et articles pour apprendre à coder." },
{ "title": "CSS-Tricks", "url": "https://css-tricks.com/feed/", "category": "Développement", "lang": "en", "description": "Tout sur le CSS et le front-end." },
{ "title": "Smashing Magazine", "url": "https://www.smashingmagazine.com/feed/", "category": "Développement", "lang": "en", "description": "Design et développement web." },
{ "title": "Développez.com", "url": "https://www.developpez.com/index/rss", "category": "Développement", "lang": "fr", "description": "L'actualité des développeurs francophones." },
{ "title": "Human Coders News", "url": "https://news.humancoders.com/items/feed", "category": "Développement", "lang": "fr", "description": "Veille dev partagée par la communauté francophone." },
{ "title": "IT-Connect", "url": "https://www.it-connect.fr/feed/", "category": "IT / Sysadmin", "lang": "fr", "description": "Tutoriels et actus sysadmin, Windows et Linux." },
{ "title": "ZDNet France", "url": "https://www.zdnet.fr/feeds/rss/actualites/", "category": "IT / Sysadmin", "lang": "fr", "description": "L'actualité IT pour les professionnels." },
{ "title": "Silicon", "url": "https://www.silicon.fr/feed", "category": "IT / Sysadmin", "lang": "fr", "description": "L'actualité IT et cloud pour les DSI." },
{ "title": "The Register", "url": "https://www.theregister.com/headlines.atom", "category": "IT / Sysadmin", "lang": "en", "description": "L'IT sans langue de bois." },
{ "title": "ServeTheHome", "url": "https://www.servethehome.com/feed/", "category": "IT / Sysadmin", "lang": "en", "description": "Serveurs, stockage, réseau et homelab." },
{ "title": "Phoronix", "url": "https://www.phoronix.com/rss.php", "category": "Open source", "lang": "en", "description": "Linux, matériel et open source." },
{ "title": "LinuxFr", "url": "https://linuxfr.org/news.atom", "category": "Open source", "lang": "fr", "description": "L'actualité du logiciel libre en français." },
{ "title": "It's FOSS", "url": "https://itsfoss.com/rss/", "category": "Open source", "lang": "en", "description": "Tutoriels et actus Linux/open source." },
{ "title": "CERT-FR", "url": "https://www.cert.ssi.gouv.fr/feed/", "category": "Sécurité", "lang": "fr", "description": "Alertes et avis de sécurité de l'ANSSI." },
{ "title": "Krebs on Security", "url": "https://krebsonsecurity.com/feed/", "category": "Sécurité", "lang": "en", "description": "Investigations cybersécurité de Brian Krebs." },
{ "title": "Zataz", "url": "https://www.zataz.com/feed/", "category": "Sécurité", "lang": "fr", "description": "Actualité cybersécurité et fuites de données." },
{ "title": "Futura Sciences", "url": "https://www.futura-sciences.com/rss/actualites.xml", "category": "Sciences", "lang": "fr", "description": "Actualités scientifiques tous domaines." },
{ "title": "CNRS Le Journal", "url": "https://lejournal.cnrs.fr/rss", "category": "Sciences", "lang": "fr", "description": "Le journal de la recherche française." },
{ "title": "Gamekult", "url": "https://www.gamekult.com/feed.xml", "category": "Jeux vidéo", "lang": "fr", "description": "Actualité et critiques jeux vidéo." },
{ "title": "Rock Paper Shotgun", "url": "https://www.rockpapershotgun.com/feed", "category": "Jeux vidéo", "lang": "en", "description": "Jeux PC, tests et actus." }
{
"title": "Le Monde",
"url": "https://www.lemonde.fr/rss/une.xml",
"category": "Actualités",
"lang": "fr",
"description": "La une du journal Le Monde."
},
{
"title": "France Info",
"url": "https://www.franceinfo.fr/titres.rss",
"category": "Actualités",
"lang": "fr",
"description": "Les titres de France Info en continu."
},
{
"title": "Courrier International",
"url": "https://www.courrierinternational.com/feed/all/rss.xml",
"category": "Actualités",
"lang": "fr",
"description": "L'actualité internationale vue par la presse étrangère."
},
{
"title": "BBC News",
"url": "https://feeds.bbci.co.uk/news/rss.xml",
"category": "Actualités",
"lang": "en",
"description": "Fil d'actualité mondial de la BBC."
},
{
"title": "Korben",
"url": "https://korben.info/feed",
"category": "Tech",
"lang": "fr",
"description": "Actu tech, astuces et bidouilles."
},
{
"title": "Numerama",
"url": "https://www.numerama.com/feed/",
"category": "Tech",
"lang": "fr",
"description": "Société numérique, tech et sciences."
},
{
"title": "Next",
"url": "https://next.ink/feed/",
"category": "Tech",
"lang": "fr",
"description": "Actualité informatique et numérique de fond."
},
{
"title": "Journal du hacker",
"url": "https://www.journalduhacker.net/rss",
"category": "Tech",
"lang": "fr",
"description": "Liens tech partagés par la communauté francophone."
},
{
"title": "Ars Technica",
"url": "https://feeds.arstechnica.com/arstechnica/index",
"category": "Tech",
"lang": "en",
"description": "Analyses tech approfondies."
},
{
"title": "Hacker News",
"url": "https://news.ycombinator.com/rss",
"category": "Tech",
"lang": "en",
"description": "Les liens les plus discutés de la communauté HN."
},
{
"title": "The Verge",
"url": "https://www.theverge.com/rss/index.xml",
"category": "Tech",
"lang": "en",
"description": "Tech, science et culture numérique."
},
{
"title": "Dev.to",
"url": "https://dev.to/feed",
"category": "Développement",
"lang": "en",
"description": "Articles de la communauté dev.to."
},
{
"title": "GitHub Blog",
"url": "https://github.blog/feed/",
"category": "Développement",
"lang": "en",
"description": "Nouveautés et ingénierie GitHub."
},
{
"title": "Laravel News",
"url": "https://feed.laravel-news.com/",
"category": "Développement",
"lang": "en",
"description": "L'écosystème Laravel au quotidien."
},
{
"title": "Stack Overflow Blog",
"url": "https://stackoverflow.blog/feed/",
"category": "Développement",
"lang": "en",
"description": "Le blog officiel de Stack Overflow."
},
{
"title": "freeCodeCamp",
"url": "https://www.freecodecamp.org/news/rss/",
"category": "Développement",
"lang": "en",
"description": "Tutoriels et articles pour apprendre à coder."
},
{
"title": "CSS-Tricks",
"url": "https://css-tricks.com/feed/",
"category": "Développement",
"lang": "en",
"description": "Tout sur le CSS et le front-end."
},
{
"title": "Smashing Magazine",
"url": "https://www.smashingmagazine.com/feed/",
"category": "Développement",
"lang": "en",
"description": "Design et développement web."
},
{
"title": "Développez.com",
"url": "https://www.developpez.com/index/rss",
"category": "Développement",
"lang": "fr",
"description": "L'actualité des développeurs francophones."
},
{
"title": "Human Coders News",
"url": "https://news.humancoders.com/items/feed",
"category": "Développement",
"lang": "fr",
"description": "Veille dev partagée par la communauté francophone."
},
{
"title": "IT-Connect",
"url": "https://www.it-connect.fr/feed/",
"category": "IT / Sysadmin",
"lang": "fr",
"description": "Tutoriels et actus sysadmin, Windows et Linux."
},
{
"title": "ZDNet France",
"url": "https://www.zdnet.fr/feeds/rss/actualites/",
"category": "IT / Sysadmin",
"lang": "fr",
"description": "L'actualité IT pour les professionnels."
},
{
"title": "Silicon",
"url": "https://www.silicon.fr/feed",
"category": "IT / Sysadmin",
"lang": "fr",
"description": "L'actualité IT et cloud pour les DSI."
},
{
"title": "The Register",
"url": "https://www.theregister.com/headlines.atom",
"category": "IT / Sysadmin",
"lang": "en",
"description": "L'IT sans langue de bois."
},
{
"title": "ServeTheHome",
"url": "https://www.servethehome.com/feed/",
"category": "IT / Sysadmin",
"lang": "en",
"description": "Serveurs, stockage, réseau et homelab."
},
{
"title": "Phoronix",
"url": "https://www.phoronix.com/rss.php",
"category": "Open source",
"lang": "en",
"description": "Linux, matériel et open source."
},
{
"title": "LinuxFr",
"url": "https://linuxfr.org/news.atom",
"category": "Open source",
"lang": "fr",
"description": "L'actualité du logiciel libre en français."
},
{
"title": "It's FOSS",
"url": "https://itsfoss.com/rss/",
"category": "Open source",
"lang": "en",
"description": "Tutoriels et actus Linux/open source."
},
{
"title": "CERT-FR",
"url": "https://www.cert.ssi.gouv.fr/feed/",
"category": "Sécurité",
"lang": "fr",
"description": "Alertes et avis de sécurité de l'ANSSI."
},
{
"title": "Krebs on Security",
"url": "https://krebsonsecurity.com/feed/",
"category": "Sécurité",
"lang": "en",
"description": "Investigations cybersécurité de Brian Krebs."
},
{
"title": "Zataz",
"url": "https://www.zataz.com/feed/",
"category": "Sécurité",
"lang": "fr",
"description": "Actualité cybersécurité et fuites de données."
},
{
"title": "Futura Sciences",
"url": "https://www.futura-sciences.com/rss/actualites.xml",
"category": "Sciences",
"lang": "fr",
"description": "Actualités scientifiques tous domaines."
},
{
"title": "CNRS Le Journal",
"url": "https://lejournal.cnrs.fr/rss",
"category": "Sciences",
"lang": "fr",
"description": "Le journal de la recherche française."
},
{
"title": "Gamekult",
"url": "https://www.gamekult.com/feed.xml",
"category": "Jeux vidéo",
"lang": "fr",
"description": "Actualité et critiques jeux vidéo."
},
{
"title": "Rock Paper Shotgun",
"url": "https://www.rockpapershotgun.com/feed",
"category": "Jeux vidéo",
"lang": "en",
"description": "Jeux PC, tests et actus."
},
{
"title": "Le Figaro",
"url": "https://www.lefigaro.fr/rss/figaro_actualites.xml",
"category": "Actualités",
"lang": "fr",
"description": "L'actualité en continu du Figaro."
},
{
"title": "France 24",
"url": "https://www.france24.com/fr/rss",
"category": "Actualités",
"lang": "fr",
"description": "L'actualité internationale en français."
},
{
"title": "The New York Times",
"url": "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
"category": "Actualités",
"lang": "en",
"description": "La une du New York Times (États-Unis)."
},
{
"title": "NPR",
"url": "https://feeds.npr.org/1001/rss.xml",
"category": "Actualités",
"lang": "en",
"description": "La radio publique américaine."
},
{
"title": "The Guardian",
"url": "https://www.theguardian.com/world/rss",
"category": "Actualités",
"lang": "en",
"description": "L'actualité mondiale du Guardian (Royaume-Uni)."
},
{
"title": "Al Jazeera English",
"url": "https://www.aljazeera.com/xml/rss/all.xml",
"category": "Actualités",
"lang": "en",
"description": "L'actualité mondiale vue du Qatar."
},
{
"title": "CBC News",
"url": "https://www.cbc.ca/webfeed/rss/rss-topstories",
"category": "Actualités",
"lang": "en",
"description": "Les grands titres canadiens."
},
{
"title": "ABC News Australia",
"url": "https://www.abc.net.au/news/feed/51120/rss.xml",
"category": "Actualités",
"lang": "en",
"description": "L'actualité australienne."
},
{
"title": "El País",
"url": "https://feeds.elpais.com/mrss-s/pages/ep/site/elpais.com/portada",
"category": "Actualités",
"lang": "es",
"description": "La portada d'El País (Espagne)."
},
{
"title": "El Mundo",
"url": "https://e00-elmundo.uecdn.es/elmundo/rss/portada.xml",
"category": "Actualités",
"lang": "es",
"description": "L'actualité espagnole d'El Mundo."
},
{
"title": "Der Spiegel",
"url": "https://www.spiegel.de/schlagzeilen/index.rss",
"category": "Actualités",
"lang": "de",
"description": "Les gros titres du Spiegel (Allemagne)."
},
{
"title": "Die Zeit",
"url": "https://newsfeed.zeit.de/index",
"category": "Actualités",
"lang": "de",
"description": "L'hebdomadaire allemand de référence."
},
{
"title": "Tagesschau",
"url": "https://www.tagesschau.de/index~rss2.xml",
"category": "Actualités",
"lang": "de",
"description": "Le journal télévisé allemand ARD."
},
{
"title": "ANSA",
"url": "https://www.ansa.it/sito/ansait_rss.xml",
"category": "Actualités",
"lang": "it",
"description": "L'agence de presse italienne."
},
{
"title": "La Repubblica",
"url": "https://www.repubblica.it/rss/homepage/rss2.0.xml",
"category": "Actualités",
"lang": "it",
"description": "La une de La Repubblica (Italie)."
},
{
"title": "G1 Globo",
"url": "https://g1.globo.com/rss/g1/",
"category": "Actualités",
"lang": "pt",
"description": "L'actualité brésilienne de Globo."
},
{
"title": "Público",
"url": "https://feeds.publico.pt/rss/home",
"category": "Actualités",
"lang": "pt",
"description": "Le quotidien portugais Público."
},
{
"title": "NU.nl",
"url": "https://www.nu.nl/rss/Algemeen",
"category": "Actualités",
"lang": "nl",
"description": "L'actualité néerlandaise en continu."
},
{
"title": "NHK News",
"url": "https://www3.nhk.or.jp/rss/news/cat0.xml",
"category": "Actualités",
"lang": "ja",
"description": "Les grands titres de la NHK (Japon)."
},
{
"title": "Meduza",
"url": "https://meduza.io/rss/all",
"category": "Actualités",
"lang": "ru",
"description": "Média russe indépendant (en exil)."
},
{
"title": "SVT Nyheter",
"url": "https://www.svt.se/nyheter/rss.xml",
"category": "Actualités",
"lang": "sv",
"description": "L'actualité de la télévision publique suédoise."
},
{
"title": "The Hindu",
"url": "https://www.thehindu.com/news/national/feeder/default.rss",
"category": "Actualités",
"lang": "en",
"description": "L'actualité nationale indienne."
},
{
"title": "Times of India",
"url": "https://timesofindia.indiatimes.com/rssfeedstopstories.cms",
"category": "Actualités",
"lang": "en",
"description": "Les grands titres du Times of India."
},
{
"title": "Wired",
"url": "https://www.wired.com/feed/rss",
"category": "Tech",
"lang": "en",
"description": "Tech, science et société (États-Unis)."
},
{
"title": "TechCrunch",
"url": "https://techcrunch.com/feed/",
"category": "Tech",
"lang": "en",
"description": "Startups et industrie tech."
},
{
"title": "Xataka",
"url": "https://www.xataka.com/feedburner.xml",
"category": "Tech",
"lang": "es",
"description": "Le grand média tech hispanophone."
},
{
"title": "Genbeta",
"url": "https://www.genbeta.com/feedburner.xml",
"category": "Tech",
"lang": "es",
"description": "Logiciels et web (Espagne)."
},
{
"title": "heise online",
"url": "https://www.heise.de/rss/heise-atom.xml",
"category": "Tech",
"lang": "de",
"description": "La référence tech allemande."
},
{
"title": "Golem.de",
"url": "https://rss.golem.de/rss.php?feed=RSS2.0",
"category": "Tech",
"lang": "de",
"description": "IT et tech pour professionnels (Allemagne)."
},
{
"title": "HDblog",
"url": "https://www.hdblog.it/feed/",
"category": "Tech",
"lang": "it",
"description": "Tech et mobile (Italie)."
},
{
"title": "Tecnoblog",
"url": "https://tecnoblog.net/feed/",
"category": "Tech",
"lang": "pt",
"description": "La tech vue du Brésil."
},
{
"title": "Tweakers",
"url": "https://feeds.feedburner.com/tweakers/mixed",
"category": "Tech",
"lang": "nl",
"description": "La communauté tech néerlandaise."
},
{
"title": "ITmedia",
"url": "https://rss.itmedia.co.jp/rss/2.0/news_bursts.xml",
"category": "Tech",
"lang": "ja",
"description": "L'actualité IT japonaise."
},
{
"title": "Niebezpiecznik",
"url": "https://niebezpiecznik.pl/feed/",
"category": "Sécurité",
"lang": "pl",
"description": "La cybersécurité vue de Pologne."
},
{
"title": "Nature",
"url": "https://www.nature.com/nature.rss",
"category": "Sciences",
"lang": "en",
"description": "La revue scientifique de référence."
},
{
"title": "NASA",
"url": "https://www.nasa.gov/feed/",
"category": "Sciences",
"lang": "en",
"description": "Les actualités de la NASA."
},
{
"title": "Ars Technica Science",
"url": "https://feeds.arstechnica.com/arstechnica/science",
"category": "Sciences",
"lang": "en",
"description": "La rubrique science d'Ars Technica."
},
{
"title": "L'Équipe",
"url": "https://dwh.lequipe.fr/api/edito/rss?path=/",
"category": "Sport",
"lang": "fr",
"description": "Toute l'actualité sportive française."
},
{
"title": "ESPN",
"url": "https://www.espn.com/espn/rss/news",
"category": "Sport",
"lang": "en",
"description": "Le sport américain et mondial."
},
{
"title": "Marca",
"url": "https://e00-marca.uecdn.es/rss/portada.xml",
"category": "Sport",
"lang": "es",
"description": "Le quotidien sportif espagnol."
},
{
"title": "kicker",
"url": "https://newsfeed.kicker.de/news/aktuell",
"category": "Sport",
"lang": "de",
"description": "Le football allemand et international."
},
{
"title": "Open Culture",
"url": "https://www.openculture.com/feed",
"category": "Culture",
"lang": "en",
"description": "Culture libre : cours, films et livres gratuits."
}
]
}
}
+5 -5
View File
@@ -6,7 +6,7 @@
<div class="form-status">{{ session('status') }}</div>
@endif
<a href="{{ url()->previous(route('dashboard')) }}" class="back-link">&larr; Retour aux articles</a>
<a href="{{ url()->previous(route('dashboard')) }}" class="back-link">&larr; {{ __('app.back_to_articles') }}</a>
<article class="card reader-article">
<h1 class="reader-title">{{ $article->title }}</h1>
@@ -15,15 +15,15 @@
@if ($article->published_at)
<span>&middot; {{ $article->published_at->diffForHumans() }}</span>
@endif
<a href="{{ $article->link }}" target="_blank" rel="noopener" class="reader-source-link">Voir la source originale &#8599;</a>
<a href="{{ $article->link }}" target="_blank" rel="noopener" class="reader-source-link">{{ __('app.view_original') }} &#8599;</a>
</div>
@if ($article->full_content_error)
<div class="reader-error">
<p>Impossible de récupérer le contenu complet de cet article ({{ $article->full_content_error }}).</p>
<p>{{ __('app.fetch_error', ['error' => $article->full_content_error]) }}</p>
<form method="POST" action="{{ route('articles.retry', $article) }}">
@csrf
<button type="submit" class="btn btn-ghost btn-sm">Réessayer</button>
<button type="submit" class="btn btn-ghost btn-sm">{{ __('app.retry') }}</button>
</form>
@if ($article->excerpt)
<p class="mt-2">{{ $article->excerpt }}</p>
@@ -32,7 +32,7 @@
@elseif ($contentHtml)
<div class="prose">{!! $contentHtml !!}</div>
@else
<p class="text-muted">Récupération du contenu en cours…</p>
<p class="text-muted">{{ __('app.fetching') }}</p>
@endif
</article>
</div>
+9 -9
View File
@@ -1,4 +1,4 @@
@extends('layouts.guest', ['title' => 'Connexion — BloomFeed'])
@extends('layouts.guest', ['title' => __('app.login_title').' — BloomFeed'])
@section('content')
<div class="auth-card">
@@ -6,8 +6,8 @@
<img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light">
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</div>
<h1>Connexion</h1>
<p class="subtitle">Accédez à vos flux et à votre lecture approfondie</p>
<h1>{{ __('app.login_title') }}</h1>
<p class="subtitle">{{ __('app.login_subtitle') }}</p>
@if (session('status'))
<div class="form-status">{{ session('status') }}</div>
@@ -20,20 +20,20 @@
<form method="POST" action="{{ route('login') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="email">Adresse e-mail</label>
<label for="email">{{ __('app.email') }}</label>
<input type="email" name="email" id="email" class="form-input" value="{{ old('email') }}" required autofocus>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<label for="password">{{ __('app.password') }}</label>
<input type="password" name="password" id="password" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Se connecter</button>
<button type="submit" class="btn btn-primary btn-block">{{ __('app.login') }}</button>
</form>
@unless (\App\Models\User::query()->exists())
@if (! \App\Models\User::query()->exists() || \App\Models\Setting::registrationsOpen())
<div class="switch-link">
Pas encore de compte ? <a href="{{ route('register') }}">Créer un compte</a>
{{ __('app.no_account_yet') }} <a href="{{ route('register') }}">{{ __('app.create_account') }}</a>
</div>
@endunless
@endif
</div>
@endsection
+11 -9
View File
@@ -1,4 +1,6 @@
@extends('layouts.guest', ['title' => 'Créer un compte — BloomFeed'])
@extends('layouts.guest', ['title' => __('app.create_account').' — BloomFeed'])
@php($isFirstUser = ! \App\Models\User::query()->exists())
@section('content')
<div class="auth-card">
@@ -6,8 +8,8 @@
<img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light">
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</div>
<h1>Configurer votre instance</h1>
<p class="subtitle">Ce compte sera le seul compte propriétaire de cette instance BloomFeed</p>
<h1>{{ $isFirstUser ? __('app.register_owner_title') : __('app.register_title') }}</h1>
<p class="subtitle">{{ $isFirstUser ? __('app.register_owner_subtitle') : __('app.register_subtitle') }}</p>
@if ($errors->any())
<div class="form-error mt-1">{{ $errors->first() }}</div>
@@ -16,26 +18,26 @@
<form method="POST" action="{{ route('register') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="name">Nom</label>
<label for="name">{{ __('app.name') }}</label>
<input type="text" name="name" id="name" class="form-input" value="{{ old('name') }}" required autofocus>
</div>
<div class="form-group">
<label for="email">Adresse e-mail</label>
<label for="email">{{ __('app.email') }}</label>
<input type="email" name="email" id="email" class="form-input" value="{{ old('email') }}" required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<label for="password">{{ __('app.password') }}</label>
<input type="password" name="password" id="password" class="form-input" required>
</div>
<div class="form-group">
<label for="password_confirmation">Confirmer le mot de passe</label>
<label for="password_confirmation">{{ __('app.confirm_password') }}</label>
<input type="password" name="password_confirmation" id="password_confirmation" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Créer mon compte</button>
<button type="submit" class="btn btn-primary btn-block">{{ __('app.register_button') }}</button>
</form>
<div class="switch-link">
Déjà un compte ? <a href="{{ route('login') }}">Se connecter</a>
{{ __('app.already_account') }} <a href="{{ route('login') }}">{{ __('app.login') }}</a>
</div>
</div>
@endsection
+25 -10
View File
@@ -1,4 +1,4 @@
@extends('layouts.app', ['title' => 'Catalogue — BloomFeed'])
@extends('layouts.app', ['title' => __('app.catalog').' — BloomFeed'])
@section('content')
<div class="main-column" style="width:100%;">
@@ -8,22 +8,37 @@
<div class="market-header">
<div>
<h2 style="margin:0;">Catalogue de flux</h2>
<h2 style="margin:0;">{{ __('app.catalog_title') }}</h2>
<p class="text-muted text-sm" style="margin:4px 0 0;">
Une sélection de flux RSS livrée avec BloomFeed ajoutez-les à votre collection en un clic.
{{ __('app.catalog_lede') }}
</p>
</div>
</div>
@if ($feeds->isEmpty())
<div class="card empty-state">
Le catalogue est vide pour le moment.
{{ __('app.catalog_empty') }}
</div>
@else
<input type="search" id="market-search" class="form-input market-search" placeholder="Rechercher un flux… (nom, description, catégorie)">
@php($languageNames = [
'fr' => 'Français', 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch',
'it' => 'Italiano', 'pt' => 'Português', 'nl' => 'Nederlands', 'ja' => '日本語',
'ru' => 'Русский', 'pl' => 'Polski', 'sv' => 'Svenska',
])
@php($languages = $feeds->flatten(1)->pluck('lang')->unique()->sort()->values())
<div class="market-toolbar">
<input type="search" id="market-search" class="form-input market-search" placeholder="{{ __('app.search_placeholder') }}">
<select id="market-lang" class="form-input market-lang-select">
<option value="">{{ __('app.all_languages') }}</option>
@foreach ($languages as $lang)
<option value="{{ $lang }}">{{ $languageNames[$lang] ?? strtoupper($lang) }}</option>
@endforeach
</select>
</div>
<div class="market-chips" id="market-chips">
<button type="button" class="chip active" data-category="">Tout</button>
<button type="button" class="chip active" data-category="">{{ __('app.all') }}</button>
@foreach ($feeds->keys() as $category)
<button type="button" class="chip" data-category="{{ $category }}">{{ $category }}</button>
@endforeach
@@ -32,7 +47,7 @@
<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" data-category="{{ $category }}" data-lang="{{ $item['lang'] }}" 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>
@@ -41,13 +56,13 @@
<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>
<span class="market-added"> {{ __('app.added') }}</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>
<button type="submit" class="btn btn-primary btn-sm">+ {{ __('app.add') }}</button>
</form>
@endif
</div>
@@ -56,7 +71,7 @@
@endforeach
</div>
<div class="card empty-state" id="market-empty" hidden>Aucun flux ne correspond à votre recherche.</div>
<div class="card empty-state" id="market-empty" hidden>{{ __('app.no_match') }}</div>
@endif
</div>
@endsection
+5 -5
View File
@@ -1,4 +1,4 @@
@extends('layouts.app', ['title' => 'Articles — BloomFeed'])
@extends('layouts.app', ['title' => __('app.articles').' — BloomFeed'])
@section('content')
<div class="sidebar">
@@ -11,11 +11,11 @@
@endif
<div class="flex-between">
<h2 style="margin:0;">Derniers articles</h2>
<h2 style="margin:0;">{{ __('app.latest_articles') }}</h2>
<div class="flex" style="gap:8px;">
<a href="{{ route('dashboard', array_filter(['feed' => request('feed'), 'unread' => request('unread') ? null : 1])) }}"
class="btn btn-ghost btn-sm">
{{ request('unread') ? 'Tout afficher' : 'Non lus uniquement' }}
{{ request('unread') ? __('app.show_all') : __('app.unread_only') }}
</a>
</div>
</div>
@@ -25,9 +25,9 @@
@empty
<div class="card empty-state">
@if ($feeds->isEmpty())
Ajoutez votre premier flux RSS depuis la barre latérale pour commencer.
{{ __('app.empty_no_feeds') }}
@else
Aucun article pour le moment. Essayez d'actualiser vos flux.
{{ __('app.empty_no_articles') }}
@endif
</div>
@endforelse
+11 -11
View File
@@ -1,4 +1,4 @@
@extends('layouts.app', ['title' => 'Mes flux — BloomFeed'])
@extends('layouts.app', ['title' => __('app.my_feeds').' — BloomFeed'])
@section('content')
<div class="main-column" style="width:100%;">
@@ -7,19 +7,19 @@
@endif
<div class="card">
<h2>Ajouter un flux</h2>
<h2>{{ __('app.add_feed') }}</h2>
<form method="POST" action="{{ route('feeds.store') }}" class="inline-form">
@csrf
<input type="url" name="url" class="form-input" placeholder="https://exemple.com/rss.xml" required>
<input type="text" name="title" class="form-input" placeholder="Titre personnalisé (optionnel)" style="max-width:220px;">
<button type="submit" class="btn btn-primary">Ajouter</button>
<input type="url" name="url" class="form-input" placeholder="{{ __('app.feed_url_placeholder') }}" required>
<input type="text" name="title" class="form-input" placeholder="{{ __('app.custom_title_optional') }}" style="max-width:220px;">
<button type="submit" class="btn btn-primary">{{ __('app.add') }}</button>
</form>
@error('url')
<div class="form-error mt-1">{{ $message }}</div>
@enderror
</div>
<h2 class="mt-3">Mes flux</h2>
<h2 class="mt-3">{{ __('app.my_feeds') }}</h2>
@forelse ($feeds as $feed)
<div class="card flex-between">
@@ -27,9 +27,9 @@
<div style="font-weight:600;">{{ $feed->displayTitle() }}</div>
<div class="text-sm text-muted">{{ $feed->url }}</div>
<div class="text-sm text-muted mt-1">
{{ $feed->articles_count }} article(s)
{{ __('app.articles_count', ['count' => $feed->articles_count]) }}
@if ($feed->last_fetched_at)
&middot; actualisé {{ $feed->last_fetched_at->diffForHumans() }}
&middot; {{ __('app.refreshed_ago', ['time' => $feed->last_fetched_at->diffForHumans()]) }}
@endif
@if ($feed->last_fetch_status && $feed->last_fetch_status !== 'ok')
&middot; <span style="color:var(--danger);">{{ $feed->last_fetch_status }}</span>
@@ -39,17 +39,17 @@
<div class="flex" style="gap:8px;">
<form method="POST" action="{{ route('feeds.refresh', $feed) }}">
@csrf
<button type="submit" class="btn btn-ghost btn-sm">Actualiser</button>
<button type="submit" class="btn btn-ghost btn-sm">{{ __('app.refresh') }}</button>
</form>
<form method="POST" action="{{ route('feeds.destroy', $feed) }}">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm" data-confirm="Supprimer ce flux et tous ses articles ?">Supprimer</button>
<button type="submit" class="btn btn-danger btn-sm" data-confirm="{{ __('app.confirm_delete_feed') }}">{{ __('app.delete') }}</button>
</form>
</div>
</div>
@empty
<div class="card empty-state">Vous n'avez pas encore ajouté de flux.</div>
<div class="card empty-state">{{ __('app.no_feeds_yet') }}</div>
@endforelse
</div>
@endsection
+14 -17
View File
@@ -1,21 +1,18 @@
@extends('layouts.marketing', ['title' => 'BloomFeed — votre agrégateur RSS auto-hébergé'])
@extends('layouts.marketing', ['title' => 'BloomFeed'])
@section('content')
<section class="hero">
<div class="hero-inner">
<span class="eyebrow">Auto-hébergé &middot; Vie privée</span>
<h1>Vos flux RSS, lus en profondeur, sans jamais quitter BloomFeed.</h1>
<p class="hero-lede">
BloomFeed centralise vos flux préférés et en extrait le contenu complet à la demande,
directement sur votre propre serveur. Aucune donnée envoyée à des tiers.
</p>
<span class="eyebrow">{{ __('app.hero_eyebrow') }}</span>
<h1>{{ __('app.hero_title') }}</h1>
<p class="hero-lede">{{ __('app.hero_lede') }}</p>
<div class="hero-cta">
@if ($hasOwner)
<a href="{{ route('login') }}" class="btn btn-primary btn-lg">Se connecter</a>
@if ($hasOwner && ! \App\Models\Setting::registrationsOpen())
<a href="{{ route('login') }}" class="btn btn-primary btn-lg">{{ __('app.login') }}</a>
@else
<a href="{{ route('register') }}" class="btn btn-primary btn-lg">Créer un compte</a>
<a href="{{ route('register') }}" class="btn btn-primary btn-lg">{{ __('app.create_account') }}</a>
@endif
<a href="{{ route('self-hosting') }}" class="btn btn-ghost btn-lg">Comment l'auto-héberger ?</a>
<a href="{{ route('self-hosting') }}" class="btn btn-ghost btn-lg">{{ __('app.how_to_self_host') }}</a>
</div>
</div>
</section>
@@ -23,18 +20,18 @@
<section class="feature-grid">
<div class="feature-card">
<div class="feature-icon">📰</div>
<h3>Flux personnalisés</h3>
<p>Ajoutez n'importe quel flux RSS ou Atom, organisez-les et suivez vos non-lus en un coup d'œil.</p>
<h3>{{ __('app.feature_feeds_title') }}</h3>
<p>{{ __('app.feature_feeds_text') }}</p>
</div>
<div class="feature-card">
<div class="feature-icon">📖</div>
<h3>Lecture approfondie intégrée</h3>
<p>BloomFeed récupère la page source, en extrait le contenu principal et l'affiche proprement — plus besoin d'ouvrir dix onglets.</p>
<h3>{{ __('app.feature_reader_title') }}</h3>
<p>{{ __('app.feature_reader_text') }}</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<h3>Chez vous, pour vous</h3>
<p>Une instance BloomFeed = un propriétaire. Pas d'inscriptions publiques, pas de tracking, votre serveur, vos données.</p>
<h3>{{ __('app.feature_home_title') }}</h3>
<p>{{ __('app.feature_home_text') }}</p>
</div>
</section>
@endsection
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
+5 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -20,6 +20,10 @@
</script>
</head>
<body>
<div class="guest-topbar">
@include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
</div>
<div class="centered-shell">
@yield('content')
</div>
+10 -9
View File
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'BloomFeed' }}</title>
<meta name="description" content="BloomFeed est un agrégateur RSS personnel et auto-hébergé, avec lecture approfondie intégrée.">
<meta name="description" content="{{ __('app.meta_description') }}">
<link rel="icon" href="{{ asset('images/icon.png') }}">
<link rel="apple-touch-icon" href="{{ asset('images/icon.png') }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -27,12 +27,13 @@
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</a>
<div class="navbar-links">
<a href="{{ route('self-hosting') }}" class="{{ request()->routeIs('self-hosting') ? 'active' : '' }}">Self-hosting</a>
<button type="button" class="theme-toggle" data-theme-toggle title="Changer de thème"></button>
@if (\App\Models\User::query()->exists())
<a href="{{ route('login') }}" class="btn btn-primary btn-sm">Se connecter</a>
<a href="{{ route('self-hosting') }}" class="{{ request()->routeIs('self-hosting') ? 'active' : '' }}">{{ __('app.self_hosting') }}</a>
@include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
@if (\App\Models\User::query()->exists() && ! \App\Models\Setting::registrationsOpen())
<a href="{{ route('login') }}" class="btn btn-primary btn-sm">{{ __('app.login') }}</a>
@else
<a href="{{ route('register') }}" class="btn btn-primary btn-sm">Créer un compte</a>
<a href="{{ route('register') }}" class="btn btn-primary btn-sm">{{ __('app.create_account') }}</a>
@endif
</div>
</nav>
@@ -42,8 +43,8 @@
</main>
<footer class="site-footer">
<span>BloomFeed projet open-source, auto-hébergé.</span>
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">Code source</a>
<span>{{ __('app.footer_tagline') }}</span>
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">{{ __('app.source_code') }}</a>
</footer>
<script src="{{ asset('js/theme-toggle.js') }}" defer></script>
@@ -1,9 +1,9 @@
<div class="card">
<div class="sidebar-title">Flux</div>
<div class="sidebar-title">{{ __('app.feeds_label') }}</div>
<ul class="feed-list">
<li>
<a href="{{ route('dashboard') }}" class="{{ ! request('feed') ? 'active' : '' }}">
Tous les articles
{{ __('app.all_articles') }}
</a>
</li>
@foreach ($feeds as $feed)
@@ -21,18 +21,18 @@
<form method="POST" action="{{ route('feeds.store') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="sidebar-url">Ajouter un flux</label>
<input type="url" name="url" id="sidebar-url" class="form-input" placeholder="https://exemple.com/rss.xml" required>
<label for="sidebar-url">{{ __('app.add_feed') }}</label>
<input type="url" name="url" id="sidebar-url" class="form-input" placeholder="{{ __('app.feed_url_placeholder') }}" required>
</div>
<button type="submit" class="btn btn-primary btn-block btn-sm">Ajouter</button>
<button type="submit" class="btn btn-primary btn-block btn-sm">{{ __('app.add') }}</button>
</form>
<form method="POST" action="{{ route('feeds.refreshAll') }}" class="mt-2">
@csrf
<button type="submit" class="btn btn-ghost btn-block btn-sm">Tout actualiser</button>
<button type="submit" class="btn btn-ghost btn-block btn-sm">{{ __('app.refresh_all') }}</button>
</form>
<a href="{{ route('feeds.index') }}" class="text-sm text-muted mt-1" style="display:block; text-align:center;">
Gérer mes flux
{{ __('app.manage_feeds') }}
</a>
</div>
@@ -0,0 +1,8 @@
<form method="POST" action="{{ route('locale.update') }}" class="locale-form">
@csrf
<select name="locale" class="locale-select" onchange="this.form.submit()" title="{{ __('app.language') }}">
@foreach (['fr' => 'Français', 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch'] as $code => $label)
<option value="{{ $code }}" @selected(app()->getLocale() === $code)>{{ $label }}</option>
@endforeach
</select>
</form>
+9 -5
View File
@@ -4,13 +4,17 @@
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</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>
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">{{ __('app.articles') }}</a>
<a href="{{ route('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">{{ __('app.catalog') }}</a>
<a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">{{ __('app.my_feeds') }}</a>
@if (auth()->user()?->is_owner)
<a href="{{ route('settings') }}" class="{{ request()->routeIs('settings') ? 'active' : '' }}">{{ __('app.settings') }}</a>
@endif
@include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-ghost btn-sm">Déconnexion</button>
<button type="submit" class="btn btn-ghost btn-sm">{{ __('app.logout') }}</button>
</form>
</div>
</nav>
+34 -29
View File
@@ -1,55 +1,60 @@
@extends('layouts.marketing', ['title' => 'Auto-héberger BloomFeed'])
@extends('layouts.marketing', ['title' => __('app.selfhost_title').' — BloomFeed'])
@section('content')
<section class="docs-shell">
<h1>Auto-héberger BloomFeed</h1>
<p class="hero-lede">
BloomFeed est pensé pour être installé sur votre propre serveur, pour vous (ou votre foyer).
Une instance = un seul compte propriétaire. Tout tourne dans Docker : l'application
et la base de données MariaDB.
</p>
<h1>{{ __('app.selfhost_title') }}</h1>
<p class="hero-lede">{{ __('app.selfhost_intro') }}</p>
<div class="card mt-3">
<h2>Prérequis</h2>
<h2>{{ __('app.prereq_title') }}</h2>
<ul class="docs-list">
<li>Un serveur Linux avec <strong>Docker</strong> et le plugin <strong>Docker Compose</strong>.</li>
<li>Le port <strong>7081</strong> libre (modifiable dans <code>docker-compose.yml</code>).</li>
<li>Optionnel : un nom de domaine et un reverse proxy si vous voulez du HTTPS.</li>
<li>{{ __('app.prereq_docker') }}</li>
<li>{{ __('app.prereq_port') }}</li>
<li>{{ __('app.prereq_proxy') }}</li>
</ul>
</div>
<div class="card mt-2">
<h2>Installation</h2>
<h2>{{ __('app.install_title') }}</h2>
<ol class="docs-list docs-list-numbered">
<li>Clonez le dépôt sur votre serveur.</li>
<li>
Lancez le script de déploiement :
<pre class="code-block">bash scriptsite.sh</pre>
Il crée automatiquement un fichier <code>.env</code> (à partir de <code>.env.example</code>) et y génère
des secrets aléatoires (<code>APP_KEY</code>, mots de passe de base de données) s'ils sont absents
ils ne sont jamais régénérés lors des déploiements suivants.
{{ __('app.install_step_clone') }}
<pre class="code-block">git clone https://git.eldorianet.work/esteban/BloomFeed-RSS bloomfeed
cd bloomfeed</pre>
</li>
<li>Le script construit les images et démarre les conteneurs (application, planificateur, MariaDB).</li>
<li>Ouvrez <code>http://votre-serveur:7081</code> et créez le compte propriétaire cette option disparaît ensuite définitivement.</li>
<li>
{{ __('app.install_step_script') }}
<pre class="code-block">bash scriptsite.sh</pre>
{{ __('app.install_env_note') }}
</li>
<li>{{ __('app.install_step_containers') }}</li>
<li>{{ __('app.install_step_owner') }}</li>
</ol>
</div>
<div class="card mt-2">
<h2>Mettre à jour</h2>
<p>Relancez simplement <code>bash scriptsite.sh</code> : il récupère les derniers changements et reconstruit la stack sans toucher à vos données.</p>
<h2>{{ __('app.config_title') }}</h2>
<ul class="docs-list">
<li>{{ __('app.config_registrations') }}</li>
<li>{{ __('app.config_language') }}</li>
<li>{{ __('app.config_catalog') }}</li>
</ul>
</div>
<div class="card mt-2">
<h2>Désinstaller</h2>
<p>
Le script <code>uninstall.sh</code> arrête et supprime proprement la stack Docker.
Il demande une confirmation explicite avant de supprimer vos données (base de données)
et ne supprime jamais le code source sans confirmation séparée.
</p>
<h2>{{ __('app.update_title') }}</h2>
<p>{{ __('app.update_text') }}</p>
<pre class="code-block">bash scriptsite.sh</pre>
</div>
<div class="card mt-2">
<h2>Code source</h2>
<h2>{{ __('app.uninstall_title') }}</h2>
<p>{{ __('app.uninstall_text') }}</p>
<pre class="code-block">bash uninstall.sh</pre>
</div>
<div class="card mt-2">
<h2>{{ __('app.source_title') }}</h2>
<p>
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">git.eldorianet.work/esteban/BloomFeed-RSS</a>
</p>
+106
View File
@@ -0,0 +1,106 @@
@extends('layouts.app', ['title' => __('app.settings').' — BloomFeed'])
@section('content')
<div class="main-column" style="width:100%; max-width:720px; margin:0 auto;">
@if (session('status'))
<div class="form-status">{{ session('status') }}</div>
@endif
<h2 style="margin:0;">{{ __('app.settings_title') }}</h2>
<div class="card">
<h3>{{ __('app.registrations_section') }}</h3>
<p class="text-muted text-sm">{{ __('app.registrations_desc') }}</p>
<div class="flex-between mt-2">
<span class="text-sm" style="font-weight:600; color: {{ $registrationsOpen ? 'var(--green)' : 'var(--text-muted)' }};">
{{ $registrationsOpen ? __('app.registrations_state_open') : __('app.registrations_state_closed') }}
</span>
<form method="POST" action="{{ route('settings.toggleRegistrations') }}">
@csrf
<button type="submit" class="btn {{ $registrationsOpen ? 'btn-danger' : 'btn-primary' }} btn-sm">
{{ $registrationsOpen ? __('app.close_registrations') : __('app.open_registrations') }}
</button>
</form>
</div>
</div>
<div class="card">
<h3>{{ __('app.create_user_section') }}</h3>
<p class="text-muted text-sm">{{ __('app.create_user_desc') }}</p>
@if ($errors->any())
<div class="form-error mt-1">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('settings.createUser') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="new-name">{{ __('app.name') }}</label>
<input type="text" name="name" id="new-name" class="form-input" value="{{ old('name') }}" required>
</div>
<div class="form-group">
<label for="new-email">{{ __('app.email') }}</label>
<input type="email" name="email" id="new-email" class="form-input" value="{{ old('email') }}" required>
</div>
<div class="form-group">
<label for="new-password">{{ __('app.password') }}</label>
<input type="text" name="password" id="new-password" class="form-input" minlength="8" required autocomplete="off">
</div>
<button type="submit" class="btn btn-primary">{{ __('app.create_account') }}</button>
</form>
</div>
<div class="card">
<h3>{{ __('app.catalog_sources_section') }}</h3>
<p class="text-muted text-sm">{{ __('app.catalog_sources_desc') }}</p>
@error('source_url')
<div class="form-error mt-1">{{ $message }}</div>
@enderror
@if (empty($catalogSources))
<p class="text-muted text-sm mt-2">{{ __('app.no_sources') }}</p>
@else
<ul class="feed-list mt-2">
@foreach ($catalogSources as $source)
<li style="display:flex; align-items:center; justify-content:space-between; gap:12px; padding:8px 10px;">
<span class="text-sm" style="overflow-wrap:anywhere;">{{ $source }}</span>
<form method="POST" action="{{ route('settings.removeSource') }}">
@csrf
@method('DELETE')
<input type="hidden" name="source_url" value="{{ $source }}">
<button type="submit" class="btn btn-danger btn-sm">{{ __('app.remove') }}</button>
</form>
</li>
@endforeach
</ul>
@endif
<form method="POST" action="{{ route('settings.addSource') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="source-url">{{ __('app.source_url_label') }}</label>
<input type="url" name="source_url" id="source-url" class="form-input" placeholder="https://git.example.com/user/repo/raw/branch/main/bloomflux.json" required>
</div>
<button type="submit" class="btn btn-primary btn-sm">{{ __('app.add_source') }}</button>
</form>
</div>
<div class="card">
<h3>{{ __('app.users_section') }}</h3>
<ul class="feed-list mt-1">
@foreach ($users as $user)
<li style="display:flex; align-items:center; justify-content:space-between; padding:8px 10px;">
<span>
<strong>{{ $user->name }}</strong>
<span class="text-muted text-sm">&middot; {{ $user->email }}</span>
</span>
@if ($user->is_owner)
<span class="pill">{{ __('app.owner_badge') }}</span>
@endif
</li>
@endforeach
</ul>
</div>
</div>
@endsection