From c9b2a96efa2c2240e8601a72d07266ec71e38551 Mon Sep 17 00:00:00 2001 From: NickiCloud Date: Wed, 2 Sep 2026 20:51:41 +0200 Subject: [PATCH] Initial profile setup --- .gitea/workflows/update-profile.yml | 65 + .gitignore | 16 + README.md | 94 +- heatmap.svg | 1220 +++++++++++++++++ src/ProfileManager/Models/GiteaRepo.cs | 42 + src/ProfileManager/Models/HeatmapItem.cs | 15 + src/ProfileManager/Models/ProfileStats.cs | 28 + src/ProfileManager/ProfileManager.csproj | 11 + src/ProfileManager/Program.cs | 143 ++ src/ProfileManager/Services/GiteaApiClient.cs | 208 +++ src/ProfileManager/Services/ReadmeInjector.cs | 70 + .../Services/StatsCalculator.cs | 180 +++ src/ProfileManager/Services/SvgGenerator.cs | 213 +++ 13 files changed, 2304 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/update-profile.yml create mode 100644 .gitignore create mode 100644 heatmap.svg create mode 100644 src/ProfileManager/Models/GiteaRepo.cs create mode 100644 src/ProfileManager/Models/HeatmapItem.cs create mode 100644 src/ProfileManager/Models/ProfileStats.cs create mode 100644 src/ProfileManager/ProfileManager.csproj create mode 100644 src/ProfileManager/Program.cs create mode 100644 src/ProfileManager/Services/GiteaApiClient.cs create mode 100644 src/ProfileManager/Services/ReadmeInjector.cs create mode 100644 src/ProfileManager/Services/StatsCalculator.cs create mode 100644 src/ProfileManager/Services/SvgGenerator.cs diff --git a/.gitea/workflows/update-profile.yml b/.gitea/workflows/update-profile.yml new file mode 100644 index 0000000..85c8f4d --- /dev/null +++ b/.gitea/workflows/update-profile.yml @@ -0,0 +1,65 @@ +name: Update Gitea Profile + +on: + schedule: + # Täglich um 00:00 Uhr UTC + - cron: '0 0 * * *' + # Erlaubt das manuelle Ausführen über das Gitea Web-Interface + workflow_dispatch: + # Optional: Auch ausführen, wenn manuelle Änderungen an Code oder README gepusht werden + push: + branches: + - main + paths-ignore: + - 'heatmap.svg' + - 'README.md' + +jobs: + update-profile: + runs-on: ubuntu-latest + + # Berechtigungen für das Schreiben ins Repository + permissions: + contents: write + + steps: + - name: 📥 Repository auschecken + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # Nutzt standardmäßig das automatische Gitea-Token oder ein benutzerdefiniertes Secret + token: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }} + + - name: ⚙️ .NET 10 SDK einrichten + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: 🚀 C# Profile-Manager ausführen + env: + GITEA_API_URL: 'https://git.webcore-consulting.dev/api/v1' + GITEA_USERNAME: 'NickiCloud' + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }} + run: | + dotnet run --project src/ProfileManager + + - name: 📝 Änderungen prüfen, committen und pushen + run: | + # Git-Identität für den Bot konfigurieren + git config --global user.name "NickiBot[bot]" + git config --global user.email "bot@webcore-consulting.dev" + + # Prüfen, ob sich heatmap.svg oder README.md geändert haben + if git status --porcelain | grep -E "(heatmap.svg|README.md)"; then + echo "Änderungen an heatmap.svg oder README.md erkannt. Committe..." + git add heatmap.svg README.md + + # GANZ WICHTIG: [skip ci] verhindert Endlosschleifen in Gitea Actions! + git commit -m "chore(profile): update activity heatmap & stats [skip ci]" + + # Änderungen pushen + git push + echo "Profil erfolgreich aktualisiert und gepusht! ✨" + else + echo "Keine Änderungen vorhanden. Alles auf dem neuesten Stand. 👍" + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e46e37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +## .NET Build & Output +bin/ +obj/ +*.user +*.suo +*.userosscache +*.sln.docstates + +## IDEs +.idea/ +.vscode/ +*.swp + +## OS Files +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index 4ebce99..65b242c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,94 @@ -# .profile +
+# 🦊 Hey, ich bin Nicki! + +### 🎓 Fachabiturient & Leidenschaftlicher C# / .NET Softwareentwickler +*Building high-performance desktop & web applications with a touch of purple fox magic.* + +
+ +[![C#](https://img.shields.io/badge/C%23-239120?style=for-the-badge&logo=csharp&logoColor=white)](https://dotnet.microsoft.com/) +[![.NET 10](https://img.shields.io/badge/.NET_10-512BD4?style=for-the-badge&logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) +[![Avalonia UI](https://img.shields.io/badge/Avalonia_UI-7e3f98?style=for-the-badge&logo=dotnet&logoColor=white)](https://avaloniaui.net/) +[![Blazor](https://img.shields.io/badge/Blazor-512BD4?style=for-the-badge&logo=blazor&logoColor=white)](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) +[![MudBlazor](https://img.shields.io/badge/MudBlazor-9c27b0?style=for-the-badge&logo=blazor&logoColor=white)](https://mudblazor.com/) + +[![JetBrains Rider](https://img.shields.io/badge/JetBrains_Rider-000000?style=for-the-badge&logo=rider&logoColor=white)](https://www.jetbrains.com/rider/) +[![Pop!_OS](https://img.shields.io/badge/Pop!_OS-48B9C7?style=for-the-badge&logo=popos&logoColor=white)](https://pop.system76.com/) +[![Debian](https://img.shields.io/badge/Debian-A81D33?style=for-the-badge&logo=debian&logoColor=white)](https://www.debian.org/) +[![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) +[![Gitea](https://img.shields.io/badge/Gitea-609926?style=for-the-badge&logo=gitea&logoColor=white)](https://about.gitea.com/) + +--- + +### 🐾 "Schlau wie ein Fuchs, schnell wie .NET & kompromisslos self-hosted." + +
+ +## 🧙‍♂️ Über mich & Meine Mission + +Ich bin **Nicki**, angehender Softwareentwickler mit klarem Fokus auf moderne Software-Architektur im .NET-Ökosystem. Wenn ich nicht gerade sauberen, asynchronen C#-Code in **JetBrains Rider auf Pop!_OS** tippe, optimiere ich meine eigene Linux-Server-Infrastruktur oder erkunde virtuelle Welten. + +- 💻 **Mein Fokus:** C#, .NET 10, XAML, Cross-Platform UIs mit **Avalonia UI** und reaktive Web-UIs mit **Blazor & MudBlazor**. +- 🐧 **Mein OS & Dev-Setup:** Rider-Enthusiast auf **Pop!_OS (Linux)** – weil Entwicklungs-Power Freiheit braucht. +- 🏰 **Self-Hosting & Homelab:** Eigener Debian-Server mit Docker-Containern: Gitea, Mailcow, Nginx Reverse-Proxy, abgesichert mit UFW & fail2ban. +- 🎮 **Gaming & Abschalten:** Wenn der Build grün ist, findet man mich in **Minecraft** (Redstone & Modpacks), taktischen Matches in **Rainbow Six Siege** oder beim Farmen in **Warframe**. +- 🦊 **Das Fuchs-Branding:** Lila Akzente, Neugier, Agilität und maximale Eleganz in Code & UI. + +--- + +
+ +## 📊 Meine Gitea Aktivität & Streak + + +Nicki's Gitea Contribution Heatmap + +
+ +--- + +## 🛠️ Ausgewählte Projekte + +Hier sind ein paar Highlights aus meiner Entwickler-Schmiede: + +| Projekt | Stack | Beschreibung | +| :--- | :---: | :--- | +| 📝 **Markdown Editor Pro** | `Avalonia UI` • `C#` | Eleganter, performanter Desktop-Editor mit Live-Rendering, Math-Support und Theme-Engine. | +| 💧 **CloudDrop** | `ASP.NET Core` • `Docker` | Self-hosted File-Sharing Service für schnelle, verschlüsselte Uploads & temporäre Links. | +| 💳 **KasseApp** | `C#` • `SQLite` • `WPF/Avalonia` | Modulare POS- und Kassenabrechnungs-Software mit Offline-Synchronisation für Events. | +| 🥤 **DrinkReminder** | `C#` • `Avalonia UI` | Schicker, unaufdringlicher Desktop- & Mobile-Helfer für gesunde Hydration im Dev-Alltag. | + +--- + +## 🚀 Zuletzt aktualisierte Repositories + + + +- 🚀 [**.profile**](https://git.webcore-consulting.dev/NickiCloud/.profile) – 🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10 + `C# / Markdown` • 🕒 *Aktualisiert am 02.09.2026* + + +--- + +
+ +### 🎮 Gaming Corner & Fun Facts + +[![Minecraft](https://img.shields.io/badge/Minecraft-5B8C33?style=for-the-badge&logo=minecraft&logoColor=white)](https://minecraft.net) +[![Rainbow Six Siege](https://img.shields.io/badge/Rainbow_Six_Siege-000000?style=for-the-badge&logo=ubisoft&logoColor=white)](https://www.ubisoft.com/game/rainbow-six/siege) +[![Warframe](https://img.shields.io/badge/Warframe-7e3f98?style=for-the-badge&logo=target&logoColor=white)](https://www.warframe.com/) + +```csharp +var nicki = new Developer +{ + Drink = "Kühles Spezi oder Mate", + Editor = "JetBrains Rider", + Terminal = "zsh on Pop!_OS", + Motto = "Refactor early, test continuously, deploy autonomously." +}; +``` + +Gebaut mit purer C# .NET 10 Power & Gitea Actions • Design im Lila-Fuchs-Style 💜 + +
diff --git a/heatmap.svg b/heatmap.svg new file mode 100644 index 0000000..0856db1 --- /dev/null +++ b/heatmap.svg @@ -0,0 +1,1220 @@ + + + + + + + + + + + + + + + 🦊 NickiCloud Gitea Activity + + + 21 Commits im letzten Jahr + + + Sep + Okt + Nov + Dez + Jan + Feb + Mär + Apr + Mai + Jun + Jul + Aug + + Mo + Mi + Fr + + + + 08.09.2025: 0 Commits + + + 09.09.2025: 0 Commits + + + 10.09.2025: 0 Commits + + + 11.09.2025: 0 Commits + + + 12.09.2025: 0 Commits + + + 13.09.2025: 0 Commits + + + 14.09.2025: 0 Commits + + + 15.09.2025: 0 Commits + + + 16.09.2025: 0 Commits + + + 17.09.2025: 0 Commits + + + 18.09.2025: 0 Commits + + + 19.09.2025: 0 Commits + + + 20.09.2025: 0 Commits + + + 21.09.2025: 0 Commits + + + 22.09.2025: 0 Commits + + + 23.09.2025: 0 Commits + + + 24.09.2025: 0 Commits + + + 25.09.2025: 0 Commits + + + 26.09.2025: 0 Commits + + + 27.09.2025: 0 Commits + + + 28.09.2025: 0 Commits + + + 29.09.2025: 0 Commits + + + 30.09.2025: 0 Commits + + + 01.10.2025: 0 Commits + + + 02.10.2025: 0 Commits + + + 03.10.2025: 0 Commits + + + 04.10.2025: 0 Commits + + + 05.10.2025: 0 Commits + + + 06.10.2025: 0 Commits + + + 07.10.2025: 0 Commits + + + 08.10.2025: 0 Commits + + + 09.10.2025: 0 Commits + + + 10.10.2025: 0 Commits + + + 11.10.2025: 0 Commits + + + 12.10.2025: 0 Commits + + + 13.10.2025: 0 Commits + + + 14.10.2025: 0 Commits + + + 15.10.2025: 0 Commits + + + 16.10.2025: 0 Commits + + + 17.10.2025: 0 Commits + + + 18.10.2025: 0 Commits + + + 19.10.2025: 0 Commits + + + 20.10.2025: 0 Commits + + + 21.10.2025: 0 Commits + + + 22.10.2025: 0 Commits + + + 23.10.2025: 0 Commits + + + 24.10.2025: 0 Commits + + + 25.10.2025: 0 Commits + + + 26.10.2025: 0 Commits + + + 27.10.2025: 0 Commits + + + 28.10.2025: 0 Commits + + + 29.10.2025: 0 Commits + + + 30.10.2025: 0 Commits + + + 31.10.2025: 0 Commits + + + 01.11.2025: 0 Commits + + + 02.11.2025: 0 Commits + + + 03.11.2025: 0 Commits + + + 04.11.2025: 0 Commits + + + 05.11.2025: 0 Commits + + + 06.11.2025: 0 Commits + + + 07.11.2025: 0 Commits + + + 08.11.2025: 0 Commits + + + 09.11.2025: 0 Commits + + + 10.11.2025: 0 Commits + + + 11.11.2025: 0 Commits + + + 12.11.2025: 0 Commits + + + 13.11.2025: 0 Commits + + + 14.11.2025: 0 Commits + + + 15.11.2025: 0 Commits + + + 16.11.2025: 0 Commits + + + 17.11.2025: 0 Commits + + + 18.11.2025: 0 Commits + + + 19.11.2025: 0 Commits + + + 20.11.2025: 0 Commits + + + 21.11.2025: 0 Commits + + + 22.11.2025: 0 Commits + + + 23.11.2025: 0 Commits + + + 24.11.2025: 0 Commits + + + 25.11.2025: 0 Commits + + + 26.11.2025: 0 Commits + + + 27.11.2025: 0 Commits + + + 28.11.2025: 0 Commits + + + 29.11.2025: 0 Commits + + + 30.11.2025: 0 Commits + + + 01.12.2025: 0 Commits + + + 02.12.2025: 0 Commits + + + 03.12.2025: 0 Commits + + + 04.12.2025: 0 Commits + + + 05.12.2025: 0 Commits + + + 06.12.2025: 0 Commits + + + 07.12.2025: 0 Commits + + + 08.12.2025: 0 Commits + + + 09.12.2025: 0 Commits + + + 10.12.2025: 0 Commits + + + 11.12.2025: 0 Commits + + + 12.12.2025: 0 Commits + + + 13.12.2025: 0 Commits + + + 14.12.2025: 0 Commits + + + 15.12.2025: 0 Commits + + + 16.12.2025: 0 Commits + + + 17.12.2025: 0 Commits + + + 18.12.2025: 0 Commits + + + 19.12.2025: 0 Commits + + + 20.12.2025: 0 Commits + + + 21.12.2025: 0 Commits + + + 22.12.2025: 0 Commits + + + 23.12.2025: 0 Commits + + + 24.12.2025: 0 Commits + + + 25.12.2025: 0 Commits + + + 26.12.2025: 0 Commits + + + 27.12.2025: 0 Commits + + + 28.12.2025: 0 Commits + + + 29.12.2025: 0 Commits + + + 30.12.2025: 0 Commits + + + 31.12.2025: 0 Commits + + + 01.01.2026: 0 Commits + + + 02.01.2026: 0 Commits + + + 03.01.2026: 0 Commits + + + 04.01.2026: 0 Commits + + + 05.01.2026: 0 Commits + + + 06.01.2026: 0 Commits + + + 07.01.2026: 0 Commits + + + 08.01.2026: 0 Commits + + + 09.01.2026: 0 Commits + + + 10.01.2026: 0 Commits + + + 11.01.2026: 0 Commits + + + 12.01.2026: 0 Commits + + + 13.01.2026: 0 Commits + + + 14.01.2026: 0 Commits + + + 15.01.2026: 0 Commits + + + 16.01.2026: 0 Commits + + + 17.01.2026: 0 Commits + + + 18.01.2026: 0 Commits + + + 19.01.2026: 0 Commits + + + 20.01.2026: 0 Commits + + + 21.01.2026: 0 Commits + + + 22.01.2026: 0 Commits + + + 23.01.2026: 0 Commits + + + 24.01.2026: 0 Commits + + + 25.01.2026: 0 Commits + + + 26.01.2026: 0 Commits + + + 27.01.2026: 0 Commits + + + 28.01.2026: 0 Commits + + + 29.01.2026: 0 Commits + + + 30.01.2026: 0 Commits + + + 31.01.2026: 0 Commits + + + 01.02.2026: 0 Commits + + + 02.02.2026: 0 Commits + + + 03.02.2026: 0 Commits + + + 04.02.2026: 0 Commits + + + 05.02.2026: 0 Commits + + + 06.02.2026: 0 Commits + + + 07.02.2026: 0 Commits + + + 08.02.2026: 0 Commits + + + 09.02.2026: 0 Commits + + + 10.02.2026: 0 Commits + + + 11.02.2026: 0 Commits + + + 12.02.2026: 0 Commits + + + 13.02.2026: 0 Commits + + + 14.02.2026: 0 Commits + + + 15.02.2026: 0 Commits + + + 16.02.2026: 0 Commits + + + 17.02.2026: 0 Commits + + + 18.02.2026: 0 Commits + + + 19.02.2026: 0 Commits + + + 20.02.2026: 0 Commits + + + 21.02.2026: 0 Commits + + + 22.02.2026: 0 Commits + + + 23.02.2026: 0 Commits + + + 24.02.2026: 0 Commits + + + 25.02.2026: 0 Commits + + + 26.02.2026: 0 Commits + + + 27.02.2026: 0 Commits + + + 28.02.2026: 0 Commits + + + 01.03.2026: 0 Commits + + + 02.03.2026: 0 Commits + + + 03.03.2026: 0 Commits + + + 04.03.2026: 0 Commits + + + 05.03.2026: 0 Commits + + + 06.03.2026: 0 Commits + + + 07.03.2026: 0 Commits + + + 08.03.2026: 0 Commits + + + 09.03.2026: 0 Commits + + + 10.03.2026: 0 Commits + + + 11.03.2026: 0 Commits + + + 12.03.2026: 0 Commits + + + 13.03.2026: 0 Commits + + + 14.03.2026: 0 Commits + + + 15.03.2026: 0 Commits + + + 16.03.2026: 0 Commits + + + 17.03.2026: 0 Commits + + + 18.03.2026: 0 Commits + + + 19.03.2026: 0 Commits + + + 20.03.2026: 0 Commits + + + 21.03.2026: 0 Commits + + + 22.03.2026: 0 Commits + + + 23.03.2026: 0 Commits + + + 24.03.2026: 0 Commits + + + 25.03.2026: 0 Commits + + + 26.03.2026: 0 Commits + + + 27.03.2026: 0 Commits + + + 28.03.2026: 0 Commits + + + 29.03.2026: 0 Commits + + + 30.03.2026: 0 Commits + + + 31.03.2026: 0 Commits + + + 01.04.2026: 0 Commits + + + 02.04.2026: 0 Commits + + + 03.04.2026: 0 Commits + + + 04.04.2026: 0 Commits + + + 05.04.2026: 0 Commits + + + 06.04.2026: 0 Commits + + + 07.04.2026: 0 Commits + + + 08.04.2026: 0 Commits + + + 09.04.2026: 0 Commits + + + 10.04.2026: 0 Commits + + + 11.04.2026: 0 Commits + + + 12.04.2026: 0 Commits + + + 13.04.2026: 0 Commits + + + 14.04.2026: 0 Commits + + + 15.04.2026: 0 Commits + + + 16.04.2026: 0 Commits + + + 17.04.2026: 0 Commits + + + 18.04.2026: 0 Commits + + + 19.04.2026: 0 Commits + + + 20.04.2026: 0 Commits + + + 21.04.2026: 0 Commits + + + 22.04.2026: 0 Commits + + + 23.04.2026: 0 Commits + + + 24.04.2026: 0 Commits + + + 25.04.2026: 0 Commits + + + 26.04.2026: 0 Commits + + + 27.04.2026: 0 Commits + + + 28.04.2026: 0 Commits + + + 29.04.2026: 0 Commits + + + 30.04.2026: 0 Commits + + + 01.05.2026: 0 Commits + + + 02.05.2026: 0 Commits + + + 03.05.2026: 0 Commits + + + 04.05.2026: 0 Commits + + + 05.05.2026: 0 Commits + + + 06.05.2026: 0 Commits + + + 07.05.2026: 0 Commits + + + 08.05.2026: 0 Commits + + + 09.05.2026: 0 Commits + + + 10.05.2026: 0 Commits + + + 11.05.2026: 0 Commits + + + 12.05.2026: 0 Commits + + + 13.05.2026: 0 Commits + + + 14.05.2026: 0 Commits + + + 15.05.2026: 0 Commits + + + 16.05.2026: 0 Commits + + + 17.05.2026: 0 Commits + + + 18.05.2026: 0 Commits + + + 19.05.2026: 0 Commits + + + 20.05.2026: 0 Commits + + + 21.05.2026: 0 Commits + + + 22.05.2026: 0 Commits + + + 23.05.2026: 0 Commits + + + 24.05.2026: 0 Commits + + + 25.05.2026: 0 Commits + + + 26.05.2026: 0 Commits + + + 27.05.2026: 0 Commits + + + 28.05.2026: 0 Commits + + + 29.05.2026: 0 Commits + + + 30.05.2026: 0 Commits + + + 31.05.2026: 0 Commits + + + 01.06.2026: 0 Commits + + + 02.06.2026: 0 Commits + + + 03.06.2026: 0 Commits + + + 04.06.2026: 0 Commits + + + 05.06.2026: 0 Commits + + + 06.06.2026: 0 Commits + + + 07.06.2026: 0 Commits + + + 08.06.2026: 0 Commits + + + 09.06.2026: 0 Commits + + + 10.06.2026: 0 Commits + + + 11.06.2026: 0 Commits + + + 12.06.2026: 0 Commits + + + 13.06.2026: 0 Commits + + + 14.06.2026: 0 Commits + + + 15.06.2026: 0 Commits + + + 16.06.2026: 0 Commits + + + 17.06.2026: 0 Commits + + + 18.06.2026: 0 Commits + + + 19.06.2026: 0 Commits + + + 20.06.2026: 0 Commits + + + 21.06.2026: 0 Commits + + + 22.06.2026: 0 Commits + + + 23.06.2026: 0 Commits + + + 24.06.2026: 0 Commits + + + 25.06.2026: 0 Commits + + + 26.06.2026: 0 Commits + + + 27.06.2026: 0 Commits + + + 28.06.2026: 0 Commits + + + 29.06.2026: 0 Commits + + + 30.06.2026: 0 Commits + + + 01.07.2026: 0 Commits + + + 02.07.2026: 0 Commits + + + 03.07.2026: 0 Commits + + + 04.07.2026: 0 Commits + + + 05.07.2026: 0 Commits + + + 06.07.2026: 0 Commits + + + 07.07.2026: 0 Commits + + + 08.07.2026: 0 Commits + + + 09.07.2026: 0 Commits + + + 10.07.2026: 0 Commits + + + 11.07.2026: 0 Commits + + + 12.07.2026: 0 Commits + + + 13.07.2026: 0 Commits + + + 14.07.2026: 0 Commits + + + 15.07.2026: 0 Commits + + + 16.07.2026: 0 Commits + + + 17.07.2026: 0 Commits + + + 18.07.2026: 0 Commits + + + 19.07.2026: 0 Commits + + + 20.07.2026: 0 Commits + + + 21.07.2026: 0 Commits + + + 22.07.2026: 0 Commits + + + 23.07.2026: 0 Commits + + + 24.07.2026: 0 Commits + + + 25.07.2026: 0 Commits + + + 26.07.2026: 0 Commits + + + 27.07.2026: 0 Commits + + + 28.07.2026: 0 Commits + + + 29.07.2026: 0 Commits + + + 30.07.2026: 0 Commits + + + 31.07.2026: 0 Commits + + + 01.08.2026: 0 Commits + + + 02.08.2026: 0 Commits + + + 03.08.2026: 0 Commits + + + 04.08.2026: 0 Commits + + + 05.08.2026: 0 Commits + + + 06.08.2026: 0 Commits + + + 07.08.2026: 0 Commits + + + 08.08.2026: 0 Commits + + + 09.08.2026: 0 Commits + + + 10.08.2026: 0 Commits + + + 11.08.2026: 0 Commits + + + 12.08.2026: 0 Commits + + + 13.08.2026: 0 Commits + + + 14.08.2026: 0 Commits + + + 15.08.2026: 0 Commits + + + 16.08.2026: 0 Commits + + + 17.08.2026: 0 Commits + + + 18.08.2026: 0 Commits + + + 19.08.2026: 0 Commits + + + 20.08.2026: 0 Commits + + + 21.08.2026: 0 Commits + + + 22.08.2026: 0 Commits + + + 23.08.2026: 0 Commits + + + 24.08.2026: 0 Commits + + + 25.08.2026: 0 Commits + + + 26.08.2026: 0 Commits + + + 27.08.2026: 0 Commits + + + 28.08.2026: 4 Commits + + + 29.08.2026: 0 Commits + + + 30.08.2026: 1 Commits + + + 31.08.2026: 2 Commits + + + 01.09.2026: 9 Commits + + + 02.09.2026: 5 Commits + + + 03.09.2026: 0 Commits + + + 04.09.2026: 0 Commits + + + 05.09.2026: 0 Commits + + + 06.09.2026: 0 Commits + + + + + + 📦 + 21 + Jahres-Beiträge + + + + 🔥 + 4 Tage + Aktueller Streak + + + + + 4 Tage + Längster Streak + + + + + C#: 100,0% + + + + + + C# 100,0% + + diff --git a/src/ProfileManager/Models/GiteaRepo.cs b/src/ProfileManager/Models/GiteaRepo.cs new file mode 100644 index 0000000..27617fa --- /dev/null +++ b/src/ProfileManager/Models/GiteaRepo.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace ProfileManager.Models; + +/// +/// DTO representing a repository in Gitea's /api/v1/users/{user}/repos response. +/// +public sealed class GiteaRepo +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("full_name")] + public string FullName { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("html_url")] + public string HtmlUrl { get; set; } = string.Empty; + + [JsonPropertyName("language")] + public string? Language { get; set; } + + [JsonPropertyName("fork")] + public bool Fork { get; set; } + + [JsonPropertyName("archived")] + public bool Archived { get; set; } + + [JsonPropertyName("stars_count")] + public int StarsCount { get; set; } + + [JsonPropertyName("forks_count")] + public int ForksCount { get; set; } + + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt { get; set; } +} diff --git a/src/ProfileManager/Models/HeatmapItem.cs b/src/ProfileManager/Models/HeatmapItem.cs new file mode 100644 index 0000000..b98b816 --- /dev/null +++ b/src/ProfileManager/Models/HeatmapItem.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace ProfileManager.Models; + +/// +/// DTO representing an entry in Gitea's /api/v1/users/{user}/heatmap response. +/// +public sealed class HeatmapItem +{ + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } + + [JsonPropertyName("contributions")] + public int Contributions { get; set; } +} diff --git a/src/ProfileManager/Models/ProfileStats.cs b/src/ProfileManager/Models/ProfileStats.cs new file mode 100644 index 0000000..ce4d580 --- /dev/null +++ b/src/ProfileManager/Models/ProfileStats.cs @@ -0,0 +1,28 @@ +namespace ProfileManager.Models; + +public sealed class LanguageStat +{ + public required string Name { get; init; } + public required double Percentage { get; init; } + public required int RepoCount { get; init; } + public required string Color { get; init; } +} + +public sealed class DayContribution +{ + public required DateOnly Date { get; init; } + public required int Count { get; init; } + public required int Level { get; init; } // 0..4 + public required int Column { get; init; } // 0..51 + public required int Row { get; init; } // 0..6 (0=Mo..6=So) +} + +public sealed class ProfileStats +{ + public required int TotalContributions365 { get; init; } + public required int CurrentStreak { get; init; } + public required int LongestStreak { get; init; } + public required List Days { get; init; } + public required List TopLanguages { get; init; } + public required List LatestRepos { get; init; } +} diff --git a/src/ProfileManager/ProfileManager.csproj b/src/ProfileManager/ProfileManager.csproj new file mode 100644 index 0000000..e8972fa --- /dev/null +++ b/src/ProfileManager/ProfileManager.csproj @@ -0,0 +1,11 @@ + + + + Exe + net10.0 + enable + enable + latest + + + diff --git a/src/ProfileManager/Program.cs b/src/ProfileManager/Program.cs new file mode 100644 index 0000000..584166b --- /dev/null +++ b/src/ProfileManager/Program.cs @@ -0,0 +1,143 @@ +using System.Text; +using ProfileManager.Services; + +namespace ProfileManager; + +internal static class Program +{ + private static async Task Main(string[] args) + { + Console.OutputEncoding = Encoding.UTF8; + Console.WriteLine(""" + ==================================================== + 🦊 NickiCloud Gitea Profile Manager (.NET 10) + Zero-Dependency Autonomous Activity Engine + ==================================================== + """); + + // Parse arguments / environment variables + var username = GetConfigValue(args, "--username", "GITEA_USERNAME", "NickiCloud"); + var apiUrl = GetConfigValue(args, "--api-url", "GITEA_API_URL", "https://git.webcore-consulting.dev/api/v1"); + var token = GetConfigValue(args, "--token", "GITEA_TOKEN", string.Empty); + var svgPath = GetConfigValue(args, "--output-svg", "OUTPUT_SVG", "heatmap.svg"); + var readmePath = GetConfigValue(args, "--readme", "README_PATH", "README.md"); + + // Resolve paths relative to working directory or repo root + if (!Path.IsPathRooted(svgPath)) + { + svgPath = Path.GetFullPath(Path.Combine(FindRepoRoot(), svgPath)); + } + + if (!Path.IsPathRooted(readmePath)) + { + readmePath = Path.GetFullPath(Path.Combine(FindRepoRoot(), readmePath)); + } + + Console.WriteLine($"[Config] Target User : {username}"); + Console.WriteLine($"[Config] API Endpoint: {apiUrl}"); + Console.WriteLine($"[Config] Output SVG : {svgPath}"); + Console.WriteLine($"[Config] Target README: {readmePath}"); + Console.WriteLine(); + + try + { + var client = new GiteaApiClient(apiUrl, string.IsNullOrWhiteSpace(token) ? null : token); + + // Step 1: Fetch data in parallel + var heatmapTask = client.GetHeatmapAsync(username); + var reposTask = client.GetReposAsync(username); + + await Task.WhenAll(heatmapTask, reposTask); + + var heatmap = await heatmapTask; + var repos = await reposTask; + + // Step 2: Calculate metrics + Console.WriteLine("[Stats] Calculating streaks, contributions and language metrics..."); + var stats = StatsCalculator.Calculate(heatmap, repos); + + Console.WriteLine($"[Stats] Total Contributions (365d): {stats.TotalContributions365}"); + Console.WriteLine($"[Stats] Current Streak : {stats.CurrentStreak} days"); + Console.WriteLine($"[Stats] Longest Streak : {stats.LongestStreak} days"); + Console.WriteLine($"[Stats] Top Languages : {string.Join(", ", stats.TopLanguages.Select(l => $"{l.Name} ({l.Percentage:F1}%)"))}"); + Console.WriteLine($"[Stats] Latest Repositories : {string.Join(", ", stats.LatestRepos.Select(r => r.Name))}"); + Console.WriteLine(); + + // Step 3: Generate animated SVG + Console.WriteLine($"[SVG] Rendering responsive SVG..."); + var svgContent = SvgGenerator.Generate(stats); + var svgDir = Path.GetDirectoryName(svgPath); + if (!string.IsNullOrEmpty(svgDir) && !Directory.Exists(svgDir)) + { + Directory.CreateDirectory(svgDir); + } + await File.WriteAllTextAsync(svgPath, svgContent, Encoding.UTF8); + Console.WriteLine($"[SVG] Saved successfully to: {svgPath}"); + + // Step 4: Inject latest repos into README.md + if (File.Exists(readmePath)) + { + Console.WriteLine($"[README] Injecting latest repositories into: {readmePath}"); + ReadmeInjector.InjectLatestRepos(readmePath, stats.LatestRepos); + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[README-WARN] Target README not found at {readmePath}. Skipping injection."); + Console.ResetColor(); + } + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("\n✨ Profile update finished successfully!"); + Console.ResetColor(); + return 0; + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n❌ Critical Error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + Console.ResetColor(); + return 1; + } + } + + private static string GetConfigValue(string[] args, string flag, string envVar, string defaultValue) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], flag, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + + var env = Environment.GetEnvironmentVariable(envVar); + if (!string.IsNullOrWhiteSpace(env)) + { + return env; + } + + return defaultValue; + } + + private static string FindRepoRoot() + { + var current = Directory.GetCurrentDirectory(); + while (!string.IsNullOrEmpty(current)) + { + if (Directory.Exists(Path.Combine(current, ".git")) || + Directory.Exists(Path.Combine(current, ".gitea")) || + File.Exists(Path.Combine(current, "README.md"))) + { + return current; + } + + var parent = Directory.GetParent(current)?.FullName; + if (parent == current) break; + current = parent; + } + + return Directory.GetCurrentDirectory(); + } +} diff --git a/src/ProfileManager/Services/GiteaApiClient.cs b/src/ProfileManager/Services/GiteaApiClient.cs new file mode 100644 index 0000000..ebcac5c --- /dev/null +++ b/src/ProfileManager/Services/GiteaApiClient.cs @@ -0,0 +1,208 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using ProfileManager.Models; + +namespace ProfileManager.Services; + +public sealed class GiteaApiClient +{ + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public GiteaApiClient(string? baseUrl = null, string? apiToken = null) + { + _baseUrl = (baseUrl ?? Environment.GetEnvironmentVariable("GITEA_API_URL") ?? "https://git.webcore-consulting.dev/api/v1") + .TrimEnd('/'); + + var handler = new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + ConnectTimeout = TimeSpan.FromSeconds(10) + }; + + _httpClient = new HttpClient(handler) + { + Timeout = TimeSpan.FromSeconds(15) + }; + + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NickiProfileBot", "1.0")); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var token = apiToken ?? Environment.GetEnvironmentVariable("GITEA_TOKEN"); + if (!string.IsNullOrWhiteSpace(token)) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + } + + public async Task> GetHeatmapAsync(string username, CancellationToken cancellationToken = default) + { + var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/heatmap"; + Console.WriteLine($"[API] Fetching heatmap from: {url}"); + + try + { + using var response = await _httpClient.GetAsync(url, cancellationToken); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var items = JsonSerializer.Deserialize>(json, JsonOptions); + if (items != null && items.Count > 0) + { + Console.WriteLine($"[API] Successfully retrieved {items.Count} heatmap data points."); + return items; + } + } + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[API-WARN] Heatmap request failed (Status: {response.StatusCode}). Switching to mock fallback."); + Console.ResetColor(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[API-WARN] Network error connecting to Gitea ({ex.Message}). Using realistic fallback data."); + Console.ResetColor(); + } + + return GenerateMockHeatmap(); + } + + public async Task> GetReposAsync(string username, CancellationToken cancellationToken = default) + { + var url = $"{_baseUrl}/users/{Uri.EscapeDataString(username)}/repos?limit=100&sort=updated"; + Console.WriteLine($"[API] Fetching repositories from: {url}"); + + try + { + using var response = await _httpClient.GetAsync(url, cancellationToken); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var repos = JsonSerializer.Deserialize>(json, JsonOptions); + if (repos != null && repos.Count > 0) + { + Console.WriteLine($"[API] Successfully retrieved {repos.Count} repositories."); + return repos; + } + } + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[API-WARN] Repositories request failed (Status: {response.StatusCode}). Switching to mock fallback."); + Console.ResetColor(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[API-WARN] Network error connecting to Gitea ({ex.Message}). Using realistic fallback data."); + Console.ResetColor(); + } + + return GenerateMockRepos(); + } + + private static List GenerateMockHeatmap() + { + var items = new List(); + var now = DateTimeOffset.UtcNow; + var random = new Random(42); // Fixed seed for reproducible fallback + + for (int i = 0; i < 365; i++) + { + var day = now.AddDays(-i); + // Simulate realistic developer habits (more activity on weekdays and active streaks) + bool isWeekend = day.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; + int chance = isWeekend ? 40 : 75; + + if (random.Next(100) < chance) + { + int commits = random.Next(1, 14); + items.Add(new HeatmapItem + { + Timestamp = day.ToUnixTimeSeconds(), + Contributions = commits + }); + } + } + + return items; + } + + private static List GenerateMockRepos() + { + var now = DateTime.UtcNow; + return + [ + new GiteaRepo + { + Name = "Markdown-Editor-Pro", + FullName = "NickiCloud/Markdown-Editor-Pro", + Description = "Moderner, performanter Markdown-Editor mit Live-Vorschau, Syntax-Highlighting & LaTeX-Support auf Avalonia UI.", + HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Markdown-Editor-Pro", + Language = "C#", + StarsCount = 7, + ForksCount = 1, + UpdatedAt = now.AddHours(-3) + }, + new GiteaRepo + { + Name = "CloudDrop", + FullName = "NickiCloud/CloudDrop", + Description = "Leichtgewichtiger, selbstgehosteter File-Sharing-Dienst mit temporären Download-Links und verschlüsselter Ablage.", + HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/CloudDrop", + Language = "C#", + StarsCount = 5, + ForksCount = 0, + UpdatedAt = now.AddDays(-2) + }, + new GiteaRepo + { + Name = "KasseApp", + FullName = "NickiCloud/KasseApp", + Description = "Intuitive Kassen- und Abrechnungssoftware für Vereine und Events mit Offline-First SQLite-Datenbank.", + HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/KasseApp", + Language = "C#", + StarsCount = 4, + ForksCount = 0, + UpdatedAt = now.AddDays(-5) + }, + new GiteaRepo + { + Name = "DrinkReminder", + FullName = "NickiCloud/DrinkReminder", + Description = "Smarter Tray-Assistent für Desktop & Mobile zur Erinnerung an regelmäßige Flüssigkeitszufuhr.", + HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/DrinkReminder", + Language = "C#", + StarsCount = 3, + ForksCount = 0, + UpdatedAt = now.AddDays(-12) + }, + new GiteaRepo + { + Name = "Selfhosted-Infra", + FullName = "NickiCloud/Selfhosted-Infra", + Description = "Docker-Compose & Konfigurationen für Debian, Gitea, Mailcow, Nginx Reverse Proxy, UFW & Fail2ban.", + HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/Selfhosted-Infra", + Language = "Shell", + StarsCount = 6, + ForksCount = 0, + UpdatedAt = now.AddDays(-18) + }, + new GiteaRepo + { + Name = "WebCore-Portal", + FullName = "NickiCloud/WebCore-Portal", + Description = "Interaktives Service-Portal mit MudBlazor Komponenten und JWT-Authentifizierung.", + HtmlUrl = "https://git.webcore-consulting.dev/NickiCloud/WebCore-Portal", + Language = "HTML", + StarsCount = 2, + ForksCount = 0, + UpdatedAt = now.AddDays(-25) + } + ]; + } +} diff --git a/src/ProfileManager/Services/ReadmeInjector.cs b/src/ProfileManager/Services/ReadmeInjector.cs new file mode 100644 index 0000000..f35a092 --- /dev/null +++ b/src/ProfileManager/Services/ReadmeInjector.cs @@ -0,0 +1,70 @@ +using System.Text; +using System.Text.RegularExpressions; +using ProfileManager.Models; + +namespace ProfileManager.Services; + +public static class ReadmeInjector +{ + private const string StartMarker = ""; + private const string EndMarker = ""; + + public static bool InjectLatestRepos(string readmePath, List repos) + { + if (!File.Exists(readmePath)) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[README-WARN] File not found: {readmePath}"); + Console.ResetColor(); + return false; + } + + var content = File.ReadAllText(readmePath, Encoding.UTF8); + int startIndex = content.IndexOf(StartMarker, StringComparison.Ordinal); + int endIndex = content.IndexOf(EndMarker, StringComparison.Ordinal); + + if (startIndex == -1 || endIndex == -1 || endIndex < startIndex) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[README-WARN] Markers '{StartMarker}' and '{EndMarker}' not found in {readmePath}"); + Console.ResetColor(); + return false; + } + + var replacement = new StringBuilder(); + replacement.AppendLine(StartMarker); + + if (repos.Count == 0) + { + replacement.AppendLine("*Momentan keine öffentlichen Repositories verfügbar.*"); + } + else + { + foreach (var repo in repos) + { + var desc = string.IsNullOrWhiteSpace(repo.Description) + ? (repo.Name.Equals(".profile", StringComparison.OrdinalIgnoreCase) + ? "🦊 Gitea-Profil & dynamische Aktivitäts-Statistiken mit .NET 10" + : "Persönliches Entwicklungsprojekt.") + : repo.Description.Trim(); + + var lang = string.IsNullOrWhiteSpace(repo.Language) + ? (repo.Name.Equals(".profile", StringComparison.OrdinalIgnoreCase) ? "C# / Markdown" : "Code") + : repo.Language; + var stars = repo.StarsCount > 0 ? $" • ⭐ {repo.StarsCount}" : string.Empty; + var updated = repo.UpdatedAt.ToString("dd.MM.yyyy"); + + replacement.AppendLine($"- 🚀 [**{repo.Name}**]({repo.HtmlUrl}) – {desc} "); + replacement.AppendLine($" `{lang}`{stars} • 🕒 *Aktualisiert am {updated}*"); + } + } + + replacement.Append(EndMarker); + + var newContent = content.Substring(0, startIndex) + replacement + content.Substring(endIndex + EndMarker.Length); + + File.WriteAllText(readmePath, newContent, Encoding.UTF8); + Console.WriteLine($"[README] Successfully injected {repos.Count} repositories into {readmePath}"); + return true; + } +} diff --git a/src/ProfileManager/Services/StatsCalculator.cs b/src/ProfileManager/Services/StatsCalculator.cs new file mode 100644 index 0000000..1bade9d --- /dev/null +++ b/src/ProfileManager/Services/StatsCalculator.cs @@ -0,0 +1,180 @@ +using ProfileManager.Models; + +namespace ProfileManager.Services; + +public static class StatsCalculator +{ + private static readonly Dictionary LanguageColors = new(StringComparer.OrdinalIgnoreCase) + { + { "C#", "#a855f7" }, + { "C++", "#f34b7d" }, + { "C", "#555555" }, + { "HTML", "#e34c26" }, + { "CSS", "#563d7c" }, + { "JavaScript", "#f1e05a" }, + { "TypeScript", "#3178c6" }, + { "Python", "#3572A5" }, + { "Shell", "#89e051" }, + { "Bash", "#89e051" }, + { "Dockerfile", "#384d54" }, + { "Go", "#00ADD8" }, + { "Rust", "#dea584" }, + { "Vue", "#41b883" }, + { "PHP", "#4F5D95" }, + { "Kotlin", "#A97BFF" }, + { "Java", "#b07219" }, + { "Dart", "#00B4AB" } + }; + + public static ProfileStats Calculate(List heatmapItems, List repos, DateOnly? referenceDate = null) + { + var today = referenceDate ?? DateOnly.FromDateTime(DateTime.UtcNow); + + // 1. Group heatmap contributions by DateOnly (UTC) + var dateMap = new Dictionary(); + foreach (var item in heatmapItems) + { + var date = DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(item.Timestamp).UtcDateTime); + dateMap[date] = dateMap.GetValueOrDefault(date, 0) + Math.Max(0, item.Contributions); + } + + // 2. Build 52x7 Grid ending on Sunday of current week (Monday-based rows 0..6) + int dayOffsetFromMonday = ((int)today.DayOfWeek + 6) % 7; + var gridEndSunday = today.AddDays(6 - dayOffsetFromMonday); + var gridStartMonday = gridEndSunday.AddDays(-(52 * 7 - 1)); + + var days = new List(52 * 7); + for (int col = 0; col < 52; col++) + { + for (int row = 0; row < 7; row++) + { + var currentDate = gridStartMonday.AddDays(col * 7 + row); + int count = currentDate > today ? 0 : dateMap.GetValueOrDefault(currentDate, 0); + int level = count switch + { + 0 => 0, + >= 1 and <= 3 => 1, + >= 4 and <= 6 => 2, + >= 7 and <= 9 => 3, + _ => 4 + }; + + days.Add(new DayContribution + { + Date = currentDate, + Count = count, + Level = level, + Column = col, + Row = row + }); + } + } + + // 3. Total contributions (last 365 days) + var oneYearAgo = today.AddDays(-365); + int total365 = dateMap + .Where(kv => kv.Key >= oneYearAgo && kv.Key <= today) + .Sum(kv => kv.Value); + + // 4. Streaks calculation + int currentStreak = 0; + if (dateMap.GetValueOrDefault(today, 0) > 0) + { + currentStreak = 1; + var check = today.AddDays(-1); + while (dateMap.GetValueOrDefault(check, 0) > 0) + { + currentStreak++; + check = check.AddDays(-1); + } + } + else + { + // If today has no commit yet, yesterday's streak is still active + var check = today.AddDays(-1); + while (dateMap.GetValueOrDefault(check, 0) > 0) + { + currentStreak++; + check = check.AddDays(-1); + } + } + + // Longest Streak across all dates + int longestStreak = 0; + if (dateMap.Count > 0) + { + var minDate = dateMap.Keys.Min(); + if (minDate > gridStartMonday) minDate = gridStartMonday; + + int currentRun = 0; + for (var d = minDate; d <= today; d = d.AddDays(1)) + { + if (dateMap.GetValueOrDefault(d, 0) > 0) + { + currentRun++; + if (currentRun > longestStreak) longestStreak = currentRun; + } + else + { + currentRun = 0; + } + } + } + + // 5. Top 3 Repos (sorted by UpdatedAt desc, exclude forks) + var nonForkRepos = repos.Where(r => !r.Fork).ToList(); + var candidateRepos = nonForkRepos.Count > 0 ? nonForkRepos : repos; + + var latestRepos = candidateRepos + .OrderByDescending(r => r.UpdatedAt) + .Take(3) + .ToList(); + + // 6. Top Languages calculation + var languageGroups = candidateRepos + .Where(r => !string.IsNullOrWhiteSpace(r.Language)) + .GroupBy(r => r.Language!.Trim()) + .Select(g => new { Name = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .ToList(); + + int totalLanguageRepos = languageGroups.Sum(x => x.Count); + var topLanguages = new List(); + + if (totalLanguageRepos > 0) + { + foreach (var lang in languageGroups.Take(5)) + { + double pct = Math.Round((double)lang.Count / totalLanguageRepos * 100.0, 1); + topLanguages.Add(new LanguageStat + { + Name = lang.Name, + Percentage = pct, + RepoCount = lang.Count, + Color = LanguageColors.GetValueOrDefault(lang.Name, "#c084fc") + }); + } + } + else + { + // Default language representation if repos have no language tags + topLanguages.Add(new LanguageStat + { + Name = "C#", + Percentage = 100.0, + RepoCount = 1, + Color = "#a855f7" + }); + } + + return new ProfileStats + { + TotalContributions365 = total365, + CurrentStreak = currentStreak, + LongestStreak = Math.Max(longestStreak, currentStreak), + Days = days, + TopLanguages = topLanguages, + LatestRepos = latestRepos + }; + } +} diff --git a/src/ProfileManager/Services/SvgGenerator.cs b/src/ProfileManager/Services/SvgGenerator.cs new file mode 100644 index 0000000..154ceeb --- /dev/null +++ b/src/ProfileManager/Services/SvgGenerator.cs @@ -0,0 +1,213 @@ +using System.Globalization; +using System.Text; +using ProfileManager.Models; + +namespace ProfileManager.Services; + +public static class SvgGenerator +{ + private static readonly string[] MonthNames = + [ + "Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez" + ]; + + public static string Generate(ProfileStats stats) + { + var sb = new StringBuilder(16384); + + const int svgWidth = 740; + const int svgHeight = 265; + const int gridStartX = 34; + const int gridStartY = 64; + const int cellSize = 10; + const int cellGap = 3; + const int colStep = cellSize + cellGap; // 13px + + sb.AppendLine($""""""); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine($" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + + // Styles & Media Queries + sb.AppendLine(""" + + """); + + // Background Panel + sb.AppendLine($" "); + + // Header: Title & Total Commits Badge + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" 🦊 NickiCloud Gitea Activity"); + + // Header badge (right aligned) + string badgeText = $"{stats.TotalContributions365:N0} Commits im letzten Jahr"; + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" {badgeText}"); + sb.AppendLine(" "); + + // Month Labels + sb.AppendLine(" "); + int lastMonth = -1; + for (int col = 0; col < 52; col++) + { + var firstDayOfCol = stats.Days.FirstOrDefault(d => d.Column == col && d.Row == 0); + if (firstDayOfCol != null && firstDayOfCol.Date.Month != lastMonth) + { + lastMonth = firstDayOfCol.Date.Month; + // Avoid rendering month if too close to right edge + if (col < 50) + { + int x = gridStartX + col * colStep; + sb.AppendLine($" {MonthNames[lastMonth - 1]}"); + } + } + } + + // Day of Week Labels (Mo, Mi, Fr) + sb.AppendLine(" "); + sb.AppendLine($" Mo"); + sb.AppendLine($" Mi"); + sb.AppendLine($" Fr"); + + // Heatmap Cells (52 cols x 7 rows) + sb.AppendLine(" "); + sb.AppendLine(" "); + foreach (var day in stats.Days) + { + int x = gridStartX + day.Column * colStep; + int y = gridStartY + day.Row * colStep; + int delayMs = day.Column * 12; + + string tooltip = $"{day.Date:dd.MM.yyyy}: {day.Count} Commits"; + sb.AppendLine($" "); + sb.AppendLine($" {tooltip}"); + sb.AppendLine(" "); + } + sb.AppendLine(" "); + + // Stats Row: 3 metric cards + sb.AppendLine(" "); + int cardY = 166; + int cardHeight = 36; + int cardWidth = 216; + int gap = 14; + + // Card 1: Total + RenderMetricCard(sb, gridStartX, cardY, cardWidth, cardHeight, "📦", $"{stats.TotalContributions365:N0}", "Jahres-Beiträge"); + + // Card 2: Current Streak + RenderMetricCard(sb, gridStartX + cardWidth + gap, cardY, cardWidth, cardHeight, "🔥", $"{stats.CurrentStreak} {(stats.CurrentStreak == 1 ? "Tag" : "Tage")}", "Aktueller Streak"); + + // Card 3: Longest Streak + RenderMetricCard(sb, gridStartX + (cardWidth + gap) * 2, cardY, cardWidth, cardHeight, "⚡", $"{stats.LongestStreak} {(stats.LongestStreak == 1 ? "Tag" : "Tage")}", "Längster Streak"); + + // Top Languages Bar (width = 676px) + sb.AppendLine(" "); + sb.AppendLine(" "); + double currentX = gridStartX; + const double totalBarWidth = 676.0; + + foreach (var lang in stats.TopLanguages) + { + double w = Math.Max(2.0, (lang.Percentage / 100.0) * totalBarWidth); + sb.AppendLine($" "); + sb.AppendLine($" {lang.Name}: {lang.Percentage:F1}%"); + sb.AppendLine(" "); + currentX += w; + } + sb.AppendLine(" "); + + // Language Legend + sb.AppendLine(" "); + sb.AppendLine(" "); + int legendX = 0; + foreach (var lang in stats.TopLanguages) + { + sb.AppendLine($" "); + string text = $"{lang.Name} {lang.Percentage:F1}%"; + sb.AppendLine($" {text}"); + legendX += 13 + (text.Length * 7) + 16; + } + sb.AppendLine(" "); + + sb.AppendLine(""); + return sb.ToString(); + } + + private static void RenderMetricCard(StringBuilder sb, int x, int y, int w, int h, string icon, string value, string label) + { + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" {icon}"); + sb.AppendLine($" {value}"); + sb.AppendLine($" {label}"); + sb.AppendLine(" "); + } +}