Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 585e5d5455 | |||
| b3ca39ddc2 | |||
| 9675e73406 | |||
| b81c2042e9 | |||
| 677eac2632 | |||
| fde5ab91c8 | |||
| ffcfce2b31 | |||
| 27a8240d3f | |||
| d70c2ab1b9 | |||
| 2809d18c12 | |||
| 1555dc02e9 | |||
| b71d99b048 | |||
| 6c83e4a7ee | |||
| c63955f8a5 | |||
| 37badea913 | |||
| 839bd24309 | |||
| 3ff25d3f7e | |||
| ac6f11cefa | |||
| 6d0cada610 | |||
| 3c23aae864 | |||
| c65395b780 | |||
| 89ab27540d | |||
| 2e4a6ae7c4 | |||
| 23a5714119 | |||
| e55f17f0c8 | |||
| b7a9091183 | |||
| 898f2e0b58 | |||
| 274762c825 | |||
| 26d7c4e6b3 | |||
| f9d8fc5b82 | |||
|
|
339f03e38e | ||
|
|
3c7b13ee5f | ||
|
|
61f588d733 | ||
|
|
6ef62f069c | ||
|
|
c7cb02cf2d | ||
|
|
9d85044b7f | ||
|
|
cd5a179125 | ||
|
|
36dff70f98 | ||
|
|
6a2b6e46e0 | ||
|
|
f6fee70cd0 | ||
|
|
f602adb4c1 | ||
|
|
9745d38995 |
52
README.md
52
README.md
@@ -1,6 +1,6 @@
|
||||
# Facebook Post Tracker
|
||||
|
||||
Eine Chrome/Firefox-Extension mit Docker-Backend zum Verwalten und Abhaken von Facebook-Beiträgen über mehrere Browser-Profile hinweg.
|
||||
Eine Chrome/Firefox-Extension mit Docker-Backend und Single-Page-Webinterface zum Verwalten und Abhaken von Facebook-Beiträgen über mehrere Browser-Profile hinweg.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -46,6 +46,14 @@ docker-compose up -d
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
**Login-Schutz aktivieren:** Setze Benutzername/Passwort in `docker-compose.yml` via `AUTH_USERNAME` und `AUTH_PASSWORD`. Beispiel (bereits eingetragen):
|
||||
```
|
||||
environment:
|
||||
- AUTH_USERNAME=admin
|
||||
- AUTH_PASSWORD=changeme
|
||||
```
|
||||
Denke daran, eigene Werte zu hinterlegen.
|
||||
|
||||
Das Backend läuft nun auf `http://localhost:3000`
|
||||
Das Web-Interface ist erreichbar unter `http://localhost:8080`
|
||||
|
||||
@@ -81,15 +89,31 @@ Das Web-Interface ist erreichbar unter `http://localhost:8080`
|
||||
- Siehst du den Status: `X/Y Profile ✓/✗`
|
||||
- Klicke auf "Abhaken", wenn noch nicht für dein Profil erledigt
|
||||
|
||||
### Im Web-Interface:
|
||||
### Im Web-Interface (SPA):
|
||||
|
||||
1. Öffne `http://localhost:8080`
|
||||
2. Wähle dein aktuelles Profil aus
|
||||
3. **Offene Beiträge**: Zeigt alle Beiträge, die noch nicht von allen Profilen abgehakt wurden
|
||||
4. **Alle Beiträge**: Zeigt alle erfassten Beiträge
|
||||
5. Klicke auf "Beitrag öffnen & abhaken", um:
|
||||
- Den Beitrag in neuem Tab zu öffnen
|
||||
- Automatisch für dein Profil abzuhaken
|
||||
Öffne `http://localhost:8080` und nutze die Navigation oben, um zwischen den Views zu wechseln. Die SPA-Views hängen am Query-Parameter `view`:
|
||||
|
||||
- `?view=posts` (Standard): Offene/alle Beiträge
|
||||
- `?view=dashboard`: Kennzahlen & Statistiken
|
||||
- `?view=settings`: Einstellungen/Schwellwerte
|
||||
- `?view=bookmarks`: Bookmark-Suche (nicht täglich)
|
||||
- `?view=automation`: Request-Automationen (HTTP/Email/Flows)
|
||||
- `?view=daily-bookmarks`: Daily Bookmarks (tägliche Liste mit Platzhaltern)
|
||||
|
||||
Direktlinks wie `automation.html` oder `daily-bookmarks.html` leiten nur noch auf die passenden SPA-Views um.
|
||||
|
||||
### Daily Bookmarks (SPA-View)
|
||||
|
||||
- View: `http://localhost:8080/?view=daily-bookmarks`
|
||||
- Lege Links mit dynamischen Platzhaltern an (z. B. `{{day}}`, `{{date-1}}`, `{{counter:123}}`)
|
||||
- Markiere Bookmarks einmal pro Tag als erledigt; Status landet in SQLite
|
||||
- Links werden pro gewähltem Tag aufgelöst, z. B. `https://www.test.de/tag-{{day}}/`
|
||||
|
||||
### Automationen (SPA-View)
|
||||
|
||||
- View: `http://localhost:8080/?view=automation`
|
||||
- HTTP-/Email-/Flow-Automationen mit Platzhaltern, Intervalen, Jitter
|
||||
- Echtzeit-Updates via SSE (`/api/events`)
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
@@ -182,10 +206,18 @@ npm run dev
|
||||
## Technologie-Stack
|
||||
|
||||
- **Backend**: Node.js, Express, SQLite (better-sqlite3)
|
||||
- **Web-Interface**: Vanilla JavaScript, HTML, CSS
|
||||
- **Web-Interface**: Vanilla JavaScript, HTML, CSS (SPA)
|
||||
- **Extension**: Manifest V3 (Chrome & Firefox kompatibel)
|
||||
- **Deployment**: Docker, docker-compose
|
||||
|
||||
## Updates & Abhängigkeiten
|
||||
|
||||
- **NPM-Dependencies (Backend)**: `cd backend && npm install && npm outdated` prüfen. Upgrade per `npm update` oder gezielt Versionen anheben, anschließend Tests/Manuallauf.
|
||||
- **Frontend-Libs**: `web/vendor/list.min.js` wird lokal gebundled; bei Update die neue Version in `web/vendor` legen und Caching beachten.
|
||||
- **Browser-Extension**: Manifest V3, Icons/Assets im Ordner `extension/`. Bei Browser-Updates ggf. Permissions/Manifest anpassen, danach in Chrome/Firefox neu laden.
|
||||
- **Docker-Images**: `docker-compose build` neu bauen nach Dependency-Updates. Basis-Images im `backend/Dockerfile` prüfen.
|
||||
- **SPA-Views**: Navigation über `?view=`; direkte HTML-Seiten (`automation.html`, `daily-bookmarks.html`, `bookmarks.html`, `dashboard.html`, `settings.html`) sind Redirect-Stubs. Neue Views sollten in `index.html` + `app.js` (Navigation) ergänzt werden.
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
@@ -1,10 +1,13 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV CXXFLAGS="-std=c++20"
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install --production
|
||||
RUN apk add --no-cache python3 make g++ \
|
||||
&& npm install --production
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
1752
backend/package-lock.json
generated
Normal file
1752
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,9 @@
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"uuid": "^9.0.1"
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"uuid": "^9.0.1",
|
||||
"nodemailer": "^7.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
|
||||
4366
backend/server.js
4366
backend/server.js
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,12 @@ services:
|
||||
- "3001:3000"
|
||||
volumes:
|
||||
- ./backend/server.js:/app/server.js:ro
|
||||
- db-data:/app/data
|
||||
- /opt/docker/posttracker/data:/app/data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- AUTH_USERNAME=admin
|
||||
- AUTH_PASSWORD=RQWhrqowg3rihr
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
restart: unless-stopped
|
||||
@@ -24,6 +26,28 @@ services:
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
|
||||
sqlite-web:
|
||||
image: coleifer/sqlite-web
|
||||
container_name: fb-sqlite-web
|
||||
command:
|
||||
- sqlite_web
|
||||
- /data/tracker.db
|
||||
- --host
|
||||
- 0.0.0.0
|
||||
- --port
|
||||
- "8080"
|
||||
ports:
|
||||
- "8083:8080"
|
||||
volumes:
|
||||
- /opt/docker/posttracker/data:/data
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
2657
extension/content.js
2657
extension/content.js
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Facebook Post Tracker",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Track Facebook posts across multiple profiles",
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
||||
File diff suppressed because one or more lines are too long
446
fb_feed.txt
446
fb_feed.txt
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
noScreenshot.png
BIN
noScreenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
98
package-lock.json
generated
Normal file
98
package-lock.json
generated
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "fb",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-loose": "^8.5.2",
|
||||
"esprima": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-loose": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
|
||||
"integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
package.json
Normal file
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-loose": "^8.5.2",
|
||||
"esprima": "^4.0.1"
|
||||
}
|
||||
}
|
||||
BIN
tracker.db
Normal file
BIN
tracker.db
Normal file
Binary file not shown.
@@ -1,16 +1,45 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY posts.html /usr/share/nginx/html/
|
||||
COPY dashboard.html /usr/share/nginx/html/
|
||||
COPY settings.html /usr/share/nginx/html/
|
||||
COPY bookmarks.html /usr/share/nginx/html/
|
||||
COPY daily-bookmarks.html /usr/share/nginx/html/
|
||||
COPY automation.html /usr/share/nginx/html/
|
||||
COPY login.html /usr/share/nginx/html/
|
||||
COPY style.css /usr/share/nginx/html/
|
||||
COPY dashboard.css /usr/share/nginx/html/
|
||||
COPY settings.css /usr/share/nginx/html/
|
||||
COPY daily-bookmarks.css /usr/share/nginx/html/
|
||||
COPY automation.css /usr/share/nginx/html/
|
||||
COPY app.js /usr/share/nginx/html/
|
||||
COPY dashboard.js /usr/share/nginx/html/
|
||||
COPY settings.js /usr/share/nginx/html/
|
||||
COPY daily-bookmarks.js /usr/share/nginx/html/
|
||||
COPY automation.js /usr/share/nginx/html/
|
||||
COPY login.js /usr/share/nginx/html/
|
||||
COPY vendor /usr/share/nginx/html/vendor/
|
||||
COPY assets /usr/share/nginx/html/assets/
|
||||
|
||||
RUN set -e; \
|
||||
ASSET_VERSION="$(sha256sum \
|
||||
/usr/share/nginx/html/app.js \
|
||||
/usr/share/nginx/html/dashboard.js \
|
||||
/usr/share/nginx/html/settings.js \
|
||||
/usr/share/nginx/html/daily-bookmarks.js \
|
||||
/usr/share/nginx/html/automation.js \
|
||||
/usr/share/nginx/html/login.js \
|
||||
/usr/share/nginx/html/vendor/list.min.js \
|
||||
/usr/share/nginx/html/style.css \
|
||||
/usr/share/nginx/html/dashboard.css \
|
||||
/usr/share/nginx/html/settings.css \
|
||||
/usr/share/nginx/html/daily-bookmarks.css \
|
||||
/usr/share/nginx/html/automation.css \
|
||||
| sha256sum | awk '{print $1}')"; \
|
||||
sed -i "s/__ASSET_VERSION__/${ASSET_VERSION}/g" /usr/share/nginx/html/index.html /usr/share/nginx/html/login.html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
2524
web/app.js
2524
web/app.js
File diff suppressed because it is too large
Load Diff
749
web/automation.css
Normal file
749
web/automation.css
Normal file
@@ -0,0 +1,749 @@
|
||||
:root {
|
||||
--automation-bg: #f0f2f5;
|
||||
--automation-card: #ffffff;
|
||||
--automation-card-soft: #f7f8fa;
|
||||
--automation-border: #e4e6eb;
|
||||
--automation-muted: #6b7280;
|
||||
--automation-text: #0f172a;
|
||||
--automation-accent: #1877f2;
|
||||
--automation-accent-2: #2563eb;
|
||||
--automation-danger: #dc2626;
|
||||
--automation-success: #059669;
|
||||
--automation-shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
--automation-shadow-md: 0 14px 45px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.automation-view * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.automation-view {
|
||||
color: var(--automation-text);
|
||||
}
|
||||
|
||||
.automation-view .auto-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.automation-view .auto-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--automation-card);
|
||||
border: 1px solid var(--automation-border);
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
box-shadow: var(--automation-shadow-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.automation-view .auto-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(120deg, rgba(24, 119, 242, 0.08), rgba(37, 99, 235, 0.05));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.automation-view .auto-hero > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.automation-view .hero-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.automation-view .hero-text h1 {
|
||||
margin: 4px 0;
|
||||
font-size: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--automation-text);
|
||||
}
|
||||
|
||||
.automation-view .eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--automation-accent-2);
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.automation-view .back-link {
|
||||
color: var(--automation-accent);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.automation-view .back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.automation-view .hero-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.automation-view .hero-stats {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.automation-view .stat {
|
||||
background: var(--automation-card-soft);
|
||||
border: 1px solid var(--automation-border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
box-shadow: var(--automation-shadow-sm);
|
||||
}
|
||||
|
||||
.automation-view .stat-label {
|
||||
color: var(--automation-muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.automation-view .stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.automation-view .panel {
|
||||
background: var(--automation-card);
|
||||
border: 1px solid var(--automation-border);
|
||||
border-radius: 12px;
|
||||
padding: 18px 18px 16px;
|
||||
box-shadow: var(--automation-shadow-sm);
|
||||
}
|
||||
|
||||
.automation-view .panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.automation-view .panel-eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 12px;
|
||||
color: var(--automation-muted);
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.automation-view .panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.automation-view .form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.automation-view .field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.automation-view .field.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.automation-view .field.inline {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.automation-view .field[data-section] {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.automation-view label {
|
||||
font-weight: 600;
|
||||
color: var(--automation-text);
|
||||
}
|
||||
|
||||
.automation-view input,
|
||||
.automation-view textarea,
|
||||
.automation-view select {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--automation-border);
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
color: var(--automation-text);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.automation-view input:focus,
|
||||
.automation-view textarea:focus,
|
||||
.automation-view select:focus {
|
||||
border-color: var(--automation-accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.automation-view textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.automation-view small {
|
||||
color: var(--automation-muted);
|
||||
}
|
||||
|
||||
.automation-view .template-hint {
|
||||
border: 1px dashed var(--automation-border);
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
color: var(--automation-muted);
|
||||
}
|
||||
|
||||
.automation-view .template-title {
|
||||
margin: 0 0 6px;
|
||||
color: var(--automation-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.automation-view .template-copy {
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.automation-view .form-status {
|
||||
min-height: 22px;
|
||||
color: var(--automation-muted);
|
||||
}
|
||||
|
||||
.automation-view .form-status.error {
|
||||
color: var(--automation-danger);
|
||||
}
|
||||
|
||||
.automation-view .form-status.success {
|
||||
color: var(--automation-success);
|
||||
}
|
||||
|
||||
.automation-view .table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.automation-view .auto-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.automation-view .auto-table th,
|
||||
.automation-view .auto-table td {
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--automation-border);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.automation-view .auto-table th {
|
||||
color: var(--automation-muted);
|
||||
font-weight: 700;
|
||||
background: var(--automation-card-soft);
|
||||
}
|
||||
|
||||
.automation-view .auto-table th[data-sort-column="runs"],
|
||||
.automation-view .auto-table td.runs-count {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.automation-view .auto-table .sort-indicator {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: var(--automation-muted);
|
||||
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='black' d='M4 6l4 4 4-4z'/></svg>") no-repeat center / contain;
|
||||
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='black' d='M4 6l4 4 4-4z'/></svg>") no-repeat center / contain;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.automation-view .auto-table th.sort-asc .sort-indicator {
|
||||
background-color: var(--automation-accent-2);
|
||||
transform: rotate(180deg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.automation-view .auto-table th.sort-desc .sort-indicator {
|
||||
background-color: var(--automation-accent-2);
|
||||
transform: rotate(0deg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.automation-view .table-filter-row th {
|
||||
background: var(--automation-card);
|
||||
}
|
||||
|
||||
.automation-view .table-filter-row input,
|
||||
.automation-view .table-filter-row select {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--automation-border);
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
color: var(--automation-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.automation-view .auto-table tr.is-selected td {
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.automation-view .row-title {
|
||||
font-weight: 700;
|
||||
color: var(--automation-text);
|
||||
}
|
||||
|
||||
.automation-view .row-sub {
|
||||
color: var(--automation-muted);
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.automation-view .badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
background: #eef2ff;
|
||||
color: var(--automation-accent-2);
|
||||
border: 1px solid rgba(37, 99, 235, 0.28);
|
||||
}
|
||||
|
||||
.automation-view .badge.success {
|
||||
background: rgba(5, 150, 105, 0.12);
|
||||
color: var(--automation-success);
|
||||
border-color: rgba(5, 150, 105, 0.35);
|
||||
}
|
||||
|
||||
.automation-view .badge.error {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
color: var(--automation-danger);
|
||||
border-color: rgba(220, 38, 38, 0.35);
|
||||
}
|
||||
|
||||
.automation-view .row-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.automation-view .hidden-value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.automation-view .icon-btn {
|
||||
border: 1px solid var(--automation-border);
|
||||
background: var(--automation-card-soft);
|
||||
color: var(--automation-text);
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.automation-view .icon-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.95;
|
||||
border-color: #d1d5db;
|
||||
box-shadow: var(--automation-shadow-sm);
|
||||
}
|
||||
|
||||
.automation-view .icon-btn.danger {
|
||||
color: var(--automation-danger);
|
||||
border-color: rgba(220, 38, 38, 0.35);
|
||||
}
|
||||
|
||||
.automation-view .primary-btn,
|
||||
.automation-view .secondary-btn,
|
||||
.automation-view .ghost-btn {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.automation-view .primary-btn {
|
||||
background: linear-gradient(135deg, var(--automation-accent-2), var(--automation-accent));
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 30px rgba(24, 119, 242, 0.25);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.automation-view .secondary-btn {
|
||||
background: #e4e6eb;
|
||||
color: var(--automation-text);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.automation-view .ghost-btn {
|
||||
background: transparent;
|
||||
color: var(--automation-text);
|
||||
border-color: var(--automation-border);
|
||||
}
|
||||
|
||||
.automation-view .primary-btn:hover,
|
||||
.automation-view .secondary-btn:hover,
|
||||
.automation-view .ghost-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.automation-view .list-status,
|
||||
.automation-view .runs-status,
|
||||
.automation-view .import-status {
|
||||
min-height: 20px;
|
||||
color: var(--automation-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.automation-view .filter-input {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--automation-border);
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
color: var(--automation-text);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.automation-view .runs-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.automation-view .run-item {
|
||||
border: 1px solid var(--automation-border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: var(--automation-card-soft);
|
||||
box-shadow: var(--automation-shadow-sm);
|
||||
}
|
||||
|
||||
.automation-view .run-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.automation-view .run-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--automation-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.automation-view .run-body {
|
||||
margin: 8px 0 0;
|
||||
color: var(--automation-text);
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.automation-view .runs-hint {
|
||||
color: var(--automation-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.automation-view .import-hint {
|
||||
color: var(--automation-muted);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.automation-view .switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--automation-text);
|
||||
}
|
||||
|
||||
.automation-view .switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.automation-view .switch-slider {
|
||||
width: 42px;
|
||||
height: 22px;
|
||||
background: #d1d5db;
|
||||
border-radius: 999px;
|
||||
position: relative;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.automation-view .switch-slider::after {
|
||||
content: "";
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.automation-view .switch input:checked + .switch-slider {
|
||||
background: var(--automation-accent);
|
||||
}
|
||||
|
||||
.automation-view .switch input:checked + .switch-slider::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.automation-view .switch-label {
|
||||
color: var(--automation-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-controls input[type="time"] {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-sep {
|
||||
color: var(--automation-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid var(--automation-border);
|
||||
color: var(--automation-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-chip button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--automation-muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.automation-view .exclusion-chip button:hover {
|
||||
color: var(--automation-danger);
|
||||
}
|
||||
|
||||
.automation-view .modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.automation-view .modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.automation-view .modal__content {
|
||||
position: relative;
|
||||
background: var(--automation-card);
|
||||
border: 1px solid var(--automation-border);
|
||||
border-radius: 14px;
|
||||
padding: 20px 20px 16px;
|
||||
width: min(1100px, 96vw);
|
||||
max-height: 92vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--automation-shadow-md);
|
||||
}
|
||||
|
||||
.automation-view .modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.automation-view .modal-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.automation-view .modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.automation-view .preview-panel {
|
||||
border: 1px solid var(--automation-border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: var(--automation-card-soft);
|
||||
}
|
||||
|
||||
.automation-view .preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.automation-view .preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.automation-view .preview-block {
|
||||
border: 1px dashed var(--automation-border);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.automation-view .preview-label {
|
||||
margin: 0 0 4px;
|
||||
color: var(--automation-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.automation-view .preview-value {
|
||||
margin: 0;
|
||||
color: var(--automation-text);
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.automation-view .preview-hint {
|
||||
color: var(--automation-muted);
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.automation-view .placeholder-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.automation-view .placeholder-table td {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--automation-border);
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
color: var(--automation-text);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.automation-view .placeholder-table td.placeholder-key {
|
||||
width: 30%;
|
||||
color: var(--automation-muted);
|
||||
}
|
||||
|
||||
.automation-view .placeholder-hint {
|
||||
color: var(--automation-muted);
|
||||
font-size: 12px;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1050px) {
|
||||
.automation-view .form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.automation-view .auto-shell {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.automation-view .panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.automation-view .panel-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.automation-view .hero-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.automation-view .modal {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
21
web/automation.html
Normal file
21
web/automation.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Automationen – Post Tracker</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<script>
|
||||
(function redirectToShell() {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
params.set('view', 'automation');
|
||||
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Weiterleitung zur Automations-Ansicht…</p>
|
||||
</body>
|
||||
</html>
|
||||
1358
web/automation.js
Normal file
1358
web/automation.js
Normal file
File diff suppressed because it is too large
Load Diff
21
web/bookmarks.html
Normal file
21
web/bookmarks.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bookmarks – Post Tracker</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<script>
|
||||
(function redirectToShell() {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
params.set('view', 'bookmarks');
|
||||
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Weiterleitung zur Bookmarks-Ansicht…</p>
|
||||
</body>
|
||||
</html>
|
||||
873
web/daily-bookmarks.css
Normal file
873
web/daily-bookmarks.css
Normal file
@@ -0,0 +1,873 @@
|
||||
:root {
|
||||
--bg: #f0f2f5;
|
||||
--bg-strong: #ffffff;
|
||||
--panel: #ffffff;
|
||||
--panel-strong: #f8fafc;
|
||||
--border: #e5e7eb;
|
||||
--text: #111827;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb;
|
||||
--accent-2: #06b6d4;
|
||||
--danger: #ef4444;
|
||||
--success: #10b981;
|
||||
--shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
.daily-bookmarks-view *,
|
||||
.daily-bookmarks-view *::before,
|
||||
.daily-bookmarks-view *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.daily-bookmarks-view {
|
||||
margin: 0;
|
||||
font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.daily-bookmarks-view a {
|
||||
color: var(--accent-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.daily-bookmarks-view a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.daily-bookmarks-view .daily-shell {
|
||||
max-width: var(--content-max-width, 1600px);
|
||||
margin: 0 auto;
|
||||
padding: 0 18px 36px;
|
||||
}
|
||||
|
||||
.daily-bookmarks-view .hero {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 26px 28px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
align-self: flex-start;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.pill--soft {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.pill--accent {
|
||||
background: linear-gradient(120deg, rgba(37, 99, 235, 0.15), rgba(6, 182, 212, 0.15));
|
||||
color: #0f172a;
|
||||
border-color: rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.hero__title {
|
||||
font-size: clamp(30px, 3vw, 36px);
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.hero__lead {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
max-width: 840px;
|
||||
}
|
||||
|
||||
.hero__lead code {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
padding: 3px 6px;
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
border: 1px solid rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
.auto-open-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(37, 99, 235, 0.12), transparent 42%),
|
||||
radial-gradient(circle at 80% 30%, rgba(6, 182, 212, 0.12), transparent 38%),
|
||||
rgba(15, 23, 42, 0.6);
|
||||
z-index: 30;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.auto-open-overlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.auto-open-overlay__panel {
|
||||
width: min(940px, 100%);
|
||||
background: linear-gradient(150deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.96));
|
||||
border-radius: 22px;
|
||||
padding: 38px 42px 40px;
|
||||
box-shadow: 0 32px 90px rgba(15, 23, 42, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto-open-overlay__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.auto-open-overlay__timer {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 18px 0 8px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.auto-open-overlay__count {
|
||||
font-size: clamp(72px, 12vw, 120px);
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.auto-open-overlay__unit {
|
||||
font-size: 22px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.auto-open-overlay__text {
|
||||
margin: 0 auto;
|
||||
color: #334155;
|
||||
max-width: 700px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.auto-open-overlay__hint {
|
||||
margin: 12px 0 0;
|
||||
color: #475569;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.hero__controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.day-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.day-switch__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.day-switch__day {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.day-switch__sub {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hero__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero__stats {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.bulk-actions label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.auto-open-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.auto-open-toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.bulk-actions select {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.panel__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel__header--row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel__eyebrow {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel__title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.panel__subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list-status {
|
||||
min-height: 18px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.list-status--error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text);
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field textarea:focus {
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.2);
|
||||
}
|
||||
|
||||
.field__hint {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.field--switch {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.switch-control__label {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field--switch input[type='checkbox'] {
|
||||
width: auto;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-preview {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--panel-strong);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.12);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.form-preview__label {
|
||||
margin: 0 0 4px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.form-preview__link {
|
||||
display: inline-block;
|
||||
max-width: 620px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.form-preview__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn,
|
||||
.ghost-btn {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
font-size: 14px;
|
||||
transition: transform 0.1s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: linear-gradient(120deg, var(--accent), var(--accent-2));
|
||||
color: #ffffff;
|
||||
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 45px rgba(34, 211, 238, 0.3);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #f8fafc;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ghost-btn {
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.ghost-btn--today {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-color: rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.ghost-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.primary-btn:disabled,
|
||||
.secondary-btn:disabled,
|
||||
.ghost-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ghost-btn--tiny {
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.placeholder-help {
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.placeholder-help__title {
|
||||
margin: 0 0 6px 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.placeholder-help__list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
min-height: 18px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.form-status--error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.suggestion-box {
|
||||
margin-top: -4px;
|
||||
margin-bottom: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(37, 99, 235, 0.25);
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.suggestion-box__item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.suggestion-box__text {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.suggestion-btn {
|
||||
border: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggestion-btn:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.suggestion-preview {
|
||||
display: inline-block;
|
||||
max-width: 320px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.suggestion-preview:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.bookmark-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.bookmark-table th,
|
||||
.bookmark-table td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.bookmark-table th {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.bookmark-table th.col-marker {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.bookmark-table th.col-created {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.bookmark-table th.col-last {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.bookmark-table tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.bookmark-table tr.is-done td {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border-bottom-color: rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.bookmark-table td:last-child {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bookmark-table tr.is-open td {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-bottom-color: rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.bookmark-table tr.is-inactive td {
|
||||
background: rgba(107, 114, 128, 0.16);
|
||||
border-bottom-color: rgba(107, 114, 128, 0.28);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.bookmark-table tr.is-inactive a {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border-radius: 12px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chip--marker {
|
||||
background: linear-gradient(120deg, rgba(37, 99, 235, 0.12), rgba(6, 182, 212, 0.12));
|
||||
color: #0f172a;
|
||||
border-color: rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.chip--inactive {
|
||||
background: rgba(107, 114, 128, 0.22);
|
||||
color: #0f172a;
|
||||
border-color: rgba(107, 114, 128, 0.32);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.list-summary {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.error-state,
|
||||
.loading-state {
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table-actions .ghost-btn {
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.bookmark-table .note-cell {
|
||||
max-width: 240px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bookmark-table .url-cell {
|
||||
max-width: 420px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bookmark-table .marker-cell {
|
||||
max-width: 220px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
width: min(720px, 92vw);
|
||||
background: var(--bg-strong);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.modal__close {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sort-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sort-btn::after {
|
||||
content: '↕';
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sort-btn.is-active {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sort-btn.is-active::after {
|
||||
content: attr(data-sort-direction);
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.table-filter-row th {
|
||||
background: rgba(37, 99, 235, 0.05);
|
||||
border-bottom: 1px solid rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
.table-filter-row select {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table-filter-row input[type="search"] {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.filter-hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.import-hint {
|
||||
background: #f8fafc;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin: 4px 0 10px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.panel__header--row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hero__controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-preview {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
21
web/daily-bookmarks.html
Normal file
21
web/daily-bookmarks.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Daily Bookmarks</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<script>
|
||||
(function redirectToShell() {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
params.set('view', 'daily-bookmarks');
|
||||
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Weiterleitung zu Daily Bookmarks…</p>
|
||||
</body>
|
||||
</html>
|
||||
1504
web/daily-bookmarks.js
Normal file
1504
web/daily-bookmarks.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,273 +2,20 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dashboard – Post Tracker</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Facebook Post Tracker</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
<script>
|
||||
(function redirectToShell() {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
params.set('view', 'dashboard');
|
||||
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-main">
|
||||
<h1>📊 Dashboard</h1>
|
||||
<a href="?view=posts" class="btn btn-secondary">Zurück zu Beiträgen</a>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<div class="control-group">
|
||||
<label for="timeFilter">Zeitraum:</label>
|
||||
<select id="timeFilter" class="control-select">
|
||||
<option value="all">Alle</option>
|
||||
<option value="today">Heute</option>
|
||||
<option value="week" selected>Diese Woche</option>
|
||||
<option value="month">Dieser Monat</option>
|
||||
<option value="year">Dieses Jahr</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="profileFilter">Profil-Filter:</label>
|
||||
<select id="profileFilter" class="control-select">
|
||||
<option value="all">Alle Profile</option>
|
||||
<option value="1">Profil 1</option>
|
||||
<option value="2">Profil 2</option>
|
||||
<option value="3">Profil 3</option>
|
||||
<option value="4">Profil 4</option>
|
||||
<option value="5">Profil 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="refreshBtn">
|
||||
🔄 Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loading" class="loading">Lade Statistiken...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
|
||||
<div id="dashboardContainer" class="dashboard-container" style="display: none;">
|
||||
|
||||
<!-- SECTION 1: OVERVIEW -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Übersicht</h2>
|
||||
|
||||
<!-- Primary Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-card--primary">
|
||||
<div class="stat-card__icon">📋</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Gesamt Beiträge</div>
|
||||
<div class="stat-card__value" id="totalPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--success">
|
||||
<div class="stat-card__icon">✓</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Abgeschlossen</div>
|
||||
<div class="stat-card__value" id="completedPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--warning">
|
||||
<div class="stat-card__icon">⏳</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">In Bearbeitung</div>
|
||||
<div class="stat-card__value" id="activePosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--danger">
|
||||
<div class="stat-card__icon">⚠️</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Abgelaufen</div>
|
||||
<div class="stat-card__value" id="expiredPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--info">
|
||||
<div class="stat-card__icon">🏆</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Erfolgreich</div>
|
||||
<div class="stat-card__value" id="successfulPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Erfolgsquote</div>
|
||||
<div class="metric-card__value" id="successRateMetric">0%</div>
|
||||
<div class="metric-card__change" id="successRateChange"></div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Ø Bearbeitungszeit</div>
|
||||
<div class="metric-card__value" id="avgCompletionTime">-</div>
|
||||
<div class="metric-card__subtext">bis Abschluss</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Teilnahmen heute</div>
|
||||
<div class="metric-card__value" id="checksToday">0</div>
|
||||
<div class="metric-card__change" id="checksTodayChange"></div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Deadline-Risiko</div>
|
||||
<div class="metric-card__value" id="deadlineRiskValue">0</div>
|
||||
<div class="metric-card__subtext" id="deadlineRiskText">keine Risiken</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SECTION 2: ANALYTICS -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Analyse</h2>
|
||||
|
||||
<div class="charts-row">
|
||||
<!-- Activity Timeline -->
|
||||
<div class="chart-card chart-card--full">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Aktivitätsverlauf</h3>
|
||||
<span class="chart-card__subtitle" id="timelineSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="timelineChart" width="1200" height="280"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<!-- Profile Performance -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Teilnahmen pro Profil</h3>
|
||||
<span class="chart-card__subtitle" id="profileChartSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<div id="profileChart" class="bar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Overview -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Status-Verteilung</h3>
|
||||
<span class="chart-card__subtitle" id="progressChartSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="progressChart" width="400" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<!-- Period Trend -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Teilnahmen-Trend</h3>
|
||||
<span class="chart-card__subtitle" id="trendChartSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="periodTrendChart" width="500" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Comparison -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Profilvergleich</h3>
|
||||
<span class="chart-card__subtitle" id="profileComparisonSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="profileComparisonChart" width="500" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SECTION 3: PERFORMANCE COMPARISONS -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Performance-Vergleiche</h2>
|
||||
|
||||
<div class="comparison-grid">
|
||||
<div class="comparison-card">
|
||||
<h3 class="comparison-card__title">Wochenvergleich</h3>
|
||||
<div class="comparison-card__content" id="weeklyComparison"></div>
|
||||
</div>
|
||||
<div class="comparison-card">
|
||||
<h3 class="comparison-card__title">Monatsvergleich</h3>
|
||||
<div class="comparison-card__content" id="monthlyComparison"></div>
|
||||
</div>
|
||||
<div class="comparison-card">
|
||||
<h3 class="comparison-card__title">Jahresvergleich</h3>
|
||||
<div class="comparison-card__content" id="yearlyComparison"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="success-comparison-grid">
|
||||
<div class="success-comparison-card">
|
||||
<h3 class="success-comparison-card__title">Erfolgsanalyse: Woche</h3>
|
||||
<div class="success-comparison-card__content" id="weeklySuccessComparison"></div>
|
||||
</div>
|
||||
<div class="success-comparison-card">
|
||||
<h3 class="success-comparison-card__title">Erfolgsanalyse: Monat</h3>
|
||||
<div class="success-comparison-card__content" id="monthlySuccessComparison"></div>
|
||||
</div>
|
||||
<div class="success-comparison-card">
|
||||
<h3 class="success-comparison-card__title">Erfolgsanalyse: Jahr</h3>
|
||||
<div class="success-comparison-card__content" id="yearlySuccessComparison"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SECTION 4: DETAILS -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Details</h2>
|
||||
|
||||
<div class="details-grid">
|
||||
<!-- Top Performers -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__header">
|
||||
<h3 class="detail-card__title">Top Performers</h3>
|
||||
<span class="detail-card__badge" id="performersCount">0</span>
|
||||
</div>
|
||||
<div class="detail-card__body">
|
||||
<div id="topPerformers" class="performers-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Deadlines -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__header">
|
||||
<h3 class="detail-card__title">Anstehende Deadlines</h3>
|
||||
<span class="detail-card__badge" id="deadlinesCount">0</span>
|
||||
</div>
|
||||
<div class="detail-card__body">
|
||||
<div id="upcomingDeadlines" class="deadline-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="detail-card detail-card--full">
|
||||
<div class="detail-card__header">
|
||||
<h3 class="detail-card__title">Letzte Aktivitäten</h3>
|
||||
<span class="detail-card__badge" id="activityCount">0</span>
|
||||
</div>
|
||||
<div class="detail-card__body">
|
||||
<div id="recentActivity" class="activity-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
<p>Weiterleitung zum Dashboard…</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
(() => {
|
||||
const API_URL = 'https://fb.srv.medeba-media.de/api';
|
||||
|
||||
// Check if we should redirect to posts view
|
||||
(function checkViewRouting() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const view = params.get('view');
|
||||
if (view === 'posts') {
|
||||
// Remove view parameter and keep other params
|
||||
params.delete('view');
|
||||
const remainingParams = params.toString();
|
||||
window.location.href = 'index.html' + (remainingParams ? '?' + remainingParams : '');
|
||||
}
|
||||
})();
|
||||
const LOGIN_PAGE = 'login.html';
|
||||
|
||||
let posts = [];
|
||||
let filteredPosts = [];
|
||||
@@ -20,6 +10,18 @@ let currentProfileFilter = 'all';
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const HOUR_IN_MS = 60 * 60 * 1000;
|
||||
|
||||
function handleUnauthorized(response) {
|
||||
if (response && response.status === 401) {
|
||||
if (typeof redirectToLogin === 'function') {
|
||||
redirectToLogin();
|
||||
} else {
|
||||
window.location.href = LOGIN_PAGE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function startOfDay(date) {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
@@ -323,21 +325,21 @@ function apiFetch(url, options = {}) {
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const loading = document.getElementById('dashboardLoading');
|
||||
if (loading) {
|
||||
loading.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const loading = document.getElementById('dashboardLoading');
|
||||
if (loading) {
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const error = document.getElementById('error');
|
||||
const error = document.getElementById('dashboardError');
|
||||
if (error) {
|
||||
error.textContent = message;
|
||||
error.style.display = 'block';
|
||||
@@ -345,7 +347,7 @@ function showError(message) {
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
const error = document.getElementById('error');
|
||||
const error = document.getElementById('dashboardError');
|
||||
if (error) {
|
||||
error.style.display = 'none';
|
||||
}
|
||||
@@ -1579,3 +1581,4 @@ document.getElementById('refreshBtn')?.addEventListener('click', () => {
|
||||
|
||||
// Initialize
|
||||
fetchPosts();
|
||||
})();
|
||||
|
||||
1466
web/index.html
1466
web/index.html
File diff suppressed because it is too large
Load Diff
111
web/login.html
Normal file
111
web/login.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login – Post Tracker</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
color: #0f172a;
|
||||
}
|
||||
.login-card {
|
||||
background: #fff;
|
||||
padding: 32px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.15);
|
||||
width: min(420px, 90vw);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login-card h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.login-card p {
|
||||
margin: 0 0 24px;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||
outline: none;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.08s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
box-shadow: 0 12px 30px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
button:hover {
|
||||
background: #1d4ed8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
.status {
|
||||
margin-top: 14px;
|
||||
min-height: 22px;
|
||||
color: #b91c1c;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h1>📋 Post Tracker Login</h1>
|
||||
<p>Bitte melde dich an, um das Dashboard zu öffnen.</p>
|
||||
<form id="loginForm" novalidate>
|
||||
<div class="field">
|
||||
<label for="username">Benutzername</label>
|
||||
<input type="text" id="username" name="username" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit">Anmelden</button>
|
||||
<div id="status" class="status" role="status" aria-live="polite"></div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="login.js?v=__ASSET_VERSION__"></script>
|
||||
</body>
|
||||
</html>
|
||||
125
web/login.js
Normal file
125
web/login.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const API_URL = 'https://fb.srv.medeba-media.de/api';
|
||||
const LOGIN_BROADCAST_KEY = 'fb-login-broadcast';
|
||||
|
||||
function getRedirectTarget() {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const redirect = params.get('redirect');
|
||||
if (redirect) {
|
||||
return decodeURIComponent(redirect);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Konnte Redirect-Parameter nicht lesen:', error);
|
||||
}
|
||||
return 'index.html';
|
||||
}
|
||||
|
||||
function updateStatus(message, isError = false) {
|
||||
const statusEl = document.getElementById('status');
|
||||
if (!statusEl) {
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = message || '';
|
||||
statusEl.style.color = isError ? '#b91c1c' : '#15803d';
|
||||
}
|
||||
|
||||
async function checkExistingSession() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/session`, { credentials: 'include' });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.authenticated) {
|
||||
window.location.href = getRedirectTarget();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Konnte Session nicht prüfen:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
const username = usernameInput ? usernameInput.value.trim() : '';
|
||||
const password = passwordInput ? passwordInput.value : '';
|
||||
|
||||
if (!username || !password) {
|
||||
updateStatus('Bitte Benutzername und Passwort eingeben.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Anmeldung läuft…', false);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
const message = payload && payload.error ? payload.error : 'Anmeldung fehlgeschlagen';
|
||||
updateStatus(message, true);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Erfolgreich angemeldet. Weiterleitung…', false);
|
||||
broadcastLogin();
|
||||
window.location.href = getRedirectTarget();
|
||||
} catch (error) {
|
||||
console.error('Login fehlgeschlagen:', error);
|
||||
updateStatus('Netzwerkfehler – bitte erneut versuchen.', true);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const alreadyLoggedIn = await checkExistingSession();
|
||||
if (alreadyLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupCrossTabLoginSync();
|
||||
|
||||
const form = document.getElementById('loginForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', handleLogin);
|
||||
}
|
||||
});
|
||||
|
||||
function broadcastLogin() {
|
||||
try {
|
||||
localStorage.setItem(LOGIN_BROADCAST_KEY, String(Date.now()));
|
||||
} catch (error) {
|
||||
// ignore storage errors (private mode, blocked)
|
||||
}
|
||||
if ('BroadcastChannel' in window) {
|
||||
try {
|
||||
const channel = new BroadcastChannel('fb-login');
|
||||
channel.postMessage({ type: 'login', at: Date.now() });
|
||||
channel.close();
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupCrossTabLoginSync() {
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key !== LOGIN_BROADCAST_KEY) return;
|
||||
checkExistingSession();
|
||||
});
|
||||
|
||||
if ('BroadcastChannel' in window) {
|
||||
const channel = new BroadcastChannel('fb-login');
|
||||
channel.addEventListener('message', (event) => {
|
||||
if (!event || !event.data || event.data.type !== 'login') return;
|
||||
checkExistingSession();
|
||||
});
|
||||
}
|
||||
}
|
||||
31
web/nginx.conf
Normal file
31
web/nginx.conf
Normal file
@@ -0,0 +1,31 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(html)$ {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|css)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ^~ /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ^~ /vendor/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
22
web/posts.html
Normal file
22
web/posts.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Beiträge – Post Tracker</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<script>
|
||||
(function redirectToShell() {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
params.set('view', 'posts');
|
||||
const query = params.toString();
|
||||
const target = `index.html${query ? `?${query}` : ''}${url.hash}`;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Weiterleitung zur Beiträge-Ansicht…</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,9 @@
|
||||
/* Settings Page Styles */
|
||||
|
||||
.settings-container {
|
||||
max-width: 800px;
|
||||
max-width: var(--content-max-width, 1600px);
|
||||
margin: 0 auto;
|
||||
padding: 24px 0;
|
||||
padding: 0 0 32px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
@@ -95,6 +95,20 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.grid-weights {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-field-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #1c1e21;
|
||||
}
|
||||
|
||||
.form-help a {
|
||||
color: #1877f2;
|
||||
text-decoration: none;
|
||||
@@ -112,6 +126,73 @@
|
||||
border-top: 1px solid #e4e6eb;
|
||||
}
|
||||
|
||||
.floating-save-btn {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 20;
|
||||
padding: 14px 22px;
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
border: none;
|
||||
box-shadow: 0 10px 30px rgba(37, 99, 235, 0.35);
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.2s ease;
|
||||
min-width: 210px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.floating-save-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 34px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.floating-save-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.floating-save-btn:focus {
|
||||
outline: 3px solid rgba(37, 99, 235, 0.35);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.floating-save-btn .spinner {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.floating-save-btn.is-saving {
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.floating-save-btn .spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.floating-save-btn {
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
width: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Credentials List */
|
||||
|
||||
.credentials-list {
|
||||
|
||||
@@ -2,201 +2,20 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Einstellungen – Post Tracker</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Einstellungen - Facebook Post Tracker</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="settings.css">
|
||||
<script>
|
||||
(function redirectToShell() {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
params.set('view', 'settings');
|
||||
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-main">
|
||||
<h1>⚙️ Einstellungen</h1>
|
||||
<a href="index.html" class="btn btn-secondary">Zurück zu Beiträgen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loading" class="loading" style="display: none;">Lade Einstellungen...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
<div id="success" class="success" style="display: none;"></div>
|
||||
|
||||
<div class="settings-container">
|
||||
|
||||
<!-- AI Credentials Section -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">AI-Anmeldedaten</h2>
|
||||
<p class="section-description">
|
||||
Verwalte deine API-Schlüssel für verschiedene AI-Provider. Du kannst mehrere Credentials speichern und schnell zwischen ihnen wechseln.
|
||||
</p>
|
||||
|
||||
<div id="credentialsList" class="credentials-list"></div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="addCredentialBtn">
|
||||
➕ Neue Anmeldedaten hinzufügen
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- AI Settings Section -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">AI-Kommentar-Generator</h2>
|
||||
<p class="section-description">
|
||||
Konfiguriere die automatische Generierung von Kommentaren durch KI.
|
||||
</p>
|
||||
|
||||
<form id="aiSettingsForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="aiEnabled" class="form-checkbox">
|
||||
<span>AI-Kommentar-Generator aktivieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="activeCredential" class="form-label">Aktive Anmeldedaten</label>
|
||||
<select id="activeCredential" class="form-select">
|
||||
<option value="">-- Bitte wählen --</option>
|
||||
</select>
|
||||
<p class="form-help">
|
||||
Wähle welche API-Anmeldedaten verwendet werden sollen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="aiPromptPrefix" class="form-label">Prompt-Präfix</label>
|
||||
<textarea id="aiPromptPrefix" class="form-textarea" rows="4"
|
||||
placeholder="Anweisungen für die KI vor dem Post-Text..."></textarea>
|
||||
<p class="form-help">
|
||||
Dieser Text wird vor dem eigentlichen Post-Text an die KI gesendet. Verwende <code>{FREUNDE}</code> als Platzhalter für Freundesnamen.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Profile Friends Section -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
|
||||
<p class="section-description">
|
||||
Gib für jedes Profil eine Liste von Freundesnamen an, die im Prompt verwendet werden können.
|
||||
</p>
|
||||
|
||||
<div id="profileFriendsList"></div>
|
||||
</section>
|
||||
|
||||
<!-- Save Button at the end -->
|
||||
<section class="settings-section">
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-primary" id="saveAllBtn">
|
||||
💾 Einstellungen speichern
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="testBtn">
|
||||
🧪 Kommentar testen
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Credential Modal -->
|
||||
<div id="credentialModal" class="modal" hidden>
|
||||
<div class="modal__backdrop"></div>
|
||||
<div class="modal__content" role="dialog" aria-modal="true">
|
||||
<button type="button" id="credentialModalClose" class="modal__close">×</button>
|
||||
<h2 class="modal__title" id="credentialModalTitle">Anmeldedaten hinzufügen</h2>
|
||||
|
||||
<form id="credentialForm">
|
||||
<input type="hidden" id="credentialId">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialName" class="form-label">Name *</label>
|
||||
<input type="text" id="credentialName" class="form-input" placeholder="z.B. Mein Gemini Key" required>
|
||||
<p class="form-help">Gib einen eindeutigen Namen für diese Anmeldedaten an</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialProvider" class="form-label">Provider *</label>
|
||||
<select id="credentialProvider" class="form-select" required>
|
||||
<option value="gemini">Google Gemini</option>
|
||||
<option value="claude">Anthropic Claude</option>
|
||||
<option value="openai">OpenAI / ChatGPT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialApiKey" class="form-label">API-Schlüssel</label>
|
||||
<input type="password" id="credentialApiKey" class="form-input" placeholder="sk-...">
|
||||
<p class="form-help" id="credentialApiKeyHelp"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="credentialBaseUrlGroup" style="display: none;">
|
||||
<label for="credentialBaseUrl" class="form-label">Basis-URL</label>
|
||||
<input type="text" id="credentialBaseUrl" class="form-input" placeholder="https://api.openai.com/v1">
|
||||
<p class="form-help" id="credentialBaseUrlHelp"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialModel" class="form-label">Modell</label>
|
||||
<input type="text" id="credentialModel" class="form-input" list="credentialModelOptions"
|
||||
placeholder="z.B. gpt-4o-mini">
|
||||
<datalist id="credentialModelOptions"></datalist>
|
||||
<p class="form-help" id="credentialModelHelp"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
<button type="button" class="btn btn-secondary" id="credentialCancelBtn">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Modal -->
|
||||
<div id="testModal" class="modal" hidden>
|
||||
<div class="modal__backdrop"></div>
|
||||
<div class="modal__content" role="dialog" aria-modal="true">
|
||||
<button type="button" id="testModalClose" class="modal__close">×</button>
|
||||
<h2 class="modal__title">Kommentar-Generator testen</h2>
|
||||
|
||||
<div class="modal__body">
|
||||
<div class="form-group">
|
||||
<label for="testProfileNumber" class="form-label">Test mit Profil</label>
|
||||
<select id="testProfileNumber" class="form-select">
|
||||
<option value="1">Profil 1</option>
|
||||
<option value="2">Profil 2</option>
|
||||
<option value="3">Profil 3</option>
|
||||
<option value="4">Profil 4</option>
|
||||
<option value="5">Profil 5</option>
|
||||
</select>
|
||||
<p class="form-help">Wähle ein Profil, um die Freundesnamen zu testen</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="testPostText" class="form-label">Test Post-Text</label>
|
||||
<textarea id="testPostText" class="form-textarea" rows="4"
|
||||
placeholder="Füge hier einen Beispiel-Post-Text ein..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="generateTestComment">
|
||||
✨ Kommentar generieren
|
||||
</button>
|
||||
|
||||
<div id="testLoading" class="test-loading" style="display: none;">
|
||||
Generiere Kommentar...
|
||||
</div>
|
||||
|
||||
<div id="testResult" class="test-result" style="display: none;">
|
||||
<h3>Generierter Kommentar:</h3>
|
||||
<div id="testComment" class="test-comment"></div>
|
||||
</div>
|
||||
|
||||
<div id="testError" class="test-error" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="settings.js"></script>
|
||||
<p>Weiterleitung zu den Einstellungen…</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
546
web/settings.js
546
web/settings.js
@@ -1,4 +1,6 @@
|
||||
(() => {
|
||||
const API_URL = 'https://fb.srv.medeba-media.de/api';
|
||||
const LOGIN_PAGE = 'login.html';
|
||||
|
||||
const PROVIDER_MODELS = {
|
||||
gemini: [
|
||||
@@ -42,16 +44,65 @@ const PROVIDER_INFO = {
|
||||
|
||||
let credentials = [];
|
||||
let currentSettings = null;
|
||||
let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 };
|
||||
const SPORT_WEIGHT_FIELDS = [
|
||||
{ key: 'scoreline', id: 'sportWeightScoreline' },
|
||||
{ key: 'scoreEmoji', id: 'sportWeightScoreEmoji' },
|
||||
{ key: 'sportEmoji', id: 'sportWeightSportEmoji' },
|
||||
{ key: 'sportVerb', id: 'sportWeightSportVerb' },
|
||||
{ key: 'sportNoun', id: 'sportWeightSportNoun' },
|
||||
{ key: 'hashtag', id: 'sportWeightHashtag' },
|
||||
{ key: 'teamToken', id: 'sportWeightTeamToken' },
|
||||
{ key: 'competition', id: 'sportWeightCompetition' },
|
||||
{ key: 'celebration', id: 'sportWeightCelebration' },
|
||||
{ key: 'location', id: 'sportWeightLocation' }
|
||||
];
|
||||
const SPORT_TERM_FIELDS = [
|
||||
{ key: 'nouns', id: 'sportTermsNouns', placeholder: 'auswärtssieg, liga, tor ...' },
|
||||
{ key: 'verbs', id: 'sportTermsVerbs', placeholder: 'gewinnen, punkten ...' },
|
||||
{ key: 'competitions', id: 'sportTermsCompetitions', placeholder: 'bundesliga, cup ...' },
|
||||
{ key: 'celebrations', id: 'sportTermsCelebrations', placeholder: 'sieg, tabellenführung ...' },
|
||||
{ key: 'locations', id: 'sportTermsLocations', placeholder: 'auswärts, stadion ...' },
|
||||
{ key: 'negatives', id: 'sportTermsNegatives', placeholder: 'rezept, politik ...' }
|
||||
];
|
||||
let moderationSettings = {
|
||||
sports_scoring_enabled: true,
|
||||
sports_score_threshold: 5,
|
||||
sports_score_weights: {},
|
||||
sports_terms: {},
|
||||
sports_auto_hide_enabled: false
|
||||
};
|
||||
let similaritySettings = {
|
||||
text_threshold: 0.85,
|
||||
image_distance_threshold: 6
|
||||
};
|
||||
|
||||
function handleUnauthorized(response) {
|
||||
if (response && response.status === 401) {
|
||||
if (typeof redirectToLogin === 'function') {
|
||||
redirectToLogin();
|
||||
} else {
|
||||
window.location.href = LOGIN_PAGE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function apiFetch(url, options = {}) {
|
||||
return fetch(url, {...options, credentials: 'include'});
|
||||
return fetch(url, {...options, credentials: 'include'}).then((response) => {
|
||||
if (handleUnauthorized(response)) {
|
||||
throw new Error('Nicht angemeldet');
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
bottom: 84px;
|
||||
right: 24px;
|
||||
background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'};
|
||||
color: white;
|
||||
@@ -63,6 +114,7 @@ function showToast(message, type = 'info') {
|
||||
z-index: 999999;
|
||||
max-width: 350px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
pointer-events: none;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
@@ -123,6 +175,318 @@ async function loadSettings() {
|
||||
'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n';
|
||||
}
|
||||
|
||||
async function loadHiddenSettings() {
|
||||
const res = await apiFetch(`${API_URL}/hidden-settings`);
|
||||
if (!res.ok) throw new Error('Failed to load hidden settings');
|
||||
hiddenSettings = await res.json();
|
||||
applyHiddenSettingsUI();
|
||||
}
|
||||
|
||||
function applyHiddenSettingsUI() {
|
||||
const autoToggle = document.getElementById('autoPurgeHiddenToggle');
|
||||
const retentionInput = document.getElementById('hiddenRetentionDays');
|
||||
if (autoToggle) {
|
||||
autoToggle.checked = !!hiddenSettings.auto_purge_enabled;
|
||||
}
|
||||
if (retentionInput) {
|
||||
retentionInput.value = hiddenSettings.retention_days || 90;
|
||||
retentionInput.disabled = !autoToggle?.checked;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRetentionInput(value) {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
return 90;
|
||||
}
|
||||
return Math.min(365, Math.max(1, parsed));
|
||||
}
|
||||
|
||||
async function saveHiddenSettings(event, { silent = false } = {}) {
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
const autoToggle = document.getElementById('autoPurgeHiddenToggle');
|
||||
const retentionInput = document.getElementById('hiddenRetentionDays');
|
||||
const autoEnabled = autoToggle ? autoToggle.checked : true;
|
||||
const retention = normalizeRetentionInput(retentionInput ? retentionInput.value : hiddenSettings.retention_days);
|
||||
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/hidden-settings`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
auto_purge_enabled: autoEnabled,
|
||||
retention_days: retention
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern');
|
||||
}
|
||||
hiddenSettings = await res.json();
|
||||
applyHiddenSettingsUI();
|
||||
if (!silent) {
|
||||
showSuccess('✅ Einstellungen für versteckte Beiträge gespeichert');
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeHiddenNow() {
|
||||
const btn = document.getElementById('purgeHiddenNowBtn');
|
||||
const originalText = btn ? btn.textContent : '';
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Bereinige...';
|
||||
}
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/search-posts`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Bereinigen');
|
||||
}
|
||||
showSuccess('🧹 Versteckte Beiträge wurden zurückgesetzt');
|
||||
} catch (err) {
|
||||
showError('❌ ' + err.message);
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText || 'Jetzt bereinigen';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSportsScoreThresholdInput(value) {
|
||||
const parsed = parseFloat(value);
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
return 5;
|
||||
}
|
||||
return Math.min(50, Math.max(0, parsed));
|
||||
}
|
||||
|
||||
function normalizeSportWeightInput(value, fallback = 1) {
|
||||
const parsed = parseFloat(value);
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(10, Math.max(0, parsed));
|
||||
}
|
||||
|
||||
function applyWeightInputs(enabled) {
|
||||
SPORT_WEIGHT_FIELDS.forEach(({ id }) => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.disabled = !enabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyTermInputs(enabled) {
|
||||
SPORT_TERM_FIELDS.forEach(({ id }) => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.disabled = !enabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function serializeTermListInput(value) {
|
||||
if (!value || typeof value !== 'string') return [];
|
||||
return value
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry)
|
||||
.slice(0, 200);
|
||||
}
|
||||
|
||||
function renderTermList(list) {
|
||||
if (!Array.isArray(list) || !list.length) return '';
|
||||
return list.join(', ');
|
||||
}
|
||||
|
||||
function applyModerationSettingsUI() {
|
||||
const enabledToggle = document.getElementById('sportsScoringEnabled');
|
||||
const thresholdInput = document.getElementById('sportsScoreThreshold');
|
||||
const autoHideToggle = document.getElementById('sportsAutoHideEnabled');
|
||||
if (enabledToggle) {
|
||||
enabledToggle.checked = !!moderationSettings.sports_scoring_enabled;
|
||||
}
|
||||
if (thresholdInput) {
|
||||
thresholdInput.value = moderationSettings.sports_score_threshold ?? 5;
|
||||
thresholdInput.disabled = !enabledToggle?.checked;
|
||||
}
|
||||
if (autoHideToggle) {
|
||||
autoHideToggle.checked = !!moderationSettings.sports_auto_hide_enabled;
|
||||
autoHideToggle.disabled = !enabledToggle?.checked;
|
||||
}
|
||||
SPORT_WEIGHT_FIELDS.forEach(({ key, id }) => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
const value = moderationSettings.sports_score_weights?.[key];
|
||||
input.value = typeof value === 'number' ? value : '';
|
||||
input.disabled = !enabledToggle?.checked;
|
||||
}
|
||||
});
|
||||
SPORT_TERM_FIELDS.forEach(({ key, id }) => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.value = renderTermList(moderationSettings.sports_terms?.[key]);
|
||||
input.disabled = !enabledToggle?.checked;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadModerationSettings() {
|
||||
const res = await apiFetch(`${API_URL}/moderation-settings`);
|
||||
if (!res.ok) throw new Error('Konnte Moderations-Einstellungen nicht laden');
|
||||
const data = await res.json();
|
||||
moderationSettings = {
|
||||
sports_scoring_enabled: !!data.sports_scoring_enabled,
|
||||
sports_score_threshold: normalizeSportsScoreThresholdInput(data.sports_score_threshold),
|
||||
sports_score_weights: data.sports_score_weights || {},
|
||||
sports_terms: data.sports_terms || {},
|
||||
sports_auto_hide_enabled: !!data.sports_auto_hide_enabled
|
||||
};
|
||||
applyModerationSettingsUI();
|
||||
}
|
||||
|
||||
async function saveModerationSettings(event, { silent = false } = {}) {
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
const enabledToggle = document.getElementById('sportsScoringEnabled');
|
||||
const thresholdInput = document.getElementById('sportsScoreThreshold');
|
||||
const autoHideToggle = document.getElementById('sportsAutoHideEnabled');
|
||||
const enabled = enabledToggle ? enabledToggle.checked : true;
|
||||
const threshold = thresholdInput
|
||||
? normalizeSportsScoreThresholdInput(thresholdInput.value)
|
||||
: moderationSettings.sports_score_threshold;
|
||||
const autoHide = autoHideToggle ? autoHideToggle.checked : false;
|
||||
const weights = {};
|
||||
SPORT_WEIGHT_FIELDS.forEach(({ key, id }) => {
|
||||
const input = document.getElementById(id);
|
||||
weights[key] = normalizeSportWeightInput(input ? input.value : moderationSettings.sports_score_weights?.[key], moderationSettings.sports_score_weights?.[key] ?? 1);
|
||||
});
|
||||
const terms = {};
|
||||
SPORT_TERM_FIELDS.forEach(({ key, id }) => {
|
||||
const input = document.getElementById(id);
|
||||
terms[key] = serializeTermListInput(input ? input.value : moderationSettings.sports_terms?.[key]);
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/moderation-settings`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sports_scoring_enabled: enabled,
|
||||
sports_score_threshold: threshold,
|
||||
sports_auto_hide_enabled: autoHide,
|
||||
sports_score_weights: weights,
|
||||
sports_terms: terms
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern');
|
||||
}
|
||||
moderationSettings = await res.json();
|
||||
applyModerationSettingsUI();
|
||||
if (!silent) {
|
||||
showSuccess('✅ Sport-Scoring gespeichert');
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSimilarityTextThresholdInput(value) {
|
||||
const parsed = parseFloat(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return 0.85;
|
||||
}
|
||||
return Math.min(0.99, Math.max(0.5, parsed));
|
||||
}
|
||||
|
||||
function normalizeSimilarityImageThresholdInput(value) {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return 6;
|
||||
}
|
||||
return Math.min(64, Math.max(0, parsed));
|
||||
}
|
||||
|
||||
function applySimilaritySettingsUI() {
|
||||
const textInput = document.getElementById('similarityTextThreshold');
|
||||
const imageInput = document.getElementById('similarityImageThreshold');
|
||||
if (textInput) {
|
||||
textInput.value = similaritySettings.text_threshold ?? 0.85;
|
||||
}
|
||||
if (imageInput) {
|
||||
imageInput.value = similaritySettings.image_distance_threshold ?? 6;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSimilaritySettings() {
|
||||
const res = await apiFetch(`${API_URL}/similarity-settings`);
|
||||
if (!res.ok) throw new Error('Konnte Ähnlichkeits-Einstellungen nicht laden');
|
||||
const data = await res.json();
|
||||
similaritySettings = {
|
||||
text_threshold: normalizeSimilarityTextThresholdInput(data.text_threshold),
|
||||
image_distance_threshold: normalizeSimilarityImageThresholdInput(data.image_distance_threshold)
|
||||
};
|
||||
applySimilaritySettingsUI();
|
||||
}
|
||||
|
||||
async function saveSimilaritySettings(event, { silent = false } = {}) {
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
const textInput = document.getElementById('similarityTextThreshold');
|
||||
const imageInput = document.getElementById('similarityImageThreshold');
|
||||
const textThreshold = textInput
|
||||
? normalizeSimilarityTextThresholdInput(textInput.value)
|
||||
: similaritySettings.text_threshold;
|
||||
const imageThreshold = imageInput
|
||||
? normalizeSimilarityImageThresholdInput(imageInput.value)
|
||||
: similaritySettings.image_distance_threshold;
|
||||
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/similarity-settings`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text_threshold: textThreshold,
|
||||
image_distance_threshold: imageThreshold
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern');
|
||||
}
|
||||
similaritySettings = await res.json();
|
||||
applySimilaritySettingsUI();
|
||||
if (!silent) {
|
||||
showSuccess('✅ Ähnlichkeitsregeln gespeichert');
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shorten(text, maxLength = 80) {
|
||||
if (typeof text !== 'string') {
|
||||
return '';
|
||||
@@ -514,8 +878,10 @@ async function deleteCredential(id) {
|
||||
showSuccess('Anmeldedaten gelöscht');
|
||||
}
|
||||
|
||||
// Make function globally accessible
|
||||
// Make functions globally accessible for inline handlers
|
||||
window.toggleCredentialActive = toggleCredentialActive;
|
||||
window.editCredential = editCredential;
|
||||
window.deleteCredential = deleteCredential;
|
||||
|
||||
// ============================================================================
|
||||
// DRAG AND DROP
|
||||
@@ -623,8 +989,10 @@ async function saveCredentialOrder() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(e) {
|
||||
async function saveSettings(e, { silent = false } = {}) {
|
||||
if (e && typeof e.preventDefault === 'function') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
try {
|
||||
const data = {
|
||||
@@ -645,10 +1013,16 @@ async function saveSettings(e) {
|
||||
}
|
||||
|
||||
currentSettings = await res.json();
|
||||
if (!silent) {
|
||||
showSuccess('✅ Einstellungen erfolgreich gespeichert');
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testComment() {
|
||||
@@ -717,6 +1091,66 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function saveAllSettings(event) {
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('saveAllFloatingBtn');
|
||||
const labelEl = saveBtn ? saveBtn.querySelector('.label') : null;
|
||||
const spinnerEl = saveBtn ? saveBtn.querySelector('.spinner') : null;
|
||||
const defaultLabel = saveBtn
|
||||
? (saveBtn.dataset.defaultLabel || (labelEl && labelEl.textContent.trim()) || 'Einstellungen speichern')
|
||||
: 'Einstellungen speichern';
|
||||
if (saveBtn) {
|
||||
saveBtn.dataset.defaultLabel = defaultLabel;
|
||||
}
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.classList.add('is-saving');
|
||||
if (labelEl) {
|
||||
labelEl.textContent = 'Speichern...';
|
||||
} else {
|
||||
saveBtn.textContent = 'Speichern...';
|
||||
}
|
||||
if (spinnerEl) {
|
||||
spinnerEl.style.display = 'inline-block';
|
||||
spinnerEl.classList.add('spin');
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all([
|
||||
saveSettings(null, { silent: true }),
|
||||
saveHiddenSettings(null, { silent: true }),
|
||||
saveModerationSettings(null, { silent: true }),
|
||||
saveSimilaritySettings(null, { silent: true }),
|
||||
saveAllFriends({ silent: true })
|
||||
]);
|
||||
|
||||
const allOk = results.every(Boolean);
|
||||
if (allOk) {
|
||||
showSuccess('Gespeichert');
|
||||
} else {
|
||||
showError('❌ Nicht alle Einstellungen konnten gespeichert werden');
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.classList.remove('is-saving');
|
||||
const label = saveBtn.querySelector('.label');
|
||||
const spinner = saveBtn.querySelector('.spinner');
|
||||
if (label) {
|
||||
label.textContent = saveBtn.dataset.defaultLabel || 'Einstellungen speichern';
|
||||
} else {
|
||||
saveBtn.textContent = 'Einstellungen speichern';
|
||||
}
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none';
|
||||
spinner.classList.remove('spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROFILE FRIENDS
|
||||
// ============================================================================
|
||||
@@ -752,7 +1186,7 @@ async function loadProfileFriends() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFriends(profileNumber, friendNames) {
|
||||
async function saveFriends(profileNumber, friendNames, { silent = false } = {}) {
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, {
|
||||
method: 'PUT',
|
||||
@@ -766,10 +1200,32 @@ async function saveFriends(profileNumber, friendNames) {
|
||||
}
|
||||
|
||||
profileFriends[profileNumber] = friendNames;
|
||||
if (!silent) {
|
||||
showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllFriends({ silent = false } = {}) {
|
||||
let success = true;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const input = document.getElementById(`friends${i}`);
|
||||
if (!input) {
|
||||
continue;
|
||||
}
|
||||
const newValue = input.value.trim();
|
||||
if (newValue !== profileFriends[i]) {
|
||||
const result = await saveFriends(i, newValue, { silent });
|
||||
success = success && result;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
@@ -782,13 +1238,83 @@ document.getElementById('aiSettingsForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
saveSettings(e);
|
||||
});
|
||||
document.getElementById('saveAllBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
saveSettings(e);
|
||||
});
|
||||
document.getElementById('testBtn').addEventListener('click', testComment);
|
||||
document.getElementById('testModalClose').addEventListener('click', () => document.getElementById('testModal').setAttribute('hidden', ''));
|
||||
document.getElementById('generateTestComment').addEventListener('click', generateTest);
|
||||
document.getElementById('purgeHiddenNowBtn').addEventListener('click', purgeHiddenNow);
|
||||
document.getElementById('saveAllFloatingBtn').addEventListener('click', saveAllSettings);
|
||||
|
||||
const autoPurgeHiddenToggle = document.getElementById('autoPurgeHiddenToggle');
|
||||
if (autoPurgeHiddenToggle) {
|
||||
autoPurgeHiddenToggle.addEventListener('change', () => {
|
||||
const retentionInput = document.getElementById('hiddenRetentionDays');
|
||||
if (retentionInput) {
|
||||
retentionInput.disabled = !autoPurgeHiddenToggle.checked;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sportsScoringToggle = document.getElementById('sportsScoringEnabled');
|
||||
const sportsScoreInput = document.getElementById('sportsScoreThreshold');
|
||||
if (sportsScoringToggle && sportsScoreInput) {
|
||||
sportsScoringToggle.addEventListener('change', () => {
|
||||
sportsScoreInput.disabled = !sportsScoringToggle.checked;
|
||||
applyWeightInputs(sportsScoringToggle.checked);
|
||||
applyTermInputs(sportsScoringToggle.checked);
|
||||
const autoHideToggle = document.getElementById('sportsAutoHideEnabled');
|
||||
if (autoHideToggle) {
|
||||
autoHideToggle.disabled = !sportsScoringToggle.checked;
|
||||
}
|
||||
});
|
||||
sportsScoreInput.addEventListener('blur', () => {
|
||||
sportsScoreInput.value = normalizeSportsScoreThresholdInput(sportsScoreInput.value);
|
||||
});
|
||||
SPORT_WEIGHT_FIELDS.forEach(({ id }) => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.addEventListener('blur', () => {
|
||||
input.value = normalizeSportWeightInput(input.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
SPORT_TERM_FIELDS.forEach(({ id }) => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.addEventListener('blur', () => {
|
||||
input.value = renderTermList(serializeTermListInput(input.value));
|
||||
});
|
||||
}
|
||||
});
|
||||
const moderationForm = document.getElementById('moderationSettingsForm');
|
||||
if (moderationForm) {
|
||||
moderationForm.addEventListener('submit', (e) => saveModerationSettings(e));
|
||||
}
|
||||
}
|
||||
|
||||
const similarityForm = document.getElementById('similaritySettingsForm');
|
||||
if (similarityForm) {
|
||||
const textInput = document.getElementById('similarityTextThreshold');
|
||||
const imageInput = document.getElementById('similarityImageThreshold');
|
||||
if (textInput) {
|
||||
textInput.addEventListener('blur', () => {
|
||||
textInput.value = normalizeSimilarityTextThresholdInput(textInput.value);
|
||||
});
|
||||
}
|
||||
if (imageInput) {
|
||||
imageInput.addEventListener('blur', () => {
|
||||
imageInput.value = normalizeSimilarityImageThresholdInput(imageInput.value);
|
||||
});
|
||||
}
|
||||
similarityForm.addEventListener('submit', (e) => saveSimilaritySettings(e));
|
||||
}
|
||||
|
||||
// Initialize
|
||||
Promise.all([loadCredentials(), loadSettings(), loadProfileFriends()]).catch(err => showError(err.message));
|
||||
Promise.all([
|
||||
loadCredentials(),
|
||||
loadSettings(),
|
||||
loadHiddenSettings(),
|
||||
loadModerationSettings(),
|
||||
loadSimilaritySettings(),
|
||||
loadProfileFriends()
|
||||
]).catch(err => showError(err.message));
|
||||
})();
|
||||
|
||||
865
web/style.css
865
web/style.css
@@ -4,20 +4,56 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--content-max-width: 1300px;
|
||||
--top-gap: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #f0f2f5;
|
||||
color: #050505;
|
||||
line-height: 1.5;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.shell-main {
|
||||
flex: 1;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.site-header,
|
||||
header {
|
||||
background: white;
|
||||
padding: 16px 18px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
margin-bottom: var(--top-gap);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.site-header__subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.page-toolbar {
|
||||
background: white;
|
||||
padding: 16px 18px;
|
||||
border-radius: 10px;
|
||||
@@ -28,6 +64,13 @@ header {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-toolbar__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -35,6 +78,85 @@ header {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-nav::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(79, 70, 229, 0.7), rgba(14, 165, 233, 0.7));
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.site-nav__btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.01em;
|
||||
padding: 10px 16px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav__btn--icon {
|
||||
padding: 10px 12px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.site-nav__btn::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: -8px;
|
||||
height: 3px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, #4338ca, #0ea5e9);
|
||||
transform: translateY(6px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.site-nav__btn:hover {
|
||||
color: #0f172a;
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav__btn.nav-active {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.site-nav__btn.nav-active::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -129,6 +251,14 @@ header {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.app-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-view--active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -327,6 +457,61 @@ h1 {
|
||||
|
||||
.search-container {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.merge-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.merge-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.merge-actions .btn {
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#mergeModeToggle.active {
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-filter-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
padding: 7px 12px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #eef2ff 0%, #e0f2fe 100%);
|
||||
border: 1px solid #d0d7ff;
|
||||
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.search-filter-toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #2563eb;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
@@ -404,6 +589,141 @@ h1 {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.posts-bulk-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.bulk-actions label {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bulk-actions select {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #111827;
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.auto-open-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.auto-open-toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.bulk-status {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.bulk-status--error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.auto-open-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(37, 99, 235, 0.12), transparent 42%),
|
||||
radial-gradient(circle at 80% 30%, rgba(6, 182, 212, 0.12), transparent 38%),
|
||||
rgba(15, 23, 42, 0.6);
|
||||
z-index: 30;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.auto-open-overlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.auto-open-overlay__panel {
|
||||
width: min(940px, 100%);
|
||||
background: linear-gradient(150deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.96));
|
||||
border-radius: 22px;
|
||||
padding: 38px 42px 40px;
|
||||
box-shadow: 0 32px 90px rgba(15, 23, 42, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto-open-overlay__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.auto-open-overlay__timer {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 18px 0 8px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.auto-open-overlay__count {
|
||||
font-size: clamp(72px, 12vw, 120px);
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.auto-open-overlay__unit {
|
||||
font-size: 22px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.auto-open-overlay__text {
|
||||
margin: 0 auto;
|
||||
color: #334155;
|
||||
max-width: 700px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.auto-open-overlay__hint {
|
||||
margin: 12px 0 0;
|
||||
color: #475569;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.posts-load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -434,31 +754,36 @@ h1 {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.post-counter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 26px;
|
||||
height: 26px;
|
||||
margin-right: 10px;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.post-counter__value::before {
|
||||
content: '#';
|
||||
margin-right: 2px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.post-card.complete {
|
||||
opacity: 0.7;
|
||||
border-left: 4px solid #059669;
|
||||
}
|
||||
|
||||
.post-card--highlight {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 4px rgba(102, 126, 234, 0.35);
|
||||
animation: post-card-highlight-pulse 1.4s ease-in-out 2;
|
||||
}
|
||||
|
||||
@keyframes post-card-highlight-pulse {
|
||||
0% {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 0 rgba(102, 126, 234, 0.45);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 8px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 0 rgba(102, 126, 234, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.post-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -469,9 +794,11 @@ h1 {
|
||||
|
||||
.post-header-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.post-title-with-checkbox {
|
||||
@@ -481,6 +808,32 @@ h1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.merge-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.merge-select input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #0f172a;
|
||||
}
|
||||
|
||||
.post-index {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.success-checkbox--header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -495,18 +848,18 @@ h1 {
|
||||
.post-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.post-status.complete {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: transparent;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
|
||||
@@ -788,6 +1141,7 @@ h1 {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-link__label {
|
||||
@@ -798,13 +1152,27 @@ h1 {
|
||||
.post-link__anchor {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
word-break: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.post-link__anchor:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-creator__link {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-creator__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-profiles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1061,6 +1429,402 @@ h1 {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bookmark-inline {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bookmark-inline__toggle {
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: min(540px, 92vw);
|
||||
max-height: 70vh;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18);
|
||||
padding: 16px;
|
||||
z-index: 20;
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bookmark-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(229, 231, 235, 0.8);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.bookmark-panel__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bookmark-panel__close {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: #4b5563;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.bookmark-panel__close:hover,
|
||||
.bookmark-panel__close:focus {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.bookmark-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.bookmark-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bookmark-section__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bookmark-section__title {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.bookmark-section__hint {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.bookmark-section__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bookmark-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.bookmark-row[data-state="never-used"] {
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.bookmark-row__open {
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bookmark-row__open:focus-visible {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: 3px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.bookmark-row__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.bookmark-row__query {
|
||||
font-size: 11px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.bookmark-row__meta {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.bookmark-row__remove {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 6px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.bookmark-row__remove:hover,
|
||||
.bookmark-row__remove:focus-visible {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.bookmark-status {
|
||||
font-size: 13px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.bookmark-status--error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
}
|
||||
|
||||
.bookmark-status--loading {
|
||||
background: #ede9fe;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
.bookmark-form {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bookmark-form__fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bookmark-form__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 220px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bookmark-form__field span {
|
||||
font-size: 13px;
|
||||
color: #65676b;
|
||||
}
|
||||
|
||||
.bookmark-form__field input {
|
||||
border: 1px solid #d0d3d9;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-form__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bookmark-form__hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #65676b;
|
||||
}
|
||||
|
||||
.bookmark-empty {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
background: #f3f4f6;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bookmarks-page {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.bookmarks-page__intro {
|
||||
margin-top: 12px;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bookmark-page__panel {
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 24px 28px;
|
||||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bookmark-page__panel .bookmark-list {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.bookmark-page__lead {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-panel__toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bookmark-quicksearch {
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bookmark-quicksearch__fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bookmark-quicksearch__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1 1 260px;
|
||||
}
|
||||
|
||||
.bookmark-quicksearch__field span {
|
||||
font-size: 13px;
|
||||
color: #65676b;
|
||||
}
|
||||
|
||||
.bookmark-quicksearch__field input {
|
||||
border: 1px solid #d0d3d9;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-quicksearch__hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.bookmark-panel__search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
|
||||
.bookmark-panel__search input {
|
||||
border: 1px solid #d0d3d9;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-panel__sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bookmark-panel__sort label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.bookmark-panel__sort select {
|
||||
border: 1px solid #d0d3d9;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.bookmark-sort__direction {
|
||||
border: 1px solid #d0d3d9;
|
||||
border-radius: 8px;
|
||||
background: #f3f4f6;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bookmark-sort__direction:hover {
|
||||
border-color: #a5b4fc;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.bookmark-panel {
|
||||
width: min(480px, 94vw);
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.bookmark-section__list {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.bookmark-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshot-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -1184,14 +1948,18 @@ h1 {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
padding-left: 20px;
|
||||
.header-main {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-counter {
|
||||
margin-bottom: 4px;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
.header-links {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
@@ -1222,4 +1990,25 @@ h1 {
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bookmark-inline {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bookmark-panel {
|
||||
position: static;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
max-height: none;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.bookmark-list {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.bookmark-form__actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
1
web/vendor/list.min.js
vendored
Normal file
1
web/vendor/list.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(t){if(typeof window!=="undefined"){t(window)}else if(typeof global!=="undefined"){t(global)}}(function(window){function getValue(row,name,attr){var el=row.querySelector("."+name);if(!el){return""}if(attr){return el.getAttribute(attr)||""}return el.textContent||el.innerText||""}function normalizeOrder(order){return order==="desc"?"desc":"asc"}function List(container,options){if(!(this instanceof List))return new List(container,options);this.container=typeof container==="string"?document.querySelector(container):container;if(!this.container){throw new Error("List container not found")}this.listClass=options&&options.listClass?options.listClass:"list";this.valueNames=options&&options.valueNames?options.valueNames:[];this.listEl=this.container.querySelector("."+this.listClass);if(!this.listEl){throw new Error("List element not found")}this.items=Array.from(this.listEl.children);this.filteredItems=this.items.slice();this.sortOrder="asc";this.sortKey=null}List.prototype.filter=function(fn){this.filteredItems=[];for(var i=0;i<this.items.length;i++){var row=this.items[i];var values=this._buildValues(row);if(fn({values:function(){return values}})){this.filteredItems.push(row)}}this._render();return this};List.prototype.sort=function(key,opts){var order=normalizeOrder(opts&&opts.order);this.sortKey=key;this.sortOrder=order;var self=this;var kv=this._resolveKeySpec(key);this.filteredItems.sort(function(a,b){var va=getValue(a,kv.name,kv.attr);var vb=getValue(b,kv.name,kv.attr);var na=Number(va),nb=Number(vb);var cmp;if(!isNaN(na)&&!isNaN(nb)){cmp=na-nb}else{cmp=String(va).localeCompare(String(vb))}return order==="desc"?-cmp:cmp});this._render();return this};List.prototype._resolveKeySpec=function(key){for(var i=0;i<this.valueNames.length;i++){var spec=this.valueNames[i];if(typeof spec==="string"&&spec===key){return {name:key,attr:null}}if(spec&&spec.name===key){return {name:spec.name,attr:spec.attr||null}}}return {name:key,attr:null}};List.prototype._buildValues=function(row){var out={};for(var i=0;i<this.valueNames.length;i++){var spec=this.valueNames[i],name=typeof spec==="string"?spec:spec.name,attr=spec.attr||null;out[name]=getValue(row,name,attr)}return out};List.prototype._render=function(){var parent=this.listEl;parent.innerHTML="";for(var i=0;i<this.filteredItems.length;i++){parent.appendChild(this.filteredItems[i])}};window.List=List});
|
||||
Reference in New Issue
Block a user