Host-Routing: Bot-Produktseite und Community-Hub getrennt
Eine Anwendung, zwei Gesichter — der Server erkennt an der Domain, welche Seite gefragt ist, und schreibt das als data-site an den <body>. Das Frontend rendert daraufhin entweder den Community-Hub wie bisher oder die neue Produktseite. - Bot-Produktseite (BotApp): Landing mit Feature-Übersicht, Befehls- referenz, Dashboard unter /dashboard, Orange als Leitfarbe - Weiterleitungen: Community-Routen auf der Bot-Domain und umgekehrt werden dauerhaft (301) auf die richtige Adresse geschickt — geteilte Devlog-Permalinks laufen also nicht ins Leere. Rechtstexte bleiben auf beiden erreichbar. - Open-Graph-Tags je Domain, inklusive eigener Vorschau für frei angelegte Seiten (Entwürfe bekommen bewusst keine) - Login: neue Einstellung für die Cookie-Domain, damit die Anmeldung auf beiden Seiten gilt; nach dem Discord-Login landet man wieder auf der Seite, von der man gestartet ist (vorher immer auf der Hauptadresse) - Alles greift erst, wenn beide Adressen im Setup eingetragen sind — bis dahin verhält sich die Anwendung unverändert Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
// Rahmen der Bot-Produktseite — eigene Navigation, orange Leitfarbe.
|
||||
// Das Dashboard ist dieselbe Setup-Seite wie bisher, nur unter /dashboard.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
|
||||
import { apiGet } from './api.js';
|
||||
import BotLanding from './pages/BotLanding.jsx';
|
||||
import BotCommands from './pages/BotCommands.jsx';
|
||||
import Settings from './pages/Settings.jsx';
|
||||
import Impressum from './pages/Impressum.jsx';
|
||||
import Datenschutz from './pages/Datenschutz.jsx';
|
||||
import { IconMenu, IconX } from './icons.jsx';
|
||||
|
||||
export default function BotApp() {
|
||||
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [drawer, setDrawer] = useState(false);
|
||||
const [hub, setHub] = useState(null);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => { setDrawer(false); }, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet('/api/me')
|
||||
.then((data) => setMe({ scopes: [], ...data, loading: false }))
|
||||
.catch(() => setMe({ user: null, admin: false, scopes: [], loading: false }));
|
||||
apiGet('/api/legal').then((d) => setHub(d.publicUrl)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const canDashboard = me.admin || me.scopes?.length > 0;
|
||||
|
||||
return (
|
||||
<div className="app site-bot">
|
||||
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
|
||||
<NavLink to="/" className="nav-logo">
|
||||
D4RKBOT<span>.</span>
|
||||
</NavLink>
|
||||
|
||||
<nav className="nav-links">
|
||||
<NavLink to="/features">Funktionen</NavLink>
|
||||
<NavLink to="/commands">Befehle</NavLink>
|
||||
{canDashboard && <NavLink to="/dashboard">Dashboard</NavLink>}
|
||||
{hub && <a href={hub}>Community</a>}
|
||||
</nav>
|
||||
|
||||
<div className="auth">
|
||||
{me.loading ? null : me.user ? (
|
||||
<span className="user-chip">
|
||||
{me.user.avatar && <img className="avatar" src={me.user.avatar} alt="" />}
|
||||
<span className="username">{me.user.username}</span>
|
||||
<a className="logout" href="/auth/logout" title="Abmelden">
|
||||
<IconX size={13} />
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<a className="btn-login" href="/auth/login">Anmelden</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className="burger" onClick={() => setDrawer((d) => !d)} aria-label="Menü">
|
||||
{drawer ? <IconX size={20} /> : <IconMenu size={20} />}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{drawer && (
|
||||
<nav className="drawer" onClick={() => setDrawer(false)}>
|
||||
<NavLink to="/features">Funktionen</NavLink>
|
||||
<NavLink to="/commands">Befehle</NavLink>
|
||||
{canDashboard && <NavLink to="/dashboard">Dashboard</NavLink>}
|
||||
{hub && <a href={hub}>Community</a>}
|
||||
{me.user
|
||||
? <a href="/auth/logout">Abmelden</a>
|
||||
: <a href="/auth/login">Anmelden</a>}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<main className="main">
|
||||
<Routes>
|
||||
<Route path="/" element={<BotLanding />} />
|
||||
<Route path="/features" element={<BotLanding />} />
|
||||
<Route path="/commands" element={<BotCommands />} />
|
||||
<Route path="/dashboard" element={<Settings me={me} />} />
|
||||
<Route path="/impressum" element={<Impressum />} />
|
||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
<span className="footer-logo">D4RKBOT<span>.</span></span>
|
||||
<span className="footer-sep">//</span>
|
||||
<a href="https://git.d4rkst3r.de/D4rkst3r/d4rkbot" target="_blank" rel="noreferrer">
|
||||
Quellcode
|
||||
</a>
|
||||
<span className="footer-sep">//</span>
|
||||
<NavLink to="/impressum">Impressum</NavLink>
|
||||
<NavLink to="/datenschutz">Datenschutz</NavLink>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export const IconBrand = make(Palette);
|
||||
export const IconFeeds = make(Megaphone);
|
||||
export const IconCommunity = make(Users);
|
||||
export const IconRoles = make(Shield);
|
||||
export const IconShield = make(Shield);
|
||||
export const IconSupport = make(Ticket);
|
||||
export const IconApplications = make(FileText);
|
||||
export const IconComposer = make(Pencil);
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App.jsx';
|
||||
import BotApp from './BotApp.jsx';
|
||||
import { IS_BOT } from './site.js';
|
||||
import '@fontsource/bebas-neue';
|
||||
import '@fontsource/barlow-condensed/300.css';
|
||||
import '@fontsource/barlow-condensed/400.css';
|
||||
@@ -9,10 +11,12 @@ import '@fontsource/barlow-condensed/600.css';
|
||||
import '@fontsource/share-tech-mono';
|
||||
import './style.css';
|
||||
|
||||
// Zwei Gesichter, eine Anwendung: Der Server sagt über data-site am <body>,
|
||||
// ob gerade die Produktseite oder der Community-Hub gefragt ist.
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
{IS_BOT ? <BotApp /> : <App />}
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Übersicht aller Slash-Befehle des Bots.
|
||||
import { useEffect } from 'react';
|
||||
import { IconLock } from '../icons.jsx';
|
||||
|
||||
const GROUPS = [
|
||||
['Für alle', [
|
||||
['/rank', 'Zeigt deinen Rang, deine XP und den Fortschritt zum nächsten Level.'],
|
||||
['/wunsch idee:…', 'Reicht einen Feature-Wunsch ein — landet im Voting-Kanal und auf der Roadmap.'],
|
||||
['/bug titel:… beschreibung:…', 'Meldet einen Fehler als Issue im Repository. Wird er geschlossen, bekommst du eine Nachricht.'],
|
||||
['/geburtstag setzen tag:… monat:…', 'Trägt deinen Geburtstag ein. Der Bot gratuliert morgens und vergibt eine Rolle für den Tag.'],
|
||||
['/geburtstag entfernen', 'Trägt ihn wieder aus.'],
|
||||
['/remind dauer:… text:…', 'Erinnert dich per Direktnachricht — etwa „2h" oder „30m".'],
|
||||
['/tag name:…', 'Ruft einen gespeicherten Textbaustein ab, mit Vorschlägen beim Tippen.'],
|
||||
['/ping', 'Lebenszeichen des Bots.'],
|
||||
]],
|
||||
['Moderation', [
|
||||
['/warn user:… grund:…', 'Verwarnt jemanden, schickt eine Nachricht und schreibt es ins Protokoll.'],
|
||||
['/warns user:…', 'Zeigt die Verwarnungs-Historie.'],
|
||||
['/timeout user:… dauer:…', 'Setzt eine Auszeit.'],
|
||||
['/purge anzahl:…', 'Löscht mehrere Nachrichten auf einmal.'],
|
||||
]],
|
||||
['Einrichtung', [
|
||||
['/giveaway preis:… dauer:… gewinner:…', 'Startet eine Verlosung mit Teilnahme-Knopf und automatischer Ziehung.'],
|
||||
['/ticket-setup', 'Postet den Knopf, über den Support-Tickets als private Threads entstehen.'],
|
||||
['/playtester-setup', 'Postet den Bewerbungs-Knopf für das Playtester-Programm.'],
|
||||
['/devlog-backfill', 'Holt ältere Devlog-Nachrichten nachträglich ins Archiv.'],
|
||||
['/galerie-backfill', 'Dasselbe für den Screenshot-Kanal.'],
|
||||
]],
|
||||
];
|
||||
|
||||
export default function BotCommands() {
|
||||
useEffect(() => { window.scrollTo(0, 0); }, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">BEFEHLE</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Referenz</div>
|
||||
<h1 className="page-title">Befehle</h1>
|
||||
<p className="page-subtitle">
|
||||
Alle Slash-Befehle im Überblick. Vieles läuft zusätzlich über Knöpfe
|
||||
direkt in den Nachrichten.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{GROUPS.map(([group, commands]) => (
|
||||
<div className="roadmap-group" key={group}>
|
||||
<h2 className="settings-title">
|
||||
// {group}
|
||||
{group === 'Moderation' && <IconLock size={13} />}
|
||||
{group === 'Einrichtung' && <IconLock size={13} />}
|
||||
</h2>
|
||||
<div className="cmd-list">
|
||||
{commands.map(([name, desc]) => (
|
||||
<div className="cmd-row" key={name}>
|
||||
<code className="cmd-name">{name}</code>
|
||||
<span className="cmd-desc">{desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<p className="notice">
|
||||
Moderations- und Einrichtungs-Befehle sind Mitgliedern mit den passenden
|
||||
Discord-Rechten vorbehalten.
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Produktseite des Bots: was er kann, wie er aussieht, wo es weitergeht.
|
||||
// Bewusst ohne „Einladen"-Button — der Bot ist auf einen Server ausgelegt.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { apiGet } from '../api.js';
|
||||
import {
|
||||
IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway,
|
||||
IconBirthday, IconShield, IconMagic, IconKey, IconArrowRight, IconBot,
|
||||
} from '../icons.jsx';
|
||||
|
||||
const HIGHLIGHTS = [
|
||||
[IconDevlog, 'Devlogs & Feeds', 'Entwicklungs-Berichte landen als gebrandetes Embed im Discord und werden gleichzeitig durchsuchbar archiviert. Commits und Releases aus Gitea genauso.'],
|
||||
[IconServer, 'Server-Monitor', 'Über 300 Spiele per gamedig, dazu FiveM und einfache Erreichbarkeits-Checks. Ein Live-Embed pro Server, Alarm bei Ausfall, Spielerzahl in der Bot-Präsenz.'],
|
||||
[IconLevel, 'Level & Aktivität', 'XP fürs Mitreden mit Rollen-Belohnungen, öffentliche Bestenliste und Aktivitäts-Statistik des Servers.'],
|
||||
[IconRoles, 'Rollen-Menüs', 'Selbstbedienung per Knopfdruck — im Discord und im Browser. Farben pro Knopf, wahlweise exklusiv.'],
|
||||
[IconSupport, 'Support & Kontakt', 'Tickets als private Threads mit Gesprächsprotokoll, dazu Modmail: eine Direktnachricht an den Bot landet beim Team.'],
|
||||
[IconShield, 'Moderation', 'Verwarnungen mit Verlauf, Auszeiten, Aufräumen — alles im Mod-Log, dazu Protokoll für Beitritte, Namens- und Rollenwechsel.'],
|
||||
[IconGiveaway, 'Verlosungen', 'Teilnahme per Knopf, automatische Ziehung, Neuauslosung wenn sich niemand meldet.'],
|
||||
[IconBirthday, 'Geburtstage', 'Wer mag, trägt seinen Tag ein — der Bot gratuliert morgens und vergibt eine Rolle für den Tag.'],
|
||||
[IconMagic, 'Willkommens-Karten', 'Neue Mitglieder bekommen ein gerendertes Bild mit Avatar und Mitglieds-Nummer.'],
|
||||
];
|
||||
|
||||
export default function BotLanding() {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [hub, setHub] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet('/api/legal').then((d) => setHub(d)).catch(() => {});
|
||||
apiGet('/api/serverstats').then(setStatus).catch(() => {});
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header bot-hero">
|
||||
<div className="page-bg-text">BOT</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Selbst gebaut, keine Paywall</div>
|
||||
<h1 className="page-title">
|
||||
Ein Discord-Bot,<br />der alles macht
|
||||
</h1>
|
||||
<p className="page-subtitle">
|
||||
Devlogs, Level, Moderation, Server-Monitoring, Verlosungen, Tickets —
|
||||
was sonst drei Bots mit Abo-Modell erledigen, läuft hier in einem,
|
||||
mit eigenem Webinterface.
|
||||
</p>
|
||||
<div className="home-cta">
|
||||
<Link className="btn btn-save with-icon" to="/features">
|
||||
Alle Funktionen <IconArrowRight size={15} />
|
||||
</Link>
|
||||
<Link className="btn btn-ghost with-icon" to="/commands">
|
||||
<IconBot /> Befehle ansehen
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
<div className="srv-summary">
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">16</span>
|
||||
<span className="srv-sum-label">Slash-Befehle</span>
|
||||
</div>
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">300<span className="srv-sum-of">+</span></span>
|
||||
<span className="srv-sum-label">Spiele im Monitor</span>
|
||||
</div>
|
||||
<div className="srv-sum-item">
|
||||
<span className="srv-sum-n">0<span className="srv-sum-of"> €</span></span>
|
||||
<span className="srv-sum-label">Kosten, keine Premium-Stufen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="settings-title" style={{ marginTop: '2.5rem' }}>// Was er kann</h2>
|
||||
<div className="home-tiles">
|
||||
{HIGHLIGHTS.map(([Icon, title, text]) => (
|
||||
<div className="card home-tile" key={title}>
|
||||
<span className="home-tile-icon"><Icon size={24} /></span>
|
||||
<span className="home-tile-title">{title}</span>
|
||||
<span className="home-tile-desc">{text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
|
||||
<h2 className="settings-title">// Für Entwickler</h2>
|
||||
<div className="home-tiles">
|
||||
<div className="card home-tile">
|
||||
<span className="home-tile-icon"><IconKey size={24} /></span>
|
||||
<span className="home-tile-title">API & Single Sign-On</span>
|
||||
<span className="home-tile-desc">
|
||||
Eigene Skripte posten über die API v1 mit Bearer-Schlüsseln und Bereichs-Rechten.
|
||||
Andere Dienste nutzen den Discord-Login des Bots mit und bekommen die Rollen
|
||||
gleich mitgeliefert.
|
||||
</span>
|
||||
</div>
|
||||
<div className="card home-tile">
|
||||
<span className="home-tile-icon"><IconServer size={24} /></span>
|
||||
<span className="home-tile-title">Läuft überall</span>
|
||||
<span className="home-tile-desc">
|
||||
Node und SQLite in einem Container, ein Volume für die Daten.
|
||||
Nächtliche Sicherungen inklusive — von der Datenbank und allen Repositories.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hub?.publicUrl && (
|
||||
<p className="notice" style={{ marginTop: '2rem' }}>
|
||||
Der Bot läuft für die D4RKST3R-Community
|
||||
{status?.members ? ` mit ${status.members} Mitgliedern` : ''} —
|
||||
{' '}<a href={hub.publicUrl} style={{ textDecoration: 'underline' }}>zum Community-Hub</a>.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1311,6 +1311,28 @@ export default function Settings({ me }) {
|
||||
<label>Gitea-URL</label>
|
||||
<input type="text" placeholder="https://git.d4rkst3r.de" value={form.gitea_url} onChange={(e) => setForm({ ...form, gitea_url: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Zwei Seiten</h2>
|
||||
<p className="section-intro">
|
||||
Community-Hub und Bot-Produktseite auf getrennte Adressen legen. Solange
|
||||
beide Felder leer sind, läuft alles unverändert auf der öffentlichen URL.
|
||||
Sind sie gesetzt, leitet der Server falsch aufgerufene Seiten automatisch
|
||||
auf die richtige Adresse um.
|
||||
</p>
|
||||
<div className="field">
|
||||
<label>Community-Hub <span className="field-hint">Devlogs, Roadmap, Server, Profile — Ziel für Devlog-Buttons, RSS und Link-Vorschauen</span></label>
|
||||
<input type="text" placeholder="https://hub.d4rkst3r.de" value={form.hub_url ?? ''} onChange={(e) => setForm({ ...form, hub_url: e.target.value })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Bot-Produktseite <span className="field-hint">Funktionen, Befehle und dieses Dashboard</span></label>
|
||||
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.bot_url ?? ''} onChange={(e) => setForm({ ...form, bot_url: e.target.value })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Login-Domain <span className="field-hint">z. B. <code>.d4rkst3r.de</code> — dann gilt die Anmeldung auf beiden Seiten; leer = getrennte Logins</span></label>
|
||||
<input type="text" placeholder=".d4rkst3r.de" value={form.cookie_domain ?? ''} onChange={(e) => setForm({ ...form, cookie_domain: e.target.value })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Bug-Repo <span className="field-hint">Ziel für /bug-Issues{data.status.giteaTokenConfigured ? '' : ' — ⚠️ kein GITEA_API_TOKEN, /bug ist aus'}</span></label>
|
||||
<input type="text" placeholder="D4rkst3r/EcoGame" value={form.bug_report_repo} onChange={(e) => setForm({ ...form, bug_report_repo: e.target.value })} />
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Welche der beiden Seiten läuft gerade?
|
||||
//
|
||||
// Der Server schreibt beim Ausliefern ein data-site="bot|hub" an den <body>.
|
||||
// Fehlt das (alter Build, lokale Entwicklung), fällt es auf den Hub zurück —
|
||||
// so verhält sich alles wie bisher, solange die Domains nicht getrennt sind.
|
||||
export const SITE = document.body?.dataset?.site === 'bot' ? 'bot' : 'hub';
|
||||
export const IS_BOT = SITE === 'bot';
|
||||
export const IS_HUB = SITE === 'hub';
|
||||
@@ -1569,3 +1569,37 @@ button.with-icon, a.with-icon { justify-content: center; }
|
||||
max-height: 26rem; overflow-y: auto;
|
||||
background: var(--bg2);
|
||||
}
|
||||
|
||||
/* ── BOT-PRODUKTSEITE ──────────────────────────────── */
|
||||
/* Gleiche Marke, aber Orange als Leitfarbe — beim Domainwechsel sofort erkennbar */
|
||||
.site-bot {
|
||||
--neon: #ff4d00;
|
||||
--neon2: #f5c518;
|
||||
--border: rgba(255, 77, 0, 0.15);
|
||||
}
|
||||
.site-bot .page-bg-text { color: rgba(255, 77, 0, .045); }
|
||||
.site-bot .page-header-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 77, 0, .12) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 77, 0, .12) 1px, transparent 1px);
|
||||
}
|
||||
.site-bot .particle { background: #ff4d00; }
|
||||
.bot-hero { min-height: 460px; }
|
||||
|
||||
/* Befehlsliste */
|
||||
.cmd-list { display: flex; flex-direction: column; gap: .5rem; }
|
||||
.cmd-row {
|
||||
display: flex; gap: 1.2rem; align-items: baseline; flex-wrap: wrap;
|
||||
background: var(--bg2); border: 1px solid rgba(255, 255, 255, .06);
|
||||
border-radius: 10px; padding: .7rem 1rem;
|
||||
transition: border-color .2s, background .2s;
|
||||
}
|
||||
.cmd-row:hover { border-color: var(--border); background: var(--bg3); }
|
||||
.cmd-name {
|
||||
font-family: var(--mono); font-size: .82rem; color: var(--neon);
|
||||
white-space: nowrap; flex: none; min-width: 15rem;
|
||||
}
|
||||
.cmd-desc { color: var(--muted); font-weight: 300; flex: 1; min-width: 15rem; }
|
||||
@media (max-width: 640px) {
|
||||
.cmd-name { min-width: 0; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user