diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 987e834..0000000 --- a/.drone.yml +++ /dev/null @@ -1,44 +0,0 @@ -kind: pipeline -type: docker -name: default - -steps: - - name: build - image: node - commands: - - npm install - - npm run build - - name: deploy-dev - image: alpine - environment: - FTP_HOST: - from_secret: FTP_HOST - FTP_USERNAME: - from_secret: FTP_USERNAME - FTP_PASSWORD: - from_secret: FTP_PASSWORD - commands: - - which lftp || ( apk --update add lftp ) - - lftp -e "set ftp:ssl-force true; set ssl:verify-certificate no; mirror -R ./dist/ dev.sp-codes.de/; bye" -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST - when: - branch: - - develop - event: - - push - - name: deploy - image: alpine - environment: - FTP_HOST: - from_secret: FTP_HOST - FTP_USERNAME: - from_secret: FTP_USERNAME - FTP_PASSWORD: - from_secret: FTP_PASSWORD - commands: - - which lftp || ( apk --update add lftp ) - - lftp -e "set ftp:ssl-force true; set ssl:verify-certificate no; mirror -R ./dist/ sp-codes.de/; bye" -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST - when: - branch: - - main - event: - - push diff --git a/.eleventy.js b/.eleventy.js index 9a02e4b..2287334 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -1,22 +1,84 @@ -const eleventyNavigationPlugin = require("@11ty/eleventy-navigation"); +const pluginRev = require("eleventy-plugin-rev"); +const pluginSass = require("eleventy-sass"); +const pluginTinyHtml = require("@sardine/eleventy-plugin-tinyhtml"); +const pluginNavigation = require("@11ty/eleventy-navigation"); -module.exports = function(eleventyConfig) { - eleventyConfig.addWatchTarget("./src/scss/"); +module.exports = function (eleventyConfig) { + eleventyConfig.addPlugin(pluginRev); + eleventyConfig.addPlugin(pluginTinyHtml); + eleventyConfig.addPlugin(pluginSass, { + sass: { + loadPaths: ["node_modules"], + style: "compressed", + sourceMap: false, + }, + compileOptions: { + permalink: function (contents, inputPath) { + return (data) => { + return data.page.filePathStem.replace(/^\/scss\//, "/css/") + ".css"; + }; + } + }, + rev: true + }); + eleventyConfig.addPlugin(pluginNavigation); - eleventyConfig.addPlugin(eleventyNavigationPlugin); eleventyConfig.setUseGitIgnore(false); eleventyConfig.addPassthroughCopy({ "src/img": "img", "src/font": "font", - "node_modules/@fortawesome/fontawesome-free/webfonts/": "font", - "node_modules/flag-icon-css/flags/4x3/(de|us)*": "flags" + "src/favicon.*": "", + }); + eleventyConfig.addShortcode("translatedUrl", function (currentLocale, newLocale) { + return this.page.url.replace(new RegExp(`\/${currentLocale}\/`), `/${newLocale}/`); + }); + + eleventyConfig.addFilter('year', function (value) { + return value * 12; + }); + + eleventyConfig.addFilter("sum", function (value) { + return value.map(d => d.amount).reduce((a, b) => a + b, 0); + }); + + eleventyConfig.addFilter("amount", function (value) { + // TODO update language dynamically + return value.toLocaleString('de', {minimumFractionDigits: 2}); + }); + + eleventyConfig.addFilter("banktransfers", function (donations) { + return donations + .flatMap(y => y.donations) + .filter(d => d.via === 'banktransfer') + .filter(d => d.first) + .length; + }); + + eleventyConfig.addFilter("cash", function (donations) { + return donations + .flatMap(y => y.donations) + .filter(d => d.via === 'cash') + .filter(d => d.first) + .length; + }); + + eleventyConfig.addFilter('getServiceById', (services, serviceId) => { + return services.find(s => s.id === serviceId); }); return { + // Pre-process *.md files with: (default: `liquid`) + markdownTemplateEngine: "njk", + // Pre-process *.html files with: (default: `liquid`) + htmlTemplateEngine: "njk", + // Opt-out of pre-processing global data JSON files: (default: `liquid`) + dataTemplateEngine: false, + dir: { input: "src", includes: "_includes", layouts: "_includes/layouts", + data: "_data", output: "dist" } }; diff --git a/.forgejo/workflows/build-deploy.yml b/.forgejo/workflows/build-deploy.yml new file mode 100644 index 0000000..b153732 --- /dev/null +++ b/.forgejo/workflows/build-deploy.yml @@ -0,0 +1,54 @@ +name: Build and Deploy Website + +on: [push] + +jobs: + build: + name: Build Website + runs-on: docker + container: + image: node:lts + steps: + - uses: actions/checkout@v3 + - name: Install Dependencies + run: npm install + - name: Build Website + run: npm run build + - uses: actions/upload-artifact@v3 + with: + name: build + path: dist/ + deploy-dev: + name: Deploy Dev Website + runs-on: docker + container: + image: node:lts-alpine + needs: [build] + if: github.ref == 'refs/heads/develop' + steps: + - uses: actions/download-artifact@v3 + - name: Install Dependencies + run: which lftp || ( apk --update add lftp ) + - name: Deploy Website + run: lftp -e "set ftp:ssl-force true; set ssl:verify-certificate no; mirror -R ./build/ dev.sp-codes.de/; bye" -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST + env: + FTP_HOST: ${{ secrets.FTP_HOST }} + FTP_USERNAME: ${{ secrets.FTP_USERNAME }} + FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} + deploy: + name: Deploy Website + runs-on: docker + container: + image: node:lts-alpine + needs: [build] + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/download-artifact@v3 + - name: Install Dependencies + run: which lftp || ( apk --update add lftp ) + - name: Deploy Website + run: lftp -e "set ftp:ssl-force true; set ssl:verify-certificate no; mirror -R ./build/ sp-codes.de/; bye" -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST + env: + FTP_HOST: ${{ secrets.FTP_HOST }} + FTP_USERNAME: ${{ secrets.FTP_USERNAME }} + FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} diff --git a/README.md b/README.md index 7eed4db..8e9e175 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # sp-codes.de -[](https://ci.sp-codes.de/samuel-p/sp-codes.de) +[](https://ci.sp-codes.de/samuel-p/sp-codes.de) Website for [sp-codes.de](https://sp-codes.de) @@ -9,7 +9,7 @@ Website for [sp-codes.de](https://sp-codes.de) The following Parameters are set directly on the Web-Server. ``` -Content-Security-Policy: default-src 'none'; script-src 'self' https://plausible.sp-codes.de; object-src 'none'; style-src 'self'; img-src 'self' https://shields.sp-codes.de; media-src 'none'; frame-src 'none'; font-src 'self'; connect-src 'self' https://plausible.sp-codes.de +Content-Security-Policy: default-src 'none'; script-src 'self' https://umami.sp-codes.de; object-src 'none'; style-src 'self'; img-src 'self' https://shields.sp-codes.de; media-src 'none'; frame-src 'none'; font-src 'self'; connect-src 'self' https://umami.sp-codes.de Referrer-Policy: strict-origin-when-cross-origin Feature-Policy: sync-xhr 'self' Strict-Transport-Security: max-age=31536000; includeSubDomains; preload diff --git a/package.json b/package.json index 0780854..0b9272e 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,9 @@ "version": "1.0.0", "description": "website for sp-codes.de", "scripts": { - "compile-sass": "node-sass --output-style compressed --importer=node_modules/node-sass-tilde-importer src/scss/main.scss dist/css/main.css", - "watch:eleventy": "eleventy --serve", - "watch:sass": "npm run compile-sass -- --watch", - "start": "npm-run-all compile-sass --parallel watch:*", - "build": "npm run compile-sass && eleventy && npm run move-index", + "minify-css": "uncss -n -H dist/ -o dist/css/main-*.css dist/**/*.html dist/**/**/*.html dist/**/**/**/*.html dist/**/**/**/**/*.html dist/**/**/**/**/**/*.html", + "start": "eleventy --serve --watch", + "build": "eleventy && npm run move-index && npm run minify-css", "move-index": "cpx dist/de/index.html dist/" }, "author": "samuel-p", @@ -16,19 +14,25 @@ "url": "https://git.sp-codes.de/samuel-p/sp-codes.de" }, "optionalDependencies": { - "browser-sync": "^2.26.14" + "browser-sync": "^2.29.3" }, "devDependencies": { - "@11ty/eleventy": "^0.12.1", - "@11ty/eleventy-navigation": "^0.1.6", + "@11ty/eleventy": "^2.0.1", + "@11ty/eleventy-navigation": "^0.3.5", + "@node-minify/core": "^8.0.6", + "@node-minify/crass": "^8.0.6", + "@node-minify/html-minifier": "^8.0.6", + "@sardine/eleventy-plugin-tinyhtml": "^0.2.0", "cpx": "^1.5.0", - "node-sass": "^5.0.0", - "node-sass-tilde-importer": "^1.0.2", - "npm-run-all": "^4.1.5" + "eleventy-plugin-rev": "^2.0.0", + "eleventy-sass": "^2.2.4", + "glob": "^10.3.10", + "minify": "^10.5.2", + "postcss": "^8.4.32", + "sass": "^1.69.5", + "uncss": "^0.17.3" }, "dependencies": { - "@fortawesome/fontawesome-free": "^5.15.3", - "bootstrap": "^4.6.0", - "flag-icon-css": "^3.5.0" + "bootstrap": "^5.3.2" } } diff --git a/src/_data/donations.json b/src/_data/donations.json index 6e22d35..30cff4e 100644 --- a/src/_data/donations.json +++ b/src/_data/donations.json @@ -1,10 +1,281 @@ -{ - "banktransfer": { - "number": 4, - "color": "success" +[ + { + "year": 2023, + "donations": [ + { + "date": "16.11.2023", + "amount": 25, + "via": "banktransfer", + "from": null, + "first": false + }, + { + "date": "26.10.2023", + "amount": 20, + "via": "banktransfer", + "from": "ub1x", + "first": false + }, + { + "date": "26.10.2023", + "amount": 20, + "via": "banktransfer", + "from": "ub1x", + "first": false + }, + { + "date": "17.07.2023", + "amount": 20, + "via": "banktransfer", + "from": "ub1x", + "first": false + }, + { + "date": "27.02.2023", + "amount": 40, + "via": "banktransfer", + "from": "xhoff637", + "first": true + }, + { + "date": "20.02.2023", + "amount": 50, + "via": "banktransfer", + "from": null, + "first": false + }, + { + "date": "16.01.2023", + "amount": 12, + "via": "banktransfer", + "from": null, + "first": true + } + ] }, - "cash": { - "number": 0, - "color": "inactive" + { + "year": 2022, + "donations": [ + { + "date": "15.12.2022", + "amount": 20, + "via": "banktransfer", + "from": "ub1x", + "first": false + }, + { + "date": "16.11.2022", + "amount": 25, + "via": "banktransfer", + "from": null, + "first": false + }, + { + "date": "02.11.2022", + "amount": 30, + "via": "banktransfer", + "from": null, + "first": false + }, + { + "date": "11.08.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "11.07.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "03.06.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "11.05.2022", + "amount": 20, + "via": "banktransfer", + "from": "ub1x", + "first": true + }, + { + "date": "03.05.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "04.04.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "15.03.2022", + "amount": 1, + "via": "banktransfer", + "from": "Jonathan Klatt", + "first": false + }, + { + "date": "10.03.2022", + "amount": 1.42, + "via": "banktransfer", + "from": "Jonathan Klatt", + "first": false + }, + { + "date": "03.03.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "14.02.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": false + }, + { + "date": "17.01.2022", + "amount": 1, + "via": "banktransfer", + "from": "Jonathan Klatt", + "first": true + }, + { + "date": "03.01.2022", + "amount": 10, + "via": "banktransfer", + "from": "TeomaHK", + "first": true + } + ] + }, + { + "year": 2021, + "donations": [ + { + "date": "30.11.2021", + "amount": 5, + "via": "banktransfer", + "from": "Rumo", + "first": false + }, + { + "date": "16.11.2021", + "amount": 120, + "via": "banktransfer", + "from": "poetaster", + "first": true + }, + { + "date": "16.11.2021", + "amount": 25, + "via": "banktransfer", + "from": null, + "first": true + }, + { + "date": "20.09.2021", + "amount": 20, + "via": "banktransfer", + "from": "Clemi", + "first": true + }, + { + "date": "26.08.2021", + "amount": 18.27, + "via": "opencollective", + "from": "Skoop", + "first": true + }, + { + "date": "18.08.2021", + "amount": 30, + "via": "banktransfer", + "from": null, + "first": true + }, + { + "date": "02.08.2021", + "amount": 10, + "via": "banktransfer", + "from": null, + "first": false + }, + { + "date": "01.06.2021", + "amount": 20, + "via": "banktransfer", + "from": "Rumo", + "first": true + }, + { + "date": "26.04.2021", + "amount": 8.96, + "via": "banktransfer", + "from": null, + "first": false + }, + { + "date": "23.03.2021", + "amount": 30, + "via": "banktransfer", + "from": null, + "first": true + }, + { + "date": "19.02.2021", + "amount": 20, + "via": "banktransfer", + "from": null, + "first": true + }, + { + "date": "02.02.2021", + "amount": 9.01, + "via": "opencollective", + "from": "Dennis H.", + "first": true + }, + { + "date": "20.01.2021", + "amount": 10, + "via": "banktransfer", + "from": null, + "first": true + }, + { + "date": "17.01.2021", + "amount": 4.41, + "via": "opencollective", + "from": "Michael Haak", + "first": true + } + ] + }, + { + "year": 2020, + "donations": [ + { + "date": "04.12.2020", + "amount": 10, + "via": "banktransfer", + "from": null, + "first": true + } + ] } -} +] diff --git a/src/_data/expenses.json b/src/_data/expenses.json new file mode 100644 index 0000000..0d30bc9 --- /dev/null +++ b/src/_data/expenses.json @@ -0,0 +1,33 @@ +[ + { + "name": "Services", + "type": "Dedicated Server", + "provider": "Hetzner", + "location": "Falkenstein", + "amount": 75.8 + }, + { + "name": "Monitoring", + "type": "Cloud Server", + "provider": "Hetzner", + "location": "Nürnberg", + "amount": 4.51 + }, + { + "name": "Backup", + "type": "Storage Box", + "provider": "Hetzner", + "location": "Helsinki", + "amount": 12.97 + }, + { + "name": { + "en": "Websites", + "de": "Webseiten" + }, + "type": "Webhosting", + "provider": "netcup", + "location": "Nürnberg", + "amount": 2.17 + } +] diff --git a/src/_data/services.json b/src/_data/services.json index 2f3272a..3c8ce56 100644 --- a/src/_data/services.json +++ b/src/_data/services.json @@ -1,111 +1,112 @@ [ - { - "id": "searx", - "name": "Searx", - "icon": "fas fa-search", - "url": "https://searx.sp-codes.de", - "status": "https://searx.sp-codes.de", - "summary": { - "de": "Eine privatsphären-respektierende, hackbare Metasuchmaschine.", - "en": "A privacy-respecting, hackable metasearch engine." - } - }, { "id": "matrix", "name": "Matrix", - "icon": "fas fa-comments", + "icon": "i-comments", "url": "https://chat.sp-codes.de", - "status": "https://matrix.sp-codes.de/_matrix/static/", + "status": "1", "summary": { - "de": "Ein offenes Netzwerk für sichere, dezentralisierte Kommunikation.", - "en": "An open network for secure, decentralized communication." + "de": "Die offene Plattform für sichere und dezentrale Kommunikation.", + "en": "The open platform for secure and decentralized communication." } }, { "id": "jitsi", "name": "Jitsi Meet", - "icon": "fas fa-users", + "icon": "i-users", "url": "https://jitsi.sp-codes.de", - "status": "https://jitsi.sp-codes.de", + "status": "2", "summary": { - "de": "Eine sichere, einfache und skalierbare Plattform für Videokonferenzen.", - "en": "A secure, simple and scalable platform for video conferencing." + "de": "Einfache Videokonferenzen mit Leichtigkeit.", + "en": "Easy video conferencing with ease." } }, { - "id": "gitea", - "name": "Gitea", - "icon": "fas fa-code", - "url": "https://git.sp-codes.de", - "status": "https://git.sp-codes.de", + "id": "mastodon", + "name": "Mastodon", + "icon": "i-mastodon", + "url": "https://social.sp-codes.de", + "status": "7", "summary": { - "de": "Eine leichtgewichtige Code-Hosting-Plattform für git.", - "en": "A lightweight code hosting platform for git." + "de": "Das soziale Netzwerk für dezentrale und sichere Interaktion.", + "en": "The social network for decentralized and secure interaction." + } + }, + { + "id": "peertube", + "name": "PeerTube", + "icon": "i-peertube", + "url": "https://tube.sp-codes.de", + "status": "9", + "summary": { + "de": "Die freie und dezentrale Plattform für gemeinsames Video-Streaming.", + "en": "The free and decentralized platform for collaborative video streaming." + } + }, + { + "id": "pixelfed", + "name": "Pixelfed", + "icon": "i-pixelfed", + "url": "https://pixel.sp-codes.de", + "status": "11", + "beta": true, + "summary": { + "de": "Das soziale Netzwerk für den Austausch von Bildern und Fotografie.", + "en": "The social network for sharing images and photography." + } + }, + { + "id": "forgejo", + "name": "Forgejo", + "icon": "i-git", + "url": "https://git.sp-codes.de", + "status": "13", + "summary": { + "de": "Die Plattform für einfaches und sicheres Code-Hosting.", + "en": "The platform for easy and secure code hosting." } }, { "id": "connectivitycheck", "name": "Captive Portal Check", - "icon": "fas fa-wifi", - "status": "https://connectivitycheck.sp-codes.de/generate204", + "icon": "i-wifi", + "status": "19", "summary": { - "de": "Eine datenschutzfreundliches Tool, um Anmeldeseiten in WLAN-Netzwerken zu erkennen.", - "en": "A privacy friendly Service to detect captive portals in WIFI networks." + "de": "Überprüfung der Verfügbarkeit von Internetzugang.", + "en": "Verification of Internet access availability." } }, { - "id": "firefox-sync", - "name": "Firefox Sync", - "icon": "fab fa-firefox-browser", - "status": "https://sync.firefox.sp-codes.de/token/", + "id": "ntfy", + "name": "ntfy", + "icon": "i-cloud-download", + "url": "https://ntfy.sp-codes.de", + "status": "18", "summary": { - "de": "Ein Service um Firefox Einstellungen, Lesezeichen, offene Tabs und vieles mehr über verschiedene Geräte zu synchronisieren.", - "en": "A service to sync Firefox settings, bookmarks, open tabs and much more between multiple devices." + "de": "Echtzeitbenachrichtigungen mit UnifiedPush-Unterstützung.", + "en": "Real-time notifications with UnifiedPush support." + } + }, + { + "id": "etherpad", + "name": "Etherpad", + "icon": "i-pencil-square", + "url": "https://pad.sp-codes.de", + "status": "17", + "summary": { + "de": "Gemeinsame Echtzeit-Textbearbeitung für effektive Zusammenarbeit.", + "en": "Collaborative real-time text editing for effective collaboration." } }, { "id": "shields", "name": "Shields", - "icon": "fas fa-tags", + "icon": "i-tags", "url": "https://shields.sp-codes.de", - "status": "https://shields.sp-codes.de", + "status": "20", "summary": { - "de": "Prägnante, konsistente und lesbare Badges im SVG- und Rasterformat.", - "en": "Concise, consistent, and legible badges in SVG and raster format." - } - }, - { - "id": "invidious", - "name": "Invidious", - "icon": "fab fa-youtube", - "url": "https://invidious.sp-codes.de", - "status": "https://invidious.sp-codes.de", - "summary": { - "de": "Ein alternatives YouTube-Frontend.", - "en": "An alternative YouTube-Frontend." - } - }, - { - "id": "nitter", - "name": "Nitter", - "icon": "fab fa-twitter", - "url": "https://nitter.sp-codes.de", - "status": "https://nitter.sp-codes.de", - "summary": { - "de": "Ein alternatives Twitter-Frontend.", - "en": "An alternative Twitter-Frontend." - } - }, - { - "id": "yotter", - "name": "Yotter", - "icon": "fas fa-desktop", - "url": "https://yotter.sp-codes.de", - "status": "https://yotter.sp-codes.de", - "beta": true, - "summary": { - "de": "Ein alternatives Twitter- und YouTube-Frontend.", - "en": "An alternative Twitter- and YouTube-Frontend." + "de": "Visuelle Badges zur Anzeige von Projektinformationen und Status.", + "en": "Visual badges for displaying project information and status." } } ] diff --git a/src/_data/strings.json b/src/_data/strings.json index 5748020..2fe4cff 100644 --- a/src/_data/strings.json +++ b/src/_data/strings.json @@ -32,11 +32,59 @@ "en": "Online" }, "outage": { - "de": "Fehler", - "en": "Error" + "de": "Ausfall", + "en": "Outage" }, "maintenance": { "de": "Wartung", "en": "Maintenance" + }, + "date": { + "de": "Datum", + "en": "Date" + }, + "amount": { + "de": "Betrag", + "en": "Amount" + }, + "via": { + "de": "Via", + "en": "Via" + }, + "from": { + "de": "Von", + "en": "Form" + }, + "banktransfer": { + "de": "Überweisung", + "en": "Bank transfer" + }, + "total": { + "de": "Gesamt", + "en": "Total" + }, + "name": { + "de": "Name", + "en": "Name" + }, + "type": { + "de": "Typ", + "en": "Type" + }, + "provider": { + "de": "Anbieter", + "en": "Provider" + }, + "location": { + "de": "Standort", + "en": "Location" + }, + "month": { + "de": "Monat", + "en": "Month" + }, + "year": { + "de": "Jahr", + "en": "Year" } } diff --git a/src/_includes/donations-current.html b/src/_includes/donations-current.html new file mode 100644 index 0000000..16e3174 --- /dev/null +++ b/src/_includes/donations-current.html @@ -0,0 +1,36 @@ +
{{strings.date[locale]}} | +{{strings.via[locale]}} | +{{strings.from[locale]}} | +{{strings.amount[locale]}} | +||
---|---|---|---|---|---|
{{donation.date}} | + {% if donation.via == 'opencollective' %} +Open Collective | + {% else %} +{{strings[donation.via][locale]}} | + {% endif %} + {% if donation.from %} +{{donation.from}} | + {% else %} +*** | + {% endif %} +{{donation.amount | amount}} € | +
{{strings.total[locale]}} | +{{donations[0].donations | sum | amount}} € | +
{{strings.date[locale]}} | +{{strings.via[locale]}} | +{{strings.from[locale]}} | +{{strings.amount[locale]}} | +||
---|---|---|---|---|---|
{{donation.date}} | + {% if donation.via == 'opencollective' %} +Open Collective | + {% else %} +{{strings[donation.via][locale]}} | + {% endif %} + {% if donation.from %} +{{donation.from}} | + {% else %} +*** | + {% endif %} +{{donation.amount | amount}} € | +
{{strings.total[locale]}} | +{{year.donations | sum | amount}} € | +
{{strings.name[locale]}} | +{{strings.type[locale]}} | +{{strings.provider[locale]}} | +{{strings.location[locale]}} | +{{strings.amount[locale]}} / {{strings.month[locale]}} | +{{strings.amount[locale]}} / {{strings.year[locale]}} | +|
---|---|---|---|---|---|---|
{{expense.name[locale]}} | + {% else %} +{{expense.name}} | + {% endif %} +{{expense.type}} | +{{expense.provider}} | +{{expense.location}} | +{{expense.amount | amount}} € | +{{expense.amount | year | amount}} € | +
{{strings.total[locale]}} | +{{expenses | sum | amount}} € | +{{expenses | sum | year | amount}} € | +
+ Komm gerne in der Matrix-Gruppe vorbei und lass uns diskutieren. Ich freue mich auf dein Feedback! +
+ #sp-codes:matrix.sp-codes.de ++ Mein Name ist Samuel Philipp und ich bin ein Software Engineer aus Magdeburg. In meiner Freizeit + hoste ich verschiedene freie Dienste. Hier schreibe ich Artikel rund um + Sicherheit und Datenschutz. +
+ Mehr erfahren +