new design
Todas las comprobaciones han sido exitosas
continuous-integration/drone/push Build is passing
Todas las comprobaciones han sido exitosas
continuous-integration/drone/push Build is passing
Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
@@ -1,6 +1,5 @@
|
||||
module.exports = (app, client) => {
|
||||
const constant = require('../constant'),
|
||||
zlib = require('zlib'),
|
||||
asyncHandler = require('express-async-handler'),
|
||||
{ param, validationResult } = require('express-validator')
|
||||
/**
|
||||
@@ -513,33 +512,4 @@ module.exports = (app, client) => {
|
||||
res.status(404).end()
|
||||
}
|
||||
}))
|
||||
/**
|
||||
* @swagger
|
||||
* /api/download_index:
|
||||
* get:
|
||||
* summary: Retrieve all content of ElasticSearch index.
|
||||
* description: Retrieve all content of ElasticSearch index.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: A file compressed with gzip.
|
||||
* content:
|
||||
* application/gzip:
|
||||
*/
|
||||
app.get('/api/download_index', asyncHandler(async (req, res) => {
|
||||
try {
|
||||
res.setHeader('Content-Type', 'application/gzip')
|
||||
res.setHeader('Content-disposition', 'attachment; filename=fediblock-index.json.gz')
|
||||
const result = await client.search({
|
||||
index: constant.index,
|
||||
size: 9999,
|
||||
query: {
|
||||
match_all: {}
|
||||
}
|
||||
}, { asStream: true, meta: false })
|
||||
result.pipe(zlib.createGzip()).pipe(res)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
res.status(404).end()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"cra-template": "1.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"html2canvas": "^1.4.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
@@ -1,405 +1,611 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--background-color: #212529;
|
||||
--color: #fefefe;
|
||||
--instance-list: #333333;
|
||||
--footer: #cccccc;
|
||||
--bg: #05060a;
|
||||
--bg-soft: #0f1118;
|
||||
--panel: rgba(255, 255, 255, 0.04);
|
||||
--panel-strong: rgba(255, 255, 255, 0.08);
|
||||
--text: #f7f7fb;
|
||||
--muted: #a7adc7;
|
||||
--accent: #7ef3e4;
|
||||
--accent-strong: #5ad1ff;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--background-color: #f5f5f5;
|
||||
--color: #333333;
|
||||
--instance-list: #f0f0f0;
|
||||
--footer: #666666;
|
||||
--bg: #f6f7fb;
|
||||
--bg-soft: #ffffff;
|
||||
--panel: rgba(15, 17, 24, 0.04);
|
||||
--panel-strong: rgba(15, 17, 24, 0.08);
|
||||
--text: #0f1118;
|
||||
--muted: #5a6078;
|
||||
--accent: #0066ff;
|
||||
--accent-strong: #111dff;
|
||||
--border: rgba(15, 17, 24, 0.1);
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
text-align: center;
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
margin: 0;
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
h1,
|
||||
h3,
|
||||
h4,
|
||||
.scan {
|
||||
margin: 0 auto;
|
||||
color: var(--color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--footer);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a,
|
||||
a:hover {
|
||||
color: var(--color);
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 10px;
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
padding: 2rem clamp(1rem, 4vw, 4rem) 3rem;
|
||||
background: radial-gradient(circle at top, rgba(126, 243, 228, 0.15), transparent 45%), var(--bg);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.content-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.content-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.title-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
padding: 1.75rem;
|
||||
border-radius: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(126, 243, 228, 0.15), rgba(90, 209, 255, 0.05));
|
||||
border: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title-stack {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-card h1 {
|
||||
margin: 0.2rem 0 0.35rem;
|
||||
font-size: clamp(2rem, 5vw, 3.4rem);
|
||||
}
|
||||
|
||||
.ghost-button,
|
||||
.secondary-button,
|
||||
.footer-actions button,
|
||||
.footer-actions a,
|
||||
.ticker-chip,
|
||||
.suggestions button,
|
||||
.instance-card__header button,
|
||||
.instance-card__import button,
|
||||
.modal-actions button,
|
||||
.search-panel__header .ghost-button {
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.ghost-button {
|
||||
border-color: var(--border);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.ghost-button:hover,
|
||||
.secondary-button:hover,
|
||||
.footer-actions button:hover,
|
||||
.footer-actions a:hover,
|
||||
.ticker-chip:hover,
|
||||
.suggestions button:hover,
|
||||
.instance-card__import button:hover,
|
||||
.modal-actions button:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--panel-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ticker-card,
|
||||
.stats-card,
|
||||
.search-panel,
|
||||
.scan-card,
|
||||
.instance-card,
|
||||
.instance-card__import {
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.ticker-card__header {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ticker-card__title small {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ticker-card__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticker-marquee {
|
||||
display: inline-flex;
|
||||
gap: 0.75rem;
|
||||
animation: ticker 45s linear infinite;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.ticker-card:hover .ticker-marquee {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes ticker {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.ticker-chip {
|
||||
font-size: 0.9rem;
|
||||
border-color: var(--border);
|
||||
background: var(--panel);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ticker-chip__time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.stats-card__main {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-card__icon {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 1rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--panel);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stats-card__main h2 {
|
||||
margin: 0.2rem 0;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.stats-card__links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-card__links a {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-grid div {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--panel);
|
||||
border-radius: 0.85rem;
|
||||
}
|
||||
|
||||
.stats-grid dt {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.stats-grid dd {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.search-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.search-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-stack {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 999px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.input-field input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--footer);
|
||||
background-color: var(--instance-list);
|
||||
color: var(--color);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: small;
|
||||
margin: 0 auto;
|
||||
color: var(--color);
|
||||
.secondary-button {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.title, .placeholder a, footer a, .download-csv {
|
||||
cursor: pointer;
|
||||
.suggestions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem auto;
|
||||
border-bottom: 1px solid var(--footer);
|
||||
width: 50%;
|
||||
.suggestions button {
|
||||
border-color: var(--border);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
font-size: small;
|
||||
text-align: left;
|
||||
width: 2.2in;
|
||||
background-color: var(--background-color);
|
||||
color: var(--color);
|
||||
border-radius: 5px;
|
||||
padding: 1rem 1rem 0 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
border: 1px solid var(--footer);
|
||||
}
|
||||
|
||||
.count:hover .tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--footer);
|
||||
background-color: var(--color);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
.reverse:hover {
|
||||
color: var(--color);
|
||||
background-color: var(--background-color);
|
||||
cursor: pointer;
|
||||
.list-shell {
|
||||
max-height: 65vh;
|
||||
overflow: hidden;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.instancelist {
|
||||
list-style-type: none;
|
||||
margin: 20px 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--color);
|
||||
max-height: 58vh;
|
||||
overflow: auto;
|
||||
max-height: inherit;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.instancelist li {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.instancelist li:hover {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.instance,
|
||||
.placeholder,
|
||||
h4 {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 28%;
|
||||
}
|
||||
.instance-card,
|
||||
.instance-card__import {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (min-width: 600px) and (max-width: 992px) {
|
||||
.instancelist li {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.instancelist li:hover {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
.instance,
|
||||
.placeholder,
|
||||
h4 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 58%;
|
||||
}
|
||||
.instance-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.instancelist li {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.instancelist li:hover {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.instance,
|
||||
.placeholder,
|
||||
h4 {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.api {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 88%;
|
||||
}
|
||||
.instance-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.instancelist li {
|
||||
color: var(--color);
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--footer);
|
||||
margin: 0 auto;
|
||||
max-height: 1.5em;
|
||||
overflow: hidden;
|
||||
min-height: 1.5em;
|
||||
line-height: 1.5;
|
||||
.instance-card__header button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.instancelist li:hover {
|
||||
background-color: var(--instance-list);
|
||||
cursor: pointer;
|
||||
max-height: fit-content;
|
||||
.instance-card__badge {
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.instancelist li img {
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
.instance-card__body {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
@keyframes opacity {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
.instance-card__body img {
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
.instance-card__import {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instance-card__order {
|
||||
font-weight: 700;
|
||||
margin-right: 1rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.scan-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.scan-card header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scan-card p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.footer-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.footer-metrics svg {
|
||||
color: var(--accent);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.footer-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-credit {
|
||||
margin-left: auto;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.footer-actions a.footer-credit {
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.footer-actions a.footer-credit:hover {
|
||||
color: var(--text);
|
||||
border: none;
|
||||
background: transparent;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.loader-content {
|
||||
z-index: 100;
|
||||
background: rgba(5, 6, 10, 0.75);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.loader {
|
||||
background: var(--bg-soft);
|
||||
padding: 2rem;
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
color: var(--footer);
|
||||
}
|
||||
|
||||
.count {
|
||||
text-decoration: underline dotted var(--color);
|
||||
cursor: help;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.blocklist {
|
||||
width: 90%;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.blocklist li {
|
||||
color: var(--color);
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blocklist li:hover {
|
||||
white-space: inherit;
|
||||
cursor: pointer;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.blocklist li a,
|
||||
.blocklist li a:hover,
|
||||
.blocklist li a:visited {
|
||||
color: var(--color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.blockinstance a,
|
||||
.blockinstance a:hover {
|
||||
color: var(--color);
|
||||
text-decoration: underline;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.blockcount {
|
||||
font-weight: bolder;
|
||||
.loader p {
|
||||
margin-top: 1rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
inset: 0;
|
||||
background: rgba(5, 6, 10, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 200;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background-color: var(--background-color);
|
||||
margin: 10px auto;
|
||||
padding: 0;
|
||||
border: 1px solid var(--footer);
|
||||
-webkit-animation-name: animatetop;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: animatetop;
|
||||
max-width: 640px;
|
||||
margin: auto;
|
||||
background: var(--bg-soft);
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
animation-duration: 0.4s;
|
||||
animation-name: animatetop;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 6px;
|
||||
background-color: var(--color);
|
||||
color: var(--background-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: var(--color);
|
||||
background-color: var(--background-color);
|
||||
padding: 6px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.blocklist {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.blocklist li {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.blocklist a {
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.blockcount {
|
||||
font-size: 1.4rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 6px;
|
||||
background-color: var(--color);
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
.closemodal,
|
||||
.download,
|
||||
.capture {
|
||||
color: var(--footer);
|
||||
float: right;
|
||||
font-size: 3rem;
|
||||
font-weight: bolder;
|
||||
margin: 0px 15px;
|
||||
}
|
||||
|
||||
.closemodal:hover,
|
||||
.closemodal:focus,
|
||||
.download:hover,
|
||||
.download:focus,
|
||||
.capture:hover,
|
||||
.capture:focus {
|
||||
color: var(--color);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
@keyframes animatetop {
|
||||
from {
|
||||
top: -300px;
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
}
|
||||
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animatebottom {
|
||||
from {
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
top: -300px;
|
||||
opacity: 0
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-left: 1px solid var(--background-color);
|
||||
border-right: 1px solid var(--background-color);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
h4 p {
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
|
||||
h4:hover {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.bounce {
|
||||
display: inline-block;
|
||||
animation: marquee 90s linear infinite;
|
||||
}
|
||||
|
||||
.bounce:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translateX(100vw);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.loader-content {
|
||||
z-index: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.loader {
|
||||
background-color: var(--color);
|
||||
width: fit-content;
|
||||
margin: 50vh auto;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
color: var(--background-color);
|
||||
}
|
||||
@@ -12,16 +12,19 @@ function App() {
|
||||
const [searchTerm, setSearchTerm] = useState(''),
|
||||
[matrix, setMatrix] = useState('off')
|
||||
return (
|
||||
<>
|
||||
<Title />
|
||||
<Bar setSearch={d => setSearchTerm(d)} />
|
||||
<Count />
|
||||
<Form searchTerm={searchTerm} matrix={matrix} />
|
||||
<Scan />
|
||||
<hr />
|
||||
<div className="app-shell">
|
||||
<header className="hero-grid">
|
||||
<Title />
|
||||
<Bar setSearch={d => setSearchTerm(d)} />
|
||||
<Count />
|
||||
</header>
|
||||
<main className="content-layout">
|
||||
<Form searchTerm={searchTerm} matrix={matrix} />
|
||||
<Scan />
|
||||
</main>
|
||||
<Footer setCurrentMatrix={m => setMatrix(m)} />
|
||||
<Loader />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import dayjs from '../../node_modules/dayjs/';
|
||||
import relativeTime from '../../node_modules/dayjs/plugin/relativeTime';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { FiActivity } from 'react-icons/fi';
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const Bar = (prop) => {
|
||||
const Bar = ({ setSearch }) => {
|
||||
const [bounce, setBounce] = useState([]),
|
||||
fillBounce = useCallback(async () => {
|
||||
try {
|
||||
@@ -15,14 +17,47 @@ const Bar = (prop) => {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fillBounce()
|
||||
}, [])
|
||||
}, [fillBounce])
|
||||
|
||||
return (
|
||||
<h4>
|
||||
<p className="bounce">{bounce && bounce.length > 0 ? bounce.map(b => <a onClick={() => prop.setSearch(b.content.replace(/.*#/g, '').replace(/\<\/a\>\<\/p\>/, ''))}>{b.content.replace(/<[^>]*>/g, '').replace(new RegExp(new URL(window.location.href).host), '')} - {dayjs().to(b.published)}</a>).reduce((prev, curr) => [prev, ' | ', curr]) : []}</p>
|
||||
</h4>
|
||||
<section className="ticker-card" aria-live="polite">
|
||||
<header className="ticker-card__header">
|
||||
<FiActivity aria-hidden="true" />
|
||||
<div className="ticker-card__title">
|
||||
<span>Recent activity</span>
|
||||
<small>Select any entry to reuse as a search</small>
|
||||
</div>
|
||||
</header>
|
||||
<div className="ticker-card__body">
|
||||
{bounce?.length ? (
|
||||
<div className="ticker-marquee">
|
||||
{bounce.map((b, index) => {
|
||||
const sanitized = b.content.replace(/.*#/g, '').replace(/<\/a><\/p>/, ''),
|
||||
plainText = b.content
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(new RegExp(new URL(window.location.href).host), '')
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="ticker-chip"
|
||||
key={`${b.id ?? index}-${b.published}`}
|
||||
onClick={() => setSearch(sanitized)}
|
||||
>
|
||||
<span className="ticker-chip__content">{plainText}</span>
|
||||
<span className="ticker-chip__time">{dayjs().to(b.published)}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<span className="muted">No recent activity</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { FiTrendingUp, FiExternalLink } from 'react-icons/fi';
|
||||
|
||||
const Count = () => {
|
||||
const [count, setCount] = useState('0'),
|
||||
[statsres, setStatsres] = useState(''),
|
||||
const [count, setCount] = useState(0),
|
||||
[statsres, setStatsres] = useState(null),
|
||||
fillStats = useCallback(async () => {
|
||||
try {
|
||||
const [res, stats] = await Promise.all([(await fetch('/api/count')).json(), (await fetch('/api/stats')).json()])
|
||||
@@ -11,30 +12,82 @@ const Count = () => {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await fillStats()
|
||||
})()
|
||||
}, [])
|
||||
fillStats()
|
||||
}, [fillStats])
|
||||
|
||||
const statLines = useMemo(() => {
|
||||
if (!statsres) {
|
||||
return []
|
||||
}
|
||||
const safeDiv = (numerator, denominator) => {
|
||||
if (!denominator) return 0
|
||||
return numerator / denominator
|
||||
}
|
||||
return [
|
||||
{ label: 'Statuses AVG', value: Math.round(statsres.status_avg ?? 0) },
|
||||
{ label: 'Statuses MAX', value: statsres.status_max },
|
||||
{ label: 'Domain AVG', value: Math.round(statsres.domain_avg ?? 0) },
|
||||
{ label: 'Domain MAX', value: statsres.domain_max },
|
||||
{ label: 'Users AVG', value: Math.round(statsres.user_avg ?? 0) },
|
||||
{ label: 'Users MAX', value: statsres.user_max },
|
||||
{ label: 'Stats Instances', value: statsres.stats_filtered },
|
||||
{ label: 'Total Instances', value: statsres.instance_count },
|
||||
{
|
||||
label: 'Users per Instance',
|
||||
value: safeDiv(Math.round(statsres.user_avg ?? 0), statsres.instance_count ?? 1).toFixed(2)
|
||||
},
|
||||
{
|
||||
label: 'Statuses per Domain',
|
||||
value: safeDiv(Math.round(statsres.status_avg ?? 0), Math.round(statsres.domain_avg ?? 1)).toFixed(2)
|
||||
},
|
||||
{
|
||||
label: 'Statuses per User',
|
||||
value: safeDiv(Math.round(statsres.status_avg ?? 0), Math.round(statsres.user_avg ?? 1)).toFixed(2)
|
||||
}
|
||||
]
|
||||
}, [statsres])
|
||||
|
||||
const formatNumber = value => {
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return value ?? '--'
|
||||
}
|
||||
return new Intl.NumberFormat().format(value ?? 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<h3>Search in <a href="/api/stats" target="_blank">
|
||||
<span className="count">{count}<div className="tooltip">
|
||||
<u><strong><center>STATS</center></strong></u>
|
||||
<ul><li>Statuses AVG: {Math.round(statsres.status_avg)}</li>
|
||||
<li>Statuses MAX: {statsres.status_max}</li>
|
||||
<li>Domain AVG: {Math.round(statsres.domain_avg)}</li>
|
||||
<li>Domain MAX: {statsres.domain_max}</li>
|
||||
<li>Users AVG: {Math.round(statsres.user_avg)}</li>
|
||||
<li>Users MAX: {statsres.user_max}</li>
|
||||
<li>Stats Instances: {statsres.stats_filtered}</li>
|
||||
<li>Total Instances: {statsres.instance_count}</li>
|
||||
<li>Users by Instance: {(Math.round(statsres.user_avg) / statsres.instance_count).toFixed(2)}</li>
|
||||
<li>Statuses by Domain: {(Math.round(statsres.status_avg) / Math.round(statsres.domain_avg)).toFixed(2)}</li>
|
||||
<li>Statuses by User: {(Math.round(statsres.status_avg) / Math.round(statsres.user_avg)).toFixed(2)}</li>
|
||||
</ul></div></span>
|
||||
</a> public instances<span className="api"> - <a href="/api-docs" target="_blank">API docs</a></span>
|
||||
</h3>
|
||||
<section className="stats-card">
|
||||
<div className="stats-card__main">
|
||||
<div className="stats-card__icon">
|
||||
<FiTrendingUp aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="muted">Searching across</p>
|
||||
<h2>{formatNumber(count)}</h2>
|
||||
<p className="muted">public instances</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats-card__links">
|
||||
<a href="/api/stats" target="_blank" rel="noreferrer">
|
||||
View stats
|
||||
<FiExternalLink aria-hidden="true" />
|
||||
</a>
|
||||
<a href="/api-docs" target="_blank" rel="noreferrer">
|
||||
API docs
|
||||
<FiExternalLink aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
<dl className="stats-grid">
|
||||
{statLines.map(item => (
|
||||
<div key={item.label}>
|
||||
<dt>{item.label}</dt>
|
||||
<dd>{formatNumber(item.value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { FiSun, FiMoon, FiDownload, FiGrid, FiZap } from 'react-icons/fi';
|
||||
import Messenger from '../random-text';
|
||||
|
||||
const Footer = (prop) => {
|
||||
const Footer = ({ setCurrentMatrix }) => {
|
||||
const [theme, setTheme] = useState('dark'),
|
||||
[matrix, setMatrix] = useState('off'),
|
||||
[served, setServed] = useState({ served: 0, lastscan: 0, server: 0, instances: 0, peers: 0, created: 0, updated: 0 }),
|
||||
loading = () => {
|
||||
document.querySelector('.loader-content').style.display = 'initial'
|
||||
setTimeout(() => {
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
}, 60 * 1000)
|
||||
},
|
||||
toggleTheme = useCallback(() => {
|
||||
let tm = document.documentElement.dataset.theme
|
||||
if (!theme || tm === 'light') {
|
||||
setTheme('dark')
|
||||
} else {
|
||||
setTheme('light')
|
||||
toggleLoader = useCallback(() => {
|
||||
const loader = document.querySelector('.loader-content')
|
||||
if (loader) {
|
||||
loader.style.display = 'initial'
|
||||
setTimeout(() => {
|
||||
loader.style.display = 'none'
|
||||
}, 60 * 1000)
|
||||
}
|
||||
}),
|
||||
}, []),
|
||||
toggleTheme = useCallback(() => {
|
||||
setTheme(prev => (prev === 'light' ? 'dark' : 'light'))
|
||||
}, []),
|
||||
refreshServed = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/served')
|
||||
@@ -28,38 +27,67 @@ const Footer = (prop) => {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}),
|
||||
}, []),
|
||||
toggleMatrix = useCallback(() => {
|
||||
if (matrix === 'off') {
|
||||
setMatrix('on')
|
||||
prop.setCurrentMatrix('on')
|
||||
} else {
|
||||
setMatrix('off')
|
||||
prop.setCurrentMatrix('off')
|
||||
}
|
||||
})
|
||||
setMatrix(prev => {
|
||||
const next = prev === 'off' ? 'on' : 'off'
|
||||
setCurrentMatrix(next)
|
||||
return next
|
||||
})
|
||||
}, [setCurrentMatrix])
|
||||
|
||||
useEffect(() => {
|
||||
setTheme(theme)
|
||||
document.documentElement.dataset.theme = theme
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
refreshServed()
|
||||
}, [refreshServed])
|
||||
|
||||
useEffect(() => {
|
||||
if (matrix === 'on') {
|
||||
var walker = document.createTreeWalker(document.getElementById('root'), NodeFilter.SHOW_TEXT)
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.length > 1) {
|
||||
new Messenger(walker.currentNode)
|
||||
const root = document.getElementById('root')
|
||||
if (root) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.length > 1) {
|
||||
new Messenger(walker.currentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [theme])
|
||||
}, [matrix])
|
||||
|
||||
return (
|
||||
<footer>Served <span className="served">{served.served}</span> times - Last scan <span className="lastscan">{served.lastscan}</span> peers of <span
|
||||
className="server">{served.server}</span><br />
|
||||
Total scanned <span id="instances">{served.instances}</span> instances with <span className="peers">{served.peers}</span> peers - <span
|
||||
className="created">{served.created}</span> created - <span className="updated">{served.updated}</span> updated<br />
|
||||
matrix <a className="matrix" onClick={() => toggleMatrix()}>{matrix}</a> - download json <a className="download_index" href="/api/download_index"
|
||||
onClick={() => loading()} download="fediblock-index.json.gz" target="_blank">index</a> -
|
||||
by <a href="https://about.manalejandro.com" target="_blank">ale</a> <s>©</s>2025
|
||||
<a className="darklight" onClick={() => toggleTheme()}>{!theme || theme === 'dark' ? '☼' : '☽'}</a>
|
||||
<footer className="app-footer">
|
||||
<div className="footer-metrics">
|
||||
<div>
|
||||
<FiGrid aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{served.served}</strong> requests served - Last scan {served.lastscan}
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
Total {served.instances} instances - {served.peers} peers - {served.created} created - {served.updated} updated - server {served.server}
|
||||
</p>
|
||||
</div>
|
||||
<div className="footer-actions">
|
||||
<button type="button" onClick={toggleMatrix}>
|
||||
<FiZap aria-hidden="true" />
|
||||
Matrix {matrix === 'on' ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
<a className="download_index" href="/api/download_index" onClick={toggleLoader} download="fediblock-index.json.gz" target="_blank" rel="noreferrer">
|
||||
<FiDownload aria-hidden="true" />
|
||||
Download index
|
||||
</a>
|
||||
<button type="button" onClick={toggleTheme}>
|
||||
{theme === 'dark' ? <FiSun aria-hidden="true" /> : <FiMoon aria-hidden="true" />}
|
||||
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
</button>
|
||||
<a href="https://about.manalejandro.com" target="_blank" rel="noreferrer" className="footer-credit">
|
||||
by ale <s>©</s>2025
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,166 +1,248 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { FiSearch, FiRotateCcw, FiRefreshCcw, FiChevronRight } from 'react-icons/fi';
|
||||
import Modal from './Modal';
|
||||
import Messenger from '../random-text';
|
||||
|
||||
const Form = (prop) => {
|
||||
let csv
|
||||
const sanitizeInstanceInput = value => {
|
||||
if (!value) return ''
|
||||
let sanitized = value.replace(/[^a-z0-9.\-*:]/gi, '')
|
||||
if (sanitized.startsWith('.')) {
|
||||
sanitized = sanitized.substring(1)
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
const Form = ({ searchTerm: externalSearch, matrix }) => {
|
||||
const [searchTerm, setSearchTerm] = useState(''),
|
||||
[list, setList] = useState([]),
|
||||
[typeList, setTypeList] = useState('ranking'),
|
||||
[domain, setDomain] = useState({ domain: '' }),
|
||||
[reverse, setReverse] = useState(false),
|
||||
refList = useRef(null),
|
||||
refDownload = useRef(null),
|
||||
loading = () => {
|
||||
document.querySelector('.loader-content').style.display = 'initial'
|
||||
},
|
||||
download = () => {
|
||||
if (csv.split('\n').length > 2) {
|
||||
refDownload.current.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
|
||||
refDownload.current.download = 'fediblock-top100.csv'
|
||||
csvRef = useRef(''),
|
||||
toggleLoader = useCallback((show = true) => {
|
||||
const loader = document.querySelector('.loader-content')
|
||||
if (loader) {
|
||||
loader.style.display = show ? 'initial' : 'none'
|
||||
}
|
||||
},
|
||||
}, []),
|
||||
download = useCallback(() => {
|
||||
if (csvRef.current.split('\n').length > 2) {
|
||||
const blob = new Blob([csvRef.current], { type: 'text/csv' }),
|
||||
url = window.URL.createObjectURL(blob),
|
||||
anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = 'fediblock-top100.csv'
|
||||
anchor.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
}, []),
|
||||
listinstance = useCallback(async content => {
|
||||
loading()
|
||||
if (content && content.length > 0) {
|
||||
try {
|
||||
const result = await fetch('/api/list/' + content),
|
||||
res = await result.json()
|
||||
if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) {
|
||||
setTypeList('list')
|
||||
setList(res)
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
} else {
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
window.alert('Error: No response')
|
||||
}
|
||||
} catch (e) {
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
window.alert('Error: ' + e.message)
|
||||
}
|
||||
} else {
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
if (!content || !content.length) {
|
||||
toggleLoader(false)
|
||||
return
|
||||
}
|
||||
}),
|
||||
toggleLoader(true)
|
||||
try {
|
||||
const result = await fetch('/api/list/' + content),
|
||||
res = await result.json()
|
||||
if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) {
|
||||
setTypeList('list')
|
||||
setList(res)
|
||||
} else {
|
||||
window.alert('Error: No response')
|
||||
}
|
||||
} catch (e) {
|
||||
window.alert('Error: ' + e.message)
|
||||
} finally {
|
||||
toggleLoader(false)
|
||||
}
|
||||
}, [toggleLoader]),
|
||||
ranking = useCallback(async () => {
|
||||
loading()
|
||||
toggleLoader(true)
|
||||
try {
|
||||
const result = await fetch('/api/ranking'),
|
||||
res = await result.json()
|
||||
if (Array.isArray(res) && res.length > 0) {
|
||||
setTypeList('ranking')
|
||||
setList(res)
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
} else {
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
window.alert('Error: No response')
|
||||
}
|
||||
} catch (e) {
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
window.alert('Error: ' + e.message)
|
||||
} finally {
|
||||
toggleLoader(false)
|
||||
}
|
||||
}),
|
||||
filterKeys = useCallback(event => {
|
||||
if (event.key && !event.ctrlKey && !event.altKey) {
|
||||
if ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && event.target.value !== '')) {
|
||||
if (event.key === '.' && event.target.value === '') {
|
||||
return
|
||||
} else if (event.key === 'Backspace' && event.target.value !== '') {
|
||||
setSearchTerm(searchTerm.substring(0, searchTerm.length - 1))
|
||||
} else {
|
||||
setSearchTerm(searchTerm + event.key)
|
||||
}
|
||||
}
|
||||
}, [toggleLoader]),
|
||||
handleReverseSearch = useCallback(() => {
|
||||
if (searchTerm && searchTerm.length > 0) {
|
||||
setReverse(true)
|
||||
setDomain({ domain: searchTerm })
|
||||
toggleLoader(true)
|
||||
}
|
||||
})
|
||||
}, [searchTerm, toggleLoader])
|
||||
|
||||
const handleInputChange = useCallback(event => {
|
||||
setSearchTerm(sanitizeInstanceInput(event.target.value))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm && searchTerm.length > 0) {
|
||||
listinstance(searchTerm)
|
||||
window.location.hash = searchTerm
|
||||
} else {
|
||||
(async () => {
|
||||
await ranking()
|
||||
})()
|
||||
ranking()
|
||||
}
|
||||
if (prop.matrix === 'on') {
|
||||
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
|
||||
}, [searchTerm, listinstance, ranking])
|
||||
|
||||
useEffect(() => {
|
||||
if (matrix === 'on' && refList.current) {
|
||||
const walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.length > 1) {
|
||||
new Messenger(walker.currentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [searchTerm, prop.matrix])
|
||||
}, [list, matrix])
|
||||
|
||||
useEffect(() => {
|
||||
if (prop.searchTerm && prop.searchTerm.length > 0) {
|
||||
setSearchTerm(prop.searchTerm)
|
||||
if (externalSearch && externalSearch.length > 0) {
|
||||
setSearchTerm(sanitizeInstanceInput(externalSearch))
|
||||
}
|
||||
}, [prop.searchTerm])
|
||||
}, [externalSearch])
|
||||
|
||||
useEffect(() => {
|
||||
if (window.location.hash && window.location.hash !== '#') {
|
||||
setSearchTerm(window.location.hash.substring(1))
|
||||
setSearchTerm(sanitizeInstanceInput(window.location.hash.substring(1)))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const suggestions = typeList === 'list' && list.suggests && list.suggests.length > 0 ? list.suggests : []
|
||||
const host = typeof window !== 'undefined' ? new URL(window.location.href).host : 'fediblock'
|
||||
|
||||
if (typeList === 'ranking') {
|
||||
csvRef.current = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
className="instance"
|
||||
placeholder="Type the name of the instance"
|
||||
onKeyUp={(e) => filterKeys(e)}
|
||||
value={searchTerm} />
|
||||
<button className="reverse" title="Reverse search..." onClick={() => {
|
||||
if (searchTerm && searchTerm.length > 0) {
|
||||
setReverse(true)
|
||||
setDomain({ domain: searchTerm })
|
||||
loading()
|
||||
}
|
||||
}}>Reverse</button>
|
||||
<div className="placeholder">{typeList === 'list' ? list.suggests && list.suggests.length > 0 ? list.suggests.map(suggest => (<a onClick={() => setSearchTerm(suggest)}>{suggest}</a>)).reduce((prev, curr) => [prev, ', ', curr]) : [] : []}</div>
|
||||
<ul className="instancelist" ref={refList}>{typeList === 'list' ? list.instances.map(r =>
|
||||
(<li><a onClick={() => {
|
||||
if (r.blocks) {
|
||||
setReverse(false)
|
||||
setDomain({ domain: r.domain })
|
||||
loading()
|
||||
} else {
|
||||
var a = document.createElement('a')
|
||||
a.href = '/api/detail_api/' + r.domain
|
||||
a.title = 'API info for ' + r.domain
|
||||
a.target = '_blank'
|
||||
a.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
}}>{r.domain}</a>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.blocks ? ` - ` + r.blocks + ` blocks ` : ` ` }}></span>
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">ⓘ</a>` + `<br />` : `<br />`
|
||||
}}></span>
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: r.api?.title ? `<br /><br />` + r.api.title + ` - ` + r.api.uri + `<br />` : `<br /><br />`
|
||||
}}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.last ? `Last update: ` + (new Date(r.last)).toLocaleString() + `<br />` : `` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.email ? `Email: ` + r.api.email + `<br />` : `` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: `Registration: ` + (r.api?.registrations ? `open` : `closed`) + ` - Version: ` + r.api?.version + `<br />` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.stats ? `Users: ` + r.api.stats.user_count + ` - Statuses: ` + r.api.stats.status_count + ` - Domains: ` + r.api.stats.domain_count + `<br />` : `` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.description ? `Description: ` + r.api.description + `<br />` : `` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.thumbnail ? `<img domain="${r.domain}" loading="lazy" src="${r.api.thumbnail}" />` + `<br />` : `` }}></span>
|
||||
</li>
|
||||
)) : typeList === 'ranking' ? list.map((r, i) => {
|
||||
if (i === 0) {
|
||||
csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
|
||||
return (<><li><strong>Top 100</strong><br /><small className="download-top"><a ref={refDownload} onClick={() => download()}>(Import CSV)</a></small></li><li>{i + 1} - {r.domain} - {r.count} blocks</li></>)
|
||||
} else {
|
||||
csv += !r.domain.match(/\*/) ? r.domain + ',suspend,False,False,"suspended by top 100 of ' + new URL(window.location.href).host + '",False\n' : ''
|
||||
return (<li>{i + 1} - {r.domain} - {r.count} blocks</li>)
|
||||
}
|
||||
}) : ''}
|
||||
<section className="search-panel">
|
||||
<header className="search-panel__header">
|
||||
<div>
|
||||
<span className="eyebrow">Explore instances</span>
|
||||
<h2>Review blocklists in seconds</h2>
|
||||
<p className="muted">Search a specific domain or browse the most reported top 100.</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-button" onClick={ranking}>
|
||||
<FiRefreshCcw aria-hidden="true" />
|
||||
Refresh ranking
|
||||
</button>
|
||||
</header>
|
||||
<div className="input-stack">
|
||||
<label className="input-field">
|
||||
<FiSearch aria-hidden="true" />
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
className="instance"
|
||||
placeholder="Type an instance domain"
|
||||
value={searchTerm}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</label>
|
||||
<button className="secondary-button" title="Reverse lookup" type="button" onClick={handleReverseSearch}>
|
||||
<FiRotateCcw aria-hidden="true" />
|
||||
Reverse lookup
|
||||
</button>
|
||||
</div>
|
||||
{suggestions.length > 0 && (
|
||||
<div className="suggestions">
|
||||
{suggestions.map(suggest => (
|
||||
<button type="button" key={suggest} onClick={() => setSearchTerm(sanitizeInstanceInput(suggest))}>
|
||||
{suggest}
|
||||
<FiChevronRight aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="list-shell" ref={refList}>
|
||||
<ul className="instancelist">
|
||||
{typeList === 'list' && Array.isArray(list.instances)
|
||||
? list.instances.map(r => (
|
||||
<li className="instance-card" key={r.domain}>
|
||||
<div className="instance-card__header">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (r.blocks) {
|
||||
setReverse(false)
|
||||
setDomain({ domain: r.domain })
|
||||
toggleLoader(true)
|
||||
} else {
|
||||
window.open('/api/detail_api/' + r.domain, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}}
|
||||
>
|
||||
{r.domain}
|
||||
</button>
|
||||
<span
|
||||
className="instance-card__badge"
|
||||
dangerouslySetInnerHTML={{ __html: r.blocks ? `${r.blocks} blocks` : ' ' }}
|
||||
></span>
|
||||
</div>
|
||||
<div className="instance-card__body">
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">ⓘ</a>` : ''
|
||||
}}></span>
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: r.api?.title ? `${r.api.title} - ${r.api.uri}<br />` : ''
|
||||
}}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.last ? `Last updated: ${(new Date(r.last)).toLocaleString()}<br />` : '' }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.email ? `Email: ${r.api.email}<br />` : '' }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: `Registration: ${(r.api?.registrations ? 'open' : 'closed')} - Version: ${r.api?.version ?? 'N/A'}<br />` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.stats ? `Users: ${r.api.stats.user_count} - Statuses: ${r.api.stats.status_count} - Domains: ${r.api.stats.domain_count}<br />` : '' }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.description ? `Description: ${r.api.description}<br />` : '' }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: r.api?.thumbnail ? `<img domain="${r.domain}" loading="lazy" src="${r.api.thumbnail}" alt="${r.domain} thumbnail" />` : '' }}></span>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{typeList === 'ranking' && Array.isArray(list)
|
||||
? (
|
||||
<>
|
||||
<li className="instance-card instance-card__import">
|
||||
<div>
|
||||
<strong>Top 100</strong>
|
||||
<p className="muted">Ready-to-import CSV for Mastodon.</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-button" onClick={download}>
|
||||
Download CSV
|
||||
</button>
|
||||
</li>
|
||||
{list.map((r, i) => {
|
||||
if (!r) return null
|
||||
if (!r.domain.match(/\*/) && i > 0) {
|
||||
csvRef.current += `${r.domain},suspend,False,False,"suspended by top 100 of ${host}",False\n`
|
||||
}
|
||||
return (
|
||||
<li className="instance-card" key={`${r.domain}-${i}`}>
|
||||
<span className="instance-card__order">{i + 1}</span>
|
||||
<div>
|
||||
<strong>{r.domain}</strong>
|
||||
<p className="muted">{r.count} reported blocks</p>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
</ul>
|
||||
</section >
|
||||
<Modal domain={domain} reverse={reverse} setSearch={d => setSearchTerm(d)} matrix={prop.matrix} />
|
||||
</>
|
||||
</div>
|
||||
<Modal domain={domain} reverse={reverse} setSearch={d => setSearchTerm(sanitizeInstanceInput(d))} matrix={matrix} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import '../loaders.css';
|
||||
|
||||
const loaderClasses = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario']
|
||||
|
||||
const Loader = () => {
|
||||
const loaders = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario'],
|
||||
[load, setLoad] = useState('load ' + loaders[Math.floor(Math.random() * loaders.length)])
|
||||
const [load, setLoad] = useState(() => 'load ' + loaderClasses[Math.floor(Math.random() * loaderClasses.length)])
|
||||
useEffect(() => {
|
||||
setLoad('load ' + loaders[Math.floor(Math.random() * loaders.length)])
|
||||
setLoad('load ' + loaderClasses[Math.floor(Math.random() * loaderClasses.length)])
|
||||
}, [])
|
||||
return (
|
||||
<div className="loader-content">
|
||||
<div className="loader-content" role="alert" aria-live="assertive">
|
||||
<div className="loader">
|
||||
<div className={load}></div>
|
||||
<p>Preparing live data...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useCallback, useState, useRef } from 'react';
|
||||
import html2canvas from '../../node_modules/html2canvas/dist/html2canvas.js';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { FiDownload, FiCamera, FiX } from 'react-icons/fi';
|
||||
import Messenger from '../random-text.js';
|
||||
|
||||
const Modal = (prop) => {
|
||||
let csv
|
||||
const [blocktitle, setBlocktitle] = useState(''),
|
||||
[blockcount, setBlockcount] = useState(''),
|
||||
[blockinstance, setBlockinstance] = useState(''),
|
||||
@@ -11,33 +11,54 @@ const Modal = (prop) => {
|
||||
[blocklist, setBlocklist] = useState([]),
|
||||
hrefCanvas = useRef(null),
|
||||
refList = useRef(null),
|
||||
csvRef = useRef(''),
|
||||
currentDomain = prop.domain?.domain ?? 'instance',
|
||||
loading = () => {
|
||||
document.querySelector('.loader-content').style.display = 'initial'
|
||||
const loader = document.querySelector('.loader-content')
|
||||
if (loader) {
|
||||
loader.style.display = 'initial'
|
||||
}
|
||||
},
|
||||
stopLoading = () => {
|
||||
const loader = document.querySelector('.loader-content')
|
||||
if (loader) {
|
||||
loader.style.display = 'none'
|
||||
}
|
||||
},
|
||||
closeModal = event => {
|
||||
if (event.target.classList.contains('modal') || event.target.classList.contains('closemodal')) {
|
||||
document.querySelector('.modal-content').style.animationName = 'animatebottom'
|
||||
const modalContent = document.querySelector('.modal-content')
|
||||
if (modalContent) {
|
||||
modalContent.style.animationName = 'animatebottom'
|
||||
}
|
||||
}
|
||||
},
|
||||
animationEnd = event => {
|
||||
if (event.animationName === 'animatebottom') {
|
||||
document.querySelector('.modal').style.display = 'none'
|
||||
const modal = document.querySelector('.modal')
|
||||
if (modal) {
|
||||
modal.style.display = 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
capture = useCallback(async () => {
|
||||
const canvas = await html2canvas(hrefCanvas.current, { useCORS: true }),
|
||||
capture = document.querySelector('.capture')
|
||||
capture.download = 'fediblock-' + Date.now() + '.png'
|
||||
capture.href = canvas.toDataURL('image/png', 1.0)
|
||||
capture.dispatchEvent(new MouseEvent('click'))
|
||||
}),
|
||||
download = () => {
|
||||
const download = document.querySelector('.download')
|
||||
if (csv.split('\n').length > 2) {
|
||||
download.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
|
||||
download.download = 'fediblock-' + prop.domain.domain + '.csv'
|
||||
captureLink = document.createElement('a')
|
||||
captureLink.download = 'fediblock-' + Date.now() + '.png'
|
||||
captureLink.href = canvas.toDataURL('image/png', 1.0)
|
||||
captureLink.click()
|
||||
}, []),
|
||||
download = useCallback(() => {
|
||||
if (csvRef.current.split('\n').length > 2) {
|
||||
const blob = new Blob([csvRef.current], { type: 'text/csv' }),
|
||||
url = window.URL.createObjectURL(blob),
|
||||
anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = 'fediblock-' + currentDomain + '.csv'
|
||||
anchor.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
},
|
||||
}, [currentDomain]),
|
||||
reverse = useCallback(async content => {
|
||||
if (content && content.length > 0) {
|
||||
loading()
|
||||
@@ -49,11 +70,16 @@ const Modal = (prop) => {
|
||||
setBlockinstance('ing ' + content)
|
||||
setBlocktook('took ' + res.took + 'ms')
|
||||
setBlocklist(res.instances)
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
document.querySelector('.modal-content').style.animationName = 'animatetop'
|
||||
document.querySelector('.modal').style.display = 'block'
|
||||
const modalContent = document.querySelector('.modal-content')
|
||||
const modal = document.querySelector('.modal')
|
||||
if (modalContent) {
|
||||
modalContent.style.animationName = 'animatetop'
|
||||
}
|
||||
if (modal) {
|
||||
modal.style.display = 'block'
|
||||
}
|
||||
if (prop.matrix === 'on') {
|
||||
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
|
||||
const walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.length > 1) {
|
||||
new Messenger(walker.currentNode)
|
||||
@@ -61,13 +87,14 @@ const Modal = (prop) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
}),
|
||||
}, [prop.matrix]),
|
||||
listblock = useCallback(async (domain) => {
|
||||
const result = await fetch('/api/detail/' + domain),
|
||||
res = await result.json()
|
||||
if (res.blocks && Array.isArray(res.blocks)) {
|
||||
csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
|
||||
csvRef.current = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
|
||||
setBlocktitle('Blocked List')
|
||||
setBlockcount(res.blocks.length)
|
||||
setBlocktook('took ' + res.took + 'ms')
|
||||
@@ -76,7 +103,7 @@ const Modal = (prop) => {
|
||||
+ `Last update: ` + (new Date(res.last)).toLocaleString())
|
||||
setBlocklist(res.blocks)
|
||||
if (prop.matrix === 'on') {
|
||||
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
|
||||
const walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.length > 1) {
|
||||
new Messenger(walker.currentNode)
|
||||
@@ -84,16 +111,23 @@ const Modal = (prop) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var a = document.createElement('a')
|
||||
a.href = '/api/detail_api/' + domain
|
||||
a.title = 'API info for ' + domain
|
||||
a.target = '_blank'
|
||||
a.dispatchEvent(new MouseEvent('click'))
|
||||
const link = document.createElement('a')
|
||||
link.href = '/api/detail_api/' + domain
|
||||
link.title = 'API info for ' + domain
|
||||
link.target = '_blank'
|
||||
link.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
document.querySelector('.loader-content').style.display = 'none'
|
||||
document.querySelector('.modal-content').style.animationName = 'animatetop'
|
||||
document.querySelector('.modal').style.display = 'block'
|
||||
})
|
||||
stopLoading()
|
||||
const modalContent = document.querySelector('.modal-content')
|
||||
const modal = document.querySelector('.modal')
|
||||
if (modalContent) {
|
||||
modalContent.style.animationName = 'animatetop'
|
||||
}
|
||||
if (modal) {
|
||||
modal.style.display = 'block'
|
||||
}
|
||||
}, [prop.matrix])
|
||||
|
||||
useEffect(() => {
|
||||
if (prop.domain && prop.domain.domain.length > 0) {
|
||||
if (prop.reverse) {
|
||||
@@ -102,38 +136,63 @@ const Modal = (prop) => {
|
||||
listblock(prop.domain.domain)
|
||||
}
|
||||
}
|
||||
}, [prop.domain, prop.reverse])
|
||||
}, [prop.domain, prop.reverse, listblock, reverse])
|
||||
|
||||
return (
|
||||
<div className="modal" onClick={(e) => closeModal(e)}>
|
||||
<div className="modal-content" onAnimationEnd={(e) => animationEnd(e)} ref={refList}>
|
||||
<div className="modal" onClick={closeModal}>
|
||||
<div className="modal-content" onAnimationEnd={animationEnd} ref={refList}>
|
||||
<div className="modal-header">
|
||||
<span className="closemodal" title="Close" onClick={(e) => closeModal(e)}>×</span>
|
||||
<a className="capture" title="Take snapshot" onClick={capture} type="image/png" target="_blank">📷</a>
|
||||
{!prop.reverse ? <a className="download" title="Download fediblock CSV mastodon file" onClick={download} type="text/csv" target="_blank">⇩</a> : ''}
|
||||
<h2>{blocktitle}</h2>
|
||||
<div>
|
||||
<p className="eyebrow">{blocktitle}</p>
|
||||
<h2>
|
||||
<span className="blockcount">{blockcount}</span> public instances block<span className="blockinstance" dangerouslySetInnerHTML={{ __html: blockinstance }}></span>
|
||||
</h2>
|
||||
<small className="blocktook">{blocktook}</small>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button className="capture" title="Capture snapshot" onClick={capture} type="button">
|
||||
<FiCamera aria-hidden="true" />
|
||||
</button>
|
||||
{!prop.reverse && (
|
||||
<button className="download" title="Download CSV" onClick={download} type="button">
|
||||
<FiDownload aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
<button className="closemodal" title="Close" onClick={closeModal} type="button">
|
||||
<FiX aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-body" ref={hrefCanvas}>
|
||||
<p>
|
||||
<span className="blockcount">{blockcount}</span> public instances are block<span className="blockinstance" dangerouslySetInnerHTML={{ __html: blockinstance }}></span>
|
||||
<br /><small className="blocktook">{blocktook}</small>
|
||||
</p>
|
||||
<ul className="blocklist">{prop.reverse ? blocklist.map((instance, index) => {
|
||||
return (<li>{index + 1}. <a onClick={() => {
|
||||
prop.setSearch(instance.instance)
|
||||
document.querySelector('.modal-content').style.animationName = 'animatebottom'
|
||||
}}>{instance.instance}</a>{instance.comment ? ' - ' + instance.comment : ''}</li>)
|
||||
}) : blocklist.map((r, i) => {
|
||||
<ul className="blocklist">{prop.reverse ? blocklist.map((instance, index) => (
|
||||
<li key={`reverse-${instance.instance}-${index}`}>
|
||||
{index + 1}. <button type="button" className="link-button" onClick={() => {
|
||||
prop.setSearch(instance.instance)
|
||||
const modalContent = document.querySelector('.modal-content')
|
||||
if (modalContent) {
|
||||
modalContent.style.animationName = 'animatebottom'
|
||||
}
|
||||
}}>{instance.instance}</button>{instance.comment ? ' - ' + instance.comment : ''}
|
||||
</li>
|
||||
)) : blocklist.map((r, i) => {
|
||||
if (r?.domain) {
|
||||
csv += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : ''
|
||||
return (<li>{i + 1}. <a onClick={() => {
|
||||
prop.setSearch(r.domain)
|
||||
document.querySelector('.modal-content').style.animationName = 'animatebottom'
|
||||
}}>{r.domain}</a>{r.severity ? ' - ' + r.severity : ''}{r.comment ? ' - ' + r.comment : ''}</li>)
|
||||
csvRef.current += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : ''
|
||||
return (
|
||||
<li key={`block-${r.domain}-${i}`}>
|
||||
{i + 1}. <button type="button" className="link-button" onClick={() => {
|
||||
prop.setSearch(r.domain)
|
||||
const modalContent = document.querySelector('.modal-content')
|
||||
if (modalContent) {
|
||||
modalContent.style.animationName = 'animatebottom'
|
||||
}
|
||||
}}>{r.domain}</button>{r.severity ? ' - ' + r.severity : ''}{r.comment ? ' - ' + r.comment : ''}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}</ul>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
</div>
|
||||
<div className="modal-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FiCpu } from 'react-icons/fi';
|
||||
|
||||
const Scan = () => {
|
||||
const [scan, setScan] = useState('Scanning...'),
|
||||
@@ -9,7 +10,7 @@ const Scan = () => {
|
||||
source.onmessage = event => {
|
||||
if (event.data) {
|
||||
if (event.data.length > 0) {
|
||||
setScan('Async Scanning' + event.data)
|
||||
setScan('Async Scanning ' + event.data)
|
||||
} else {
|
||||
setScan('Async Scanning...')
|
||||
}
|
||||
@@ -19,12 +20,22 @@ const Scan = () => {
|
||||
console.error(error)
|
||||
}
|
||||
load.current = true
|
||||
return () => {
|
||||
source.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
return (
|
||||
<div>
|
||||
<span className="scan">{scan}</span>
|
||||
</div>
|
||||
<section className="scan-card" aria-live="polite">
|
||||
<header>
|
||||
<FiCpu aria-hidden="true" />
|
||||
<div>
|
||||
<p className="eyebrow">Live scan</p>
|
||||
<strong>Federated monitor</strong>
|
||||
</div>
|
||||
</header>
|
||||
<p>{scan}</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { useCallback } from 'react';
|
||||
import { FiRefreshCcw } from 'react-icons/fi';
|
||||
|
||||
const Title = () => {
|
||||
const click = useCallback(() => {
|
||||
window.location.hash = ''
|
||||
window.location.reload(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<h1><a onClick={() => click()} className="title">Fediblock Instance Φ</a></h1>
|
||||
<div className="title-card">
|
||||
<div className="title-stack">
|
||||
<span className="eyebrow">Fediverse safety toolkit</span>
|
||||
<h1>Fediblock Instance Φ</h1>
|
||||
<p className="subtitle">Monitor instance health, blocks, and metrics in a clean, real-time dashboard.</p>
|
||||
</div>
|
||||
<button className="ghost-button" type="button" onClick={click}>
|
||||
<FiRefreshCcw aria-hidden="true" />
|
||||
Reset view
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Referencia en una nueva incidencia
Block a user