From ee92c212a0140c6697f9106cd68a41d794e340b0 Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@suma-ev.de>
Date: Thu, 18 Jul 2024 16:32:56 +0200
Subject: [PATCH] include advertisements

---
 .../Http/Controllers/StartpageController.php  |  28 +---
 .../app/Http/Controllers/TilesController.php  | 130 ++++++++++++++++++
 metager/app/Models/Tile.php                   |  41 ++++++
 metager/config/metager/taketiles.php          |   8 ++
 metager/resources/js/startpage/app.js         |   6 +-
 metager/resources/js/startpage/tiles.js       |  73 ++++++++++
 .../less/metager/pages/startpage/tiles.less   |  53 +++++--
 metager/resources/views/index.blade.php       |   7 +-
 .../views/layouts/staticPages.blade.php       |   3 +
 metager/resources/views/parts/tile.blade.php  |   6 +-
 metager/routes/web.php                        |   3 +
 11 files changed, 317 insertions(+), 41 deletions(-)
 create mode 100644 metager/app/Http/Controllers/TilesController.php
 create mode 100644 metager/app/Models/Tile.php
 create mode 100644 metager/config/metager/taketiles.php
 create mode 100644 metager/resources/js/startpage/tiles.js

diff --git a/metager/app/Http/Controllers/StartpageController.php b/metager/app/Http/Controllers/StartpageController.php
index b29496598..8a5b1bdd3 100644
--- a/metager/app/Http/Controllers/StartpageController.php
+++ b/metager/app/Http/Controllers/StartpageController.php
@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use Cache;
 use Illuminate\Http\Request;
 use Jenssegers\Agent\Agent;
 use Response;
@@ -28,32 +29,17 @@ class StartpageController extends Controller
             return redirect(route("resultpage", ["eingabe" => $eingabe]));
         }
 
-        $optionParams = ['param_sprueche', 'param_newtab', 'param_maps', 'param_autocomplete', 'param_lang', 'param_key'];
-        $option_values = [];
-
-        foreach ($optionParams as $param) {
-            $value = $request->input($param);
-            if ($value) {
-                $option_values[$param] = $value;
-            }
-        }
-
-        $autocomplete = 'on';
-        if (in_array('autocomplete', array_keys($option_values))) {
-            $autocomplete = $option_values['autocomplete'];
-        }
+        $ckey = hash_hmac("sha256", $request->ip() . now()->format("Y-m-d"), config("metager.taketiles.secret"));
+        Cache::put($ckey, "1", now()->addSeconds(TilesController::CACHE_DURATION_SECONDS));
+        $tiles = TilesController::TILES($ckey);
+        $tiles_update_url = route('tiles', ["ckey" => $ckey]);
 
         return view('index')
             ->with('title', trans('titles.index'))
-            ->with('homeIcon')
-            ->with('agent', new Agent())
-            ->with('navbarFocus', 'suche')
             ->with('focus', $request->input('focus', 'web'))
-            ->with('time', $request->input('param_time', '1500'))
             ->with('request', $request->input('request', 'GET'))
-            ->with('option_values', $option_values)
-            ->with('autocomplete', $autocomplete)
-            ->with('pluginmodal', $request->input('plugin-modal', 'off'))
+            ->with('tiles_update_url', $tiles_update_url)
+            ->with('tiles', $tiles)
             ->with('css', [mix('css/themes/startpage/light.css')])
             ->with('js', [mix('js/startpage/app.js')])
             ->with('darkcss', [mix('css/themes/startpage/dark.css')]);
diff --git a/metager/app/Http/Controllers/TilesController.php b/metager/app/Http/Controllers/TilesController.php
new file mode 100644
index 000000000..16473dd8c
--- /dev/null
+++ b/metager/app/Http/Controllers/TilesController.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Localization;
+use App\Models\Authorization\Authorization;
+use Exception;
+use Illuminate\Support\Facades\Redis;
+use App\Models\Tile;
+use App\SearchSettings;
+use Cache;
+use Illuminate\Http\Request;
+use Log;
+
+class TilesController extends Controller
+{
+    const CACHE_DURATION_SECONDS = 300;
+
+    public function loadTakeTiles(Request $request)
+    {
+        if (!$request->filled("ckey") || !Cache::has($request->input("ckey"))) {
+            abort(404);
+        }
+        $ckey = $request->input("ckey");
+        $count = $request->input("count", 4);
+        $tiles = [];
+        $tiles = self::TAKE_TILES($ckey, $count);
+        return response()->json($tiles);
+    }
+
+    /**
+     * Generate Tiles for a given request
+     * This includes static TIles and SUMA Tiles
+     * Take Tiles are generated asynchroniously
+     * 
+     * @param string $ckey
+     * @return array
+     */
+    public static function TILES(string $ckey): array
+    {
+        $tiles = self::STATIC_TILES();
+        $tiles = array_merge($tiles, self::SUMA_TILES());
+        return $tiles;
+    }
+
+    /**
+     * Generates Static Tiles
+     * @return Tile[]
+     */
+    private static function STATIC_TILES(): array
+    {
+        $tiles = [];
+        $tiles[] = new Tile(title: "SUMA-EV", image: "/img/tiles/sumaev.png", url: "https://suma-ev.de", image_alt: "SUMA_EV Logo");
+        //$tiles[] = new Tile(title: "Maps", image: "/img/tiles/maps.png", url: "https://maps.metager.de", image_alt: "MetaGer Maps Logo");
+        $tiles[] = new Tile(title: __('sidebar.nav28'), image: "/img/icon-settings.svg", url: route("settings", ["focus" => app(SearchSettings::class)->fokus, "url" => url()->full()]), image_alt: "Settings Logo", image_classes: "invert-dm");
+        $tiles[] = new Tile(title: __('index.plugin'), image: "/img/svg-icons/plug-in.svg", url: route("plugin"), image_alt: "MetaGer Plugin Logo");
+        return $tiles;
+    }
+
+    /**
+     * Generates dynamic Tiles booked through SUMA-EV
+     * 
+     * @return array
+     */
+    private static function SUMA_TILES(): array
+    {
+        $tiles = [];
+        return $tiles;
+    }
+
+    /**
+     * Generates Tile ads from Takeads
+     * 
+     * @return array
+     */
+    private static function TAKE_TILES(string $ckey, int $count): array
+    {
+        $tiles = [];
+        $result_cache_key = "taketiles:fetch:$ckey:$count";
+
+        $result = Cache::get($result_cache_key);
+        if ($result === null) {
+            $supported_countries = ["US", "GB", "DE", "AT", "CH", "TR"];
+            if (!config("metager.taketiles.enabled") || !in_array(Localization::getRegion(), $supported_countries)) {
+                return $tiles;
+            }
+            if (app(Authorization::class)->canDoAuthenticatedSearch(false))
+                return $tiles;
+
+            $endpoint = config("metager.taketiles.endpoint");
+            $params = [
+                "count" => $count,
+                "deviceId" => $ckey,
+                "countryCode" => Localization::getLanguage()
+            ];
+            $mission = [
+                "resulthash" => $result_cache_key,
+                "url" => $endpoint . "?" . http_build_query($params),
+                "useragent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0",
+                "headers" => [
+                    "Content-Type" => "application/json",
+                    "Authorization" => "Bearer " . config("metager.taketiles.public_key"),
+                ],
+                "cacheDuration" => ceil(self::CACHE_DURATION_SECONDS / 60),
+                "name" => "Take Tiles",
+            ];
+            $mission = json_encode($mission);
+            Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission);
+            Cache::put($ckey, "1", now()->addSeconds(self::CACHE_DURATION_SECONDS));
+            $result = Redis::blpop($result_cache_key, 0);
+            if (sizeof($result) === 2) {
+                $result = $result[1];
+            }
+        }
+
+        if ($result !== null) {
+            try {
+                $result = json_decode($result);
+                foreach ($result->data as $result_tile) {
+                    $tiles[] = new Tile(title: $result_tile->title, image: $result_tile->image, image_alt: $result_tile->title . " Image", url: $result_tile->url, classes: "advertisement");
+                }
+            } catch (Exception $e) {
+                Log::error($e);
+            }
+
+        }
+
+        return $tiles;
+    }
+}
diff --git a/metager/app/Models/Tile.php b/metager/app/Models/Tile.php
new file mode 100644
index 000000000..73e1de114
--- /dev/null
+++ b/metager/app/Models/Tile.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Models;
+
+/**
+ * Model to hold data which represents a tile as visible on the startpage
+ */
+class Tile implements \JsonSerializable
+{
+    public string $title;
+    public string $image;
+    public string $image_alt;
+    public string $url;
+    public string $classes = "";
+    public string $image_classes = "";
+    /**
+     * 
+     * @param string $title Title to show for the Tile
+     * @param string $image URL to a image to show for the Tile
+     * @param string $image_alt Alt Text to show for the image
+     * @param string $url URL to link to when the user clicks on the Tile
+     * @param string $classes Additional css classes to append for the tile
+     * @param string $image_classes Additional css classes to append to the image of the tile
+     */
+    public function __construct(string $title, string $image, string $image_alt, string $url, string $classes = "", string $image_classes = "")
+    {
+        $this->title = $title;
+        $this->image = $image;
+        $this->image_alt = $image_alt;
+        $this->url = $url;
+        $this->classes = $classes;
+        $this->image_classes = $image_classes;
+    }
+
+    public function jsonSerialize()
+    {
+        $json = get_object_vars($this);
+        $json["html"] = view("parts.tile", ["tile" => $this])->render();
+        return $json;
+    }
+}
\ No newline at end of file
diff --git a/metager/config/metager/taketiles.php b/metager/config/metager/taketiles.php
new file mode 100644
index 000000000..827872660
--- /dev/null
+++ b/metager/config/metager/taketiles.php
@@ -0,0 +1,8 @@
+<?php
+return [
+    "enabled" => env("TAKE_TILES_ENABLED", false),
+    "secret" => env("TAKE_TILES_SECRET", "12345"),
+    "endpoint" => env("TAKE_TILES_ENDPOINT", ""),
+    "public_key" => env("TAKE_TILES_PUBLIC_KEY", ""),
+
+];
\ No newline at end of file
diff --git a/metager/resources/js/startpage/app.js b/metager/resources/js/startpage/app.js
index 9241a27bb..a8845d071 100644
--- a/metager/resources/js/startpage/app.js
+++ b/metager/resources/js/startpage/app.js
@@ -1,5 +1,7 @@
+import "./tiles";
+
 // Register Keyboard listener for quicklinks on startpage
-(() => {
+(async () => {
   let sidebar_toggle = document.querySelector("#sidebarToggle");
   let skip_links_container = document.querySelector(".skiplinks");
 
@@ -28,4 +30,4 @@
     },
     { once: true }
   );
-})();
+})();
\ No newline at end of file
diff --git a/metager/resources/js/startpage/tiles.js b/metager/resources/js/startpage/tiles.js
new file mode 100644
index 000000000..71475b33f
--- /dev/null
+++ b/metager/resources/js/startpage/tiles.js
@@ -0,0 +1,73 @@
+(async () => {
+    let tile_container = document.querySelector("#tiles");
+    let tile_count = tile_container.querySelectorAll("a").length;
+
+    let advertisements = [];
+    fetchAdvertisements().then(() => udpateInterface());
+
+    async function fetchAdvertisements() {
+        let desired_tile_count = calculateDesiredTileCount();
+        let regular_tile_count = getRegularTileCount();
+        if (advertisements.length >= desired_tile_count - regular_tile_count) return;
+        let update_url = document.querySelector("meta[name=tiles-update-url]").content;
+        update_url += "&count=" + (desired_tile_count - tile_count);
+
+        return fetch(update_url).then(response => response.json()).then(response => {
+            advertisements = response;
+        });
+    }
+    function udpateInterface() {
+        let desired_tile_count = calculateDesiredTileCount();
+        let regular_tile_count = getRegularTileCount();
+
+        if (document.querySelectorAll("#tiles > a").length == desired_tile_count) return;
+        document.querySelectorAll("#tiles >a.advertisement").forEach(element => {
+            console.log("remove");
+            element.remove();
+        });
+        for (let i = 0; i < desired_tile_count - regular_tile_count; i++) {
+            if (advertisements.length < i + 1) continue;
+            let container = document.createElement("div");
+            container.innerHTML = advertisements[i].html;
+
+            tile_container.appendChild(container.firstChild);
+        }
+    }
+    function getRegularTileCount() {
+        return tile_container.querySelectorAll("a:not(.advertisement)").length;
+    }
+    function calculateDesiredTileCount() {
+        let max_tiles = 8;
+        let native_tile_count = getRegularTileCount();
+        let min_advertisements = 2;
+
+        let tile_width = parseFloat(window.getComputedStyle(document.querySelector("#tiles > a")).width.replace("px", ""));
+        let tile_gap = parseFloat(window.getComputedStyle(document.querySelector("#tiles"))["column-gap"].replace("px", ""));
+        let client_width = document.querySelector("html").clientWidth;
+
+        let desired_tile_count = 8;
+        if (client_width > 9 * tile_width + 8 * tile_gap) {
+            // Largest Screen Size => Up to 8 Tiles in one row
+            desired_tile_count = max_tiles;
+        } else if (client_width > 8 * tile_width + 7 * tile_gap) {
+            // Large Screen => Up to 7 Tiles in one row => Just Fill up
+            desired_tile_count = 7;
+        } else if (client_width > 6 * tile_width + 5 * tile_gap) {
+            desired_tile_count = 5;
+        } else if (client_width > 4 * tile_width + 3 * tile_gap) {
+            desired_tile_count = 3;
+        } else {
+            desired_tile_count = 2;
+        }
+
+        if (native_tile_count + min_advertisements > desired_tile_count) {
+            desired_tile_count *= 2;
+        }
+
+        return desired_tile_count;
+    }
+    window.addEventListener("resize", e => {
+        fetchAdvertisements().then(() => udpateInterface());
+    })
+})();
+
diff --git a/metager/resources/less/metager/pages/startpage/tiles.less b/metager/resources/less/metager/pages/startpage/tiles.less
index f9473ad27..92245173d 100644
--- a/metager/resources/less/metager/pages/startpage/tiles.less
+++ b/metager/resources/less/metager/pages/startpage/tiles.less
@@ -3,18 +3,48 @@ div#tiles-container {
     flex-grow: 1;
     padding-inline: 1rem;
 
+
     div#tiles {
         flex-grow: 0;
+        width: 100%;
+        @tile_width: 13ch;
+
+        @nine_row_width: calc(calc(9 * @tile_width) + calc(8 * 1rem));
+        @eight_row_width: calc(calc(8 * @tile_width) + calc(7 * 1rem));
+        @seven_row_width: calc(calc(7 * @tile_width) + calc(6 * 1rem));
+        @six_row_width: calc(calc(6 * @tile_width) + calc(5 * 1rem));
+        @five_row_width: calc(calc(5 * @tile_width) + calc(4 * 1rem));
+        @four_row_width: calc(calc(4 * @tile_width) + calc(3 * 1rem));
+        @three_row_width: calc(calc(3 * @tile_width) + calc(2 * 1rem));
+        @two_row_width: calc(calc(2 * @tile_width) + calc(1 * 1rem));
+
+        @media(max-width: @nine_row_width) {
+            width: @seven_row_width;
+        }
+
+        @media(max-width: @eight_row_width) {
+            width: @five_row_width;
+        }
+
+        @media(max-width: @six_row_width) {
+            width: @three_row_width;
+        }
+
+        @media(max-width: @four_row_width) {
+            width: @two_row_width;
+        }
+
         display: flex;
-        align-items: flex-start;
-        padding-top: 5dvh;
         flex-wrap: wrap;
+        justify-content: center;
+
+        padding-top: 8dvh;
         gap: 1rem;
         margin-inline: auto;
         height: max-content;
-        justify-content: space-between;
 
         >a {
+            width: @tile_width;
             display: flex;
             row-gap: .5rem;
             flex-direction: column;
@@ -22,14 +52,18 @@ div#tiles-container {
             justify-content: flex-end;
             color: @text-color;
 
+            &.skeleton {
+                display: none;
+            }
+
             >div.image {
-                @image_size: 4.5rem;
+                @image_size: 5rem;
                 width: @image_size;
                 height: @image_size;
                 padding: 1rem;
                 background-color: @highlight-color;
                 border-radius: 5px;
-                transition: width 0.5s, height 0.5s;
+                transition: background-color 0.5s;
 
                 >img {
                     width: 100%;
@@ -44,8 +78,8 @@ div#tiles-container {
             >div.title {
                 font-size: .8rem;
                 color: fade(@text-color, 40%);
-                transition: color 0.5s, width 0.5s, overflow 0.5s;
-                width: 11ch;
+                transition: color 0.5s;
+                width: @tile_width;
                 text-overflow: ellipsis;
                 overflow: hidden;
                 white-space: nowrap;
@@ -54,14 +88,11 @@ div#tiles-container {
 
             &:hover {
                 >div.image {
-                    width: 6rem;
-                    height: 6rem;
+                    background-color: lighten(@highlight-color, 10%);
                 }
 
                 >div.title {
                     color: @text-color;
-                    width: unset;
-                    overflow: unset;
                 }
             }
         }
diff --git a/metager/resources/views/index.blade.php b/metager/resources/views/index.blade.php
index 2a2294551..1f48b113a 100644
--- a/metager/resources/views/index.blade.php
+++ b/metager/resources/views/index.blade.php
@@ -39,10 +39,9 @@
   </div>
   <div id="tiles-container">
     <div id="tiles">
-      @include("parts.tile", ["url" => "https://suma-ev.de", "image" => "/img/tiles/sumaev.png", "image_alt" => "SUMA-EV Logo", "title" => "SUMA-EV"])
-      @include("parts.tile", ["url" => "https://maps.metager.de", "image" => "/img/tiles/maps.png", "image_alt" => "MetaGer Maps Logo", "title" => "Maps"])
-      @include("parts.tile", ["url" => route("settings", ["focus" => $focus, "url" => url()->full()]), "image" => "/img/icon-settings.svg", "image_alt" => "SUMA-EV Logo", "title" => __('sidebar.nav28'), "options" => ["img_class" => "invert-dm"]])
-      @include("parts.tile", ["url" => route("plugin"), "image" => "/img/svg-icons/plug-in.svg", "image_alt" => "MetaGer Plugin Logo", "title" => __("index.plugin")])
+      @foreach($tiles as $tile)
+      @include("parts.tile", ["tile" => $tile])
+    @endforeach
     </div>
   </div>
   <div id="language">
diff --git a/metager/resources/views/layouts/staticPages.blade.php b/metager/resources/views/layouts/staticPages.blade.php
index ba139a478..b35216574 100644
--- a/metager/resources/views/layouts/staticPages.blade.php
+++ b/metager/resources/views/layouts/staticPages.blade.php
@@ -12,6 +12,9 @@
 	<meta name="audience" content="all" />
 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
 	<meta name="statistics-enabled" content="{{ config("metager.matomo.enabled") }}">
+	@if(isset($tiles_update_url))
+	<meta name="tiles-update-url" content="{{ $tiles_update_url }}">
+	@endif
 	<link href="/favicon.ico" rel="icon" type="image/x-icon" />
 	<link href="/favicon.ico" rel="shortcut icon" type="image/x-icon" />
 	@foreach(LaravelLocalization::getSupportedLocales() as $locale => $locale_data)
diff --git a/metager/resources/views/parts/tile.blade.php b/metager/resources/views/parts/tile.blade.php
index ce94a9838..9d8bd6d47 100644
--- a/metager/resources/views/parts/tile.blade.php
+++ b/metager/resources/views/parts/tile.blade.php
@@ -1,6 +1,6 @@
-<a href="{{$url}}">
+<a href="{{$tile->url}}" class="{{ $tile->classes }}">
     <div class="image">
-        <img src="{{$image}}" alt="{{$image_alt}}" @if(isset($options) && array_key_exists("img_class",$options))class="{{$options["img_class"]}}"@endif>
+        <img src="{{$tile->image}}" alt="{{$tile->image_alt}}" class="{{$tile->image_classes}}">
     </div>
-    <div class="title">{{$title}}</div>
+    <div class="title">{{$tile->title}}</div>
 </a>
\ No newline at end of file
diff --git a/metager/routes/web.php b/metager/routes/web.php
index 6c6995dce..4ceb2e57e 100644
--- a/metager/routes/web.php
+++ b/metager/routes/web.php
@@ -15,6 +15,7 @@ use App\Http\Controllers\SitesearchController;
 use App\Http\Controllers\StartpageController;
 use App\Http\Controllers\StatisticsController;
 use App\Http\Controllers\SuggestionController;
+use App\Http\Controllers\TilesController;
 use App\Http\Controllers\TTSController;
 use App\Http\Controllers\ZitatController;
 use App\Localization;
@@ -341,6 +342,8 @@ Route::withoutMiddleware([\Illuminate\Foundation\Http\Middleware\ValidateCsrfTok
             ]);
     })->name("plugin");
 
+    Route::get('tiles', [TilesController::class, 'loadTakeTiles'])->name("tiles");
+
     Route::get('settings', function () {
         return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), '/'));
     });
-- 
GitLab