From dbe6d49c49992c33a3be74a92ddef25514751529 Mon Sep 17 00:00:00 2001 From: Gitea Date: Fri, 20 Jun 2025 19:48:09 +0530 Subject: [PATCH] added remote db source --- app/Models/Email.php | 29 +- app/Models/PremiumEmail.php | 7 +- app/Models/RemoteEmail.php | 24 + config/app.php | 1 + config/database.php | 20 + public/css/silktide-consent-manager.css | 569 ++++++++++++++++ public/js/silktide-consent-manager.js | 850 ++++++++++++++++++++++++ 7 files changed, 1495 insertions(+), 5 deletions(-) create mode 100644 app/Models/RemoteEmail.php create mode 100644 public/css/silktide-consent-manager.css create mode 100644 public/js/silktide-consent-manager.js diff --git a/app/Models/Email.php b/app/Models/Email.php index f3e439e..6fbbcda 100644 --- a/app/Models/Email.php +++ b/app/Models/Email.php @@ -272,7 +272,11 @@ class Email extends Model public static function parseEmail($email, $deleted = []): array { - $messages = self::fetchEmailFromDB($email); + if (config('app.fetch_from_remote_db')) { + $messages = RemoteEmail::fetchEmailFromDB($email); + } else { + $messages = self::fetchEmailFromDB($email); + } $limit = json_decode(config('app.settings.configuration_settings'))->fetch_messages_limit ?? 15; $count = 1; $response = [ @@ -282,9 +286,18 @@ class Email extends Model foreach ($messages as $message) { + // fix for null attachments + if ($message['attachments'] === null) { + $message['attachments'] = []; + } + if (in_array($message['message_id'], $deleted)) { // If it exists, delete the matching record from the 'emails' table - Email::where('message_id', $message['message_id'])->delete(); + if (config('app.fetch_from_remote_db')) { + RemoteEmail::where('message_id', $message['message_id'])->delete(); + } else { + Email::where('message_id', $message['message_id'])->delete(); + } continue; } @@ -331,7 +344,11 @@ class Email extends Model file_put_contents(storage_path('logs/zemail.csv'), request()->ip() . "," . date("Y-m-d h:i:s a") . "," . $obj['sender_email'] . "," . $email . PHP_EOL, FILE_APPEND); } } - Email::where('message_id', $message['message_id'])->update(['is_seen' => true]); + if (config('app.fetch_from_remote_db')) { + RemoteEmail::where('message_id', $message['message_id'])->update(['is_seen' => true]); + } else { + Email::where('message_id', $message['message_id'])->update(['is_seen' => true]); + } if (++$count > $limit) { break; } @@ -404,7 +421,11 @@ class Email extends Model public static function mailToDBStatus(): bool { - $latestRecord = self::orderBy('timestamp', 'desc')->first(); + if (config('app.fetch_from_remote_db')) { + $latestRecord = RemoteEmail::orderBy('timestamp', 'desc')->first(); + } else { + $latestRecord = self::orderBy('timestamp', 'desc')->first(); + } if (!$latestRecord) { return false; } diff --git a/app/Models/PremiumEmail.php b/app/Models/PremiumEmail.php index 4d2e74b..221d78d 100644 --- a/app/Models/PremiumEmail.php +++ b/app/Models/PremiumEmail.php @@ -7,6 +7,7 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Validator; +use function Laravel\Prompts\confirm; class PremiumEmail extends Model { @@ -44,7 +45,11 @@ class PremiumEmail extends Model public static function createEmail($message, $email): void { $initialData = $message; - $utcTime = CarbonImmutable::instance($message['timestamp'])->setTimezone('UTC')->toDateTimeString(); + if (config('app.fetch_from_db') && config('app.fetch_from_remote_db')) { + $utcTime = CarbonImmutable::parse($message['timestamp'])->setTimezone('UTC')->toDateTimeString(); + } else { + $utcTime = CarbonImmutable::instance($message['timestamp'])->setTimezone('UTC')->toDateTimeString(); + } $messageId = Carbon::parse($utcTime)->format('Ymd').$initialData['id']; $userId = \auth()->user()->id; $exists = PremiumEmail::where('user_id', $userId)->where('message_id', $messageId)->exists(); diff --git a/app/Models/RemoteEmail.php b/app/Models/RemoteEmail.php new file mode 100644 index 0000000..7b07900 --- /dev/null +++ b/app/Models/RemoteEmail.php @@ -0,0 +1,24 @@ + $email], [ + 'email' => 'required|email' + ]); + + if ($validator->fails()) { + return []; + } + return self::whereJsonContains('to', $email)->orderBy('timestamp', 'desc')->get(); + } +} diff --git a/config/app.php b/config/app.php index fe57419..6b767e9 100644 --- a/config/app.php +++ b/config/app.php @@ -31,6 +31,7 @@ return [ 'zemail_log' => env('ENABLE_ZEMAIL_LOGS', false), 'beta_feature' => env('APP_BETA_FEATURE', false), 'fetch_from_db' => env('FETCH_FETCH_FOR_DB', false), + 'fetch_from_remote_db' => env('FETCH_FROM_REMOTE_DB', false), 'force_db_mail' => env('FORCE_DB_MAIL', false), 'move_or_delete' => env('MOVE_OR_DELETE', null), 'auto_fetch_mail' => env('AUTO_FETCH_MAIL', false), diff --git a/config/database.php b/config/database.php index 8910562..c43794c 100644 --- a/config/database.php +++ b/config/database.php @@ -62,6 +62,26 @@ return [ ]) : [], ], + 'mysql_remote' => [ + 'driver' => 'mysql', + 'url' => env('REMOTE_DB_URL'), + 'host' => env('REMOTE_DB_HOST', '127.0.0.1'), + 'port' => env('REMOTE_DB_PORT', '3306'), + 'database' => env('REMOTE_DB_DATABASE', 'laravel'), + 'username' => env('REMOTE_DB_USERNAME', 'root'), + 'password' => env('REMOTE_DB_PASSWORD', ''), + 'unix_socket' => env('REMOTE_DB_SOCKET', ''), + 'charset' => env('REMOTE_DB_CHARSET', 'utf8mb4'), + 'collation' => env('REMOTE_DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('REMOTE_MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + 'mariadb' => [ 'driver' => 'mariadb', 'url' => env('DB_URL'), diff --git a/public/css/silktide-consent-manager.css b/public/css/silktide-consent-manager.css new file mode 100644 index 0000000..2f014e7 --- /dev/null +++ b/public/css/silktide-consent-manager.css @@ -0,0 +1,569 @@ +/* + Silktide Consent Manager - https://silktide.com/consent-manager/ + + Styles are at risked of being overridden by styles coming from the site the consent manager is used on. + To help prevent this, global wrapper elements are prefixed with "#silktide-" +*/ + +/* -------------------------------- + Global Styles - These elements exist in the main DOM and styling is limited to positioning and animation +-------------------------------- */ +/* Wrapper (Global) */ +#silktide-wrapper { + /* Global */ + --focus: 0 0 0 2px #ffffff, 0 0 0 4px #000000, 0 0 0 6px #ffffff; + --boxShadow: -5px 5px 10px 0px #00000012, 0px 0px 50px 0px #0000001a; + --fontFamily: "Helvetica Neue", "Segoe UI", Arial, sans-serif; + /* Color Scheme */ + --primaryColor: #533BE2; + --backgroundColor: #FFFFFF; + --textColor: #253B48; + /* Backdrop */ + --backdropBackgroundColor: #00000033; + --backdropBackgroundBlur: 0px; + /* Cookie Icon */ + --cookieIconColor: #533BE2; + --cookieIconBackgroundColor: #FFFFFF; + position: fixed; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + z-index: 99999; + pointer-events: none; + border: 0px; + display: flex; + justify-content: center; + align-items: center; +} + +/* Backdrop (Global) */ +#silktide-backdrop-global { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: auto; + border: 0px; + display: none; +} + +/* -------------------------------- + Links +-------------------------------- */ +#silktide-wrapper a { + all: unset; + display: inline-block; + color: var(--primaryColor); + text-decoration: underline; +} + +#silktide-wrapper a:hover { + cursor: pointer; + color: var(--textColor); +} + +/* -------------------------------- + Focus Styles +-------------------------------- */ +#silktide-wrapper a:focus, +#silktide-wrapper #silktide-banner button:focus, +#silktide-wrapper #silktide-modal button:focus, +#silktide-wrapper #silktide-cookie-icon:focus { + outline: none; + box-shadow: var(--focus); + border-radius: 5px; +} + +#silktide-wrapper #silktide-cookie-icon:focus { + border-radius: 50%; +} + +/* -------------------------------- + General Styles +-------------------------------- */ + +#silktide-wrapper .st-button { + color: var(--backgroundColor); + background-color: var(--primaryColor); + border: 2px solid var(--primaryColor); + padding: 10px 20px; + text-decoration: none; + text-align: center; + display: inline-block; + font-size: 16px; + line-height: 24px; + cursor: pointer; + border-radius: 5px; +} + +#silktide-wrapper .st-button--primary { +} + +#silktide-wrapper .st-button--primary:hover { + background-color: var(--backgroundColor); + color: var(--primaryColor); +} + +#silktide-wrapper .st-button--secondary { + background-color: var(--backgroundColor); + color: var(--primaryColor); +} + +#silktide-wrapper .st-button--secondary:hover { + background-color: var(--primaryColor); + color: var(--backgroundColor); +} + +/* -------------------------------- + Banner +-------------------------------- */ +#silktide-banner { + font-family: var(--fontFamily); + color: var(--textColor); + background-color: var(--backgroundColor); + box-sizing: border-box; + padding: 32px; + border-radius: 5px; + pointer-events: auto; + border: 0px; + position: fixed; + bottom: 16px; + right: 16px; + width: 600px; + max-width: calc(100% - 32px); + transform: translate(0, -20px); + opacity: 0; + animation: silktide-slideInDown 350ms ease-out forwards; + animation-delay: 0.3s; + box-shadow: -5px 5px 10px 0px #00000012, 0px 0px 50px 0px #0000001a; +} + +#silktide-banner:focus { + border-radius: 50%; +} + +#silktide-banner.center { + top: 50%; + left: 50%; + bottom: auto; + right: auto; + position: fixed; + transform: translate(-50%, calc(-50% - 20px)); + animation: silktide-slideInDown-center 350ms ease-out forwards; +} + +#silktide-banner.bottomLeft { + bottom: 16px; + left: 16px; + position: fixed; +} + +#silktide-banner.bottomCenter { + bottom: 16px; + left: 50%; + position: fixed; + transform: translate(-50%, -20px); + animation: silktide-slideInDown-bottomCenter 350ms ease-out forwards; +} + +#silktide-banner .preferences { + display: flex; + gap: 5px; + border: none; + padding: 0px; + background-color: transparent; + color: var(--primaryColor); + cursor: pointer; + font-size: 16px; +} + +#silktide-banner .preferences span { + display: block; + white-space: nowrap; + text-decoration: underline; +} + +#silktide-banner .preferences span:hover { + color: var(--textColor); +} + +#silktide-banner .preferences:after { + display: block; + content: '>'; + text-decoration: none; +} + +#silktide-banner p { + font-size: 16px; + line-height: 24px; + margin: 0px 0px 15px; +} + +#silktide-banner a { + display: inline-block; + color: var(--primaryColor); + text-decoration: underline; + background-color: var(--backgroundColor); +} + +#silktide-banner a:hover { + color: var(--textColor); +} + +#silktide-banner a.silktide-logo { + display: block; + fill: var(--primaryColor); /* passed down to svg > path */ + margin-left: auto; + width: 24px; + height: 24px; +} + + +#silktide-banner .actions { + display: flex; + gap: 16px; + flex-direction: column; + margin-top: 24px; +} + +@media (min-width: 600px) { + #silktide-banner .actions { + flex-direction: row; + align-items: center; + } +} + +#silktide-banner .actions-row { + display: flex; + gap: 16px; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-grow: 1; +} + +/* -------------------------------- + Modal +-------------------------------- */ +#silktide-modal { + display: none; + pointer-events: auto; + width: 800px; + max-width: 100%; + max-height: 100%; + border: 0px; + transform: translate(0px, -20px); + opacity: 0; + animation: silktide-slideInUp-center 350ms ease-out forwards; + box-shadow: -5px 5px 10px 0px #00000012, 0px 0px 50px 0px #0000001a; + font-family: var(--fontFamily); + color: var(--textColor); + flex-direction: column; + padding: 30px; + background-color: var(--backgroundColor); + border-radius: 5px; + box-sizing: border-box; +} + +/* -------------------------------- + Modal - Header +-------------------------------- */ +#silktide-modal header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + gap: 16px; +} + +#silktide-modal h1 { + font-family: var(--fontFamily); + color: var(--textColor); + font-size: 24px; + font-weight: 500; + margin: 0px; +} + +#silktide-modal .modal-close { + display: inline-flex; + border: none; + padding: 10px; + border: 0px; + cursor: pointer; + background: var(--backgroundColor); + color: var(--primaryColor); +} + +#silktide-modal .modal-close svg { + fill: var(--primaryColor); +} + +/* -------------------------------- + Modal - Content +-------------------------------- */ + +#silktide-modal section { + flex: 1; + overflow-y: auto; + margin-top: 32px; + padding-right: 7px; /* Prevents scrollbar from appearing over the switches */ +} + +#silktide-modal section::-webkit-scrollbar { + display: block; /* Force scrollbars to show */ + width: 5px; /* Width of the scrollbar */ +} + +#silktide-modal section::-webkit-scrollbar-thumb { + background-color: var(--textColor); /* Color of the scrollbar thumb */ + border-radius: 10px; /* Rounded corners for the thumb */ +} + +#silktide-modal p { + font-size: 16px; + line-height: 24px; + color: var(--textColor); + margin: 0px 0px 15px; +} + +#silktide-modal p:last-of-type { + margin: 0px; +} + +#silktide-modal fieldset { + padding: 0px; + border: none; + margin: 0px 0px 32px; +} + +#silktide-modal fieldset:last-of-type { + margin: 0px; +} + +#silktide-modal legend { + padding: 0px; + margin: 0px 0px 10px; + font-weight: 700; + color: var(--textColor); + font-size: 16px; +} + +#silktide-modal .cookie-type-content { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 24px; +} + +/* -------------------------------- + Modal - Switches +-------------------------------- */ +#silktide-modal .switch { + flex-shrink: 0; + position: relative; + display: inline-block; + height: 34px; + width: 74px; + cursor: pointer; +} + +#silktide-modal .switch:focus-within { + outline: none; + box-shadow: var(--focus); + border-radius: 25px; +} + +#silktide-modal .switch input { + opacity: 0; + position: absolute; +} + +/* Unchecked Switch Styles */ +#silktide-modal .switch__pill { + position: relative; + display: block; + height: 34px; + width: 74px; + background: var(--textColor); + border-radius: 25px; +} + +#silktide-modal .switch__dot { + position: absolute; + top: 2px; + left: 2px; + display: block; + height: 30px; + width: 30px; + background: var(--backgroundColor); + border-radius: 50%; + transition: left 150ms ease-out; +} + +#silktide-modal .switch__off, +#silktide-modal .switch__on { + text-transform: uppercase; + font-size: 15px; + font-weight: 500; + color: var(--backgroundColor); + position: absolute; + top: 7px; + right: 8px; + transition: right 150ms ease-out, opacity 150ms ease-out; +} + +#silktide-modal .switch__off { + opacity: 1; +} + +#silktide-modal .switch__on { + opacity: 0; +} + +/* Checked Switch Styles */ +#silktide-modal .switch input:checked + .switch__pill { + background: var(--primaryColor); +} + +#silktide-modal .switch input:checked ~ .switch__dot { + left: calc(100% - 32px); +} + +#silktide-modal .switch input:checked ~ .switch__off { + right: calc(100% - 32px); + opacity: 0; +} + +#silktide-modal .switch input:checked ~ .switch__on { + right: calc(100% - 34px); + opacity: 1; +} + +/* Disabled Switch Styles */ +#silktide-modal .switch input:disabled + .switch__pill { + opacity: 0.65; + cursor: not-allowed; +} + +/* -------------------------------- + Modal - Footer +-------------------------------- */ +#silktide-modal footer { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 24px; +} + +@media (min-width: 600px) { + #silktide-modal footer { + flex-direction: row; + align-items: center; + } +} + +#silktide-modal footer a { + margin-left: auto; +} + +/* Cookie Icon */ +#silktide-cookie-icon { + display: none; + position: fixed; + bottom: 10px; + left: 10px; + justify-content: center; + align-items: center; + width: 60px; + height: 60px; + border-radius: 50%; + padding: 0px; + border: none; + background-color: var(--cookieIconColor); + cursor: pointer; + box-shadow: 0px 0px 6px 0px #0000001a; + pointer-events: auto; + animation: silktide-fadeIn 0.3s ease-in-out forwards; +} + +#silktide-cookie-icon.bottomRight { + left: auto; + right: 10px; +} + +#silktide-cookie-icon svg { + fill: var(--cookieIconBackgroundColor); +} + +/* -------------------------------- + Backdrop +-------------------------------- */ +#silktide-backdrop { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--backdropBackgroundColor); + backdrop-filter: blur(var(--backdropBackgroundBlur)); + pointer-events: all; +} + +/* -------------------------------- + Animations +-------------------------------- */ +@keyframes silktide-fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes silktide-slideInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes silktide-slideInDown-center { + from { + opacity: 0; + transform: translate(-50%, calc(-50% - 20px)); + } + to { + opacity: 1; + transform: translate(-50%, -50%); + } +} + +@keyframes silktide-slideInDown-bottomCenter { + from { + opacity: 0; + transform: translate(-50%, -20px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } +} + +@keyframes silktide-slideInUp-center { + from { + opacity: 0; + transform: translate(0px, 20px); + } + to { + opacity: 1; + transform: translate(0px, 0px); + } +} diff --git a/public/js/silktide-consent-manager.js b/public/js/silktide-consent-manager.js new file mode 100644 index 0000000..bf63cdd --- /dev/null +++ b/public/js/silktide-consent-manager.js @@ -0,0 +1,850 @@ +// Silktide Consent Manager - https://silktide.com/consent-manager/ + +class SilktideCookieBanner { + constructor(config) { + this.config = config; // Save config to the instance + + this.wrapper = null; + this.banner = null; + this.modal = null; + this.cookieIcon = null; + this.backdrop = null; + + this.createWrapper(); + + if (this.shouldShowBackdrop()) { + this.createBackdrop(); + } + + this.createCookieIcon(); + this.createModal(); + + if (this.shouldShowBanner()) { + this.createBanner(); + this.showBackdrop(); + } else { + this.showCookieIcon(); + } + + this.setupEventListeners(); + + if (this.hasSetInitialCookieChoices()) { + this.loadRequiredCookies(); + this.runAcceptedCookieCallbacks(); + } + } + + destroyCookieBanner() { + // Remove all cookie banner elements from the DOM + if (this.wrapper && this.wrapper.parentNode) { + this.wrapper.parentNode.removeChild(this.wrapper); + } + + // Restore scrolling + this.allowBodyScroll(); + + // Clear all references + this.wrapper = null; + this.banner = null; + this.modal = null; + this.cookieIcon = null; + this.backdrop = null; + } + + // ---------------------------------------------------------------- + // Wrapper + // ---------------------------------------------------------------- + createWrapper() { + this.wrapper = document.createElement('div'); + this.wrapper.id = 'silktide-wrapper'; + document.body.insertBefore(this.wrapper, document.body.firstChild); + } + + // ---------------------------------------------------------------- + // Wrapper Child Generator + // ---------------------------------------------------------------- + createWrapperChild(htmlContent, id) { + // Create child element + const child = document.createElement('div'); + child.id = id; + child.innerHTML = htmlContent; + + // Ensure wrapper exists + if (!this.wrapper || !document.body.contains(this.wrapper)) { + this.createWrapper(); + } + + // Append child to wrapper + this.wrapper.appendChild(child); + return child; + } + + // ---------------------------------------------------------------- + // Backdrop + // ---------------------------------------------------------------- + createBackdrop() { + this.backdrop = this.createWrapperChild(null, 'silktide-backdrop'); + } + + showBackdrop() { + if (this.backdrop) { + this.backdrop.style.display = 'block'; + } + // Trigger optional onBackdropOpen callback + if (typeof this.config.onBackdropOpen === 'function') { + this.config.onBackdropOpen(); + } + } + + hideBackdrop() { + if (this.backdrop) { + this.backdrop.style.display = 'none'; + } + + // Trigger optional onBackdropClose callback + if (typeof this.config.onBackdropClose === 'function') { + this.config.onBackdropClose(); + } + } + + shouldShowBackdrop() { + return this.config?.background?.showBackground || false; + } + + // update the checkboxes in the modal with the values from localStorage + updateCheckboxState(saveToStorage = false) { + const preferencesSection = this.modal.querySelector('#cookie-preferences'); + const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); + + checkboxes.forEach((checkbox) => { + const [, cookieId] = checkbox.id.split('cookies-'); + const cookieType = this.config.cookieTypes.find(type => type.id === cookieId); + + if (!cookieType) return; + + if (saveToStorage) { + // Save the current state to localStorage and run callbacks + const currentState = checkbox.checked; + + if (cookieType.required) { + localStorage.setItem( + `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, + 'true' + ); + } else { + localStorage.setItem( + `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, + currentState.toString() + ); + + // Run appropriate callback + if (currentState && typeof cookieType.onAccept === 'function') { + cookieType.onAccept(); + } else if (!currentState && typeof cookieType.onReject === 'function') { + cookieType.onReject(); + } + } + } else { + // When reading values (opening modal) + if (cookieType.required) { + checkbox.checked = true; + checkbox.disabled = true; + } else { + const storedValue = localStorage.getItem( + `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}` + ); + + if (storedValue !== null) { + checkbox.checked = storedValue === 'true'; + } else { + checkbox.checked = !!cookieType.defaultValue; + } + } + } + }); + } + + setInitialCookieChoiceMade() { + window.localStorage.setItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`, 1); + } + + // ---------------------------------------------------------------- + // Consent Handling + // ---------------------------------------------------------------- + handleCookieChoice(accepted) { + // We set that an initial choice was made regardless of what it was so we don't show the banner again + this.setInitialCookieChoiceMade(); + + this.removeBanner(); + this.hideBackdrop(); + this.toggleModal(false); + this.showCookieIcon(); + + this.config.cookieTypes.forEach((type) => { + // Set localStorage and run accept/reject callbacks + if (type.required == true) { + localStorage.setItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 'true'); + if (typeof type.onAccept === 'function') { type.onAccept() } + } else { + localStorage.setItem( + `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, + accepted.toString(), + ); + + if (accepted) { + if (typeof type.onAccept === 'function') { type.onAccept(); } + } else { + if (typeof type.onReject === 'function') { type.onReject(); } + } + } + }); + + // Trigger optional onAcceptAll/onRejectAll callbacks + if (accepted && typeof this.config.onAcceptAll === 'function') { + if (typeof this.config.onAcceptAll === 'function') { this.config.onAcceptAll(); } + } else if (typeof this.config.onRejectAll === 'function') { + if (typeof this.config.onRejectAll === 'function') { this.config.onRejectAll(); } + } + + // finally update the checkboxes in the modal with the values from localStorage + this.updateCheckboxState(); + } + + getAcceptedCookies() { + return (this.config.cookieTypes || []).reduce((acc, cookieType) => { + acc[cookieType.id] = + localStorage.getItem(`silktideCookieChoice_${cookieType.id}${this.getBannerSuffix()}`) === + 'true'; + return acc; + }, {}); + } + + runAcceptedCookieCallbacks() { + if (!this.config.cookieTypes) return; + + const acceptedCookies = this.getAcceptedCookies(); + this.config.cookieTypes.forEach((type) => { + if (type.required) return; // we run required cookies separately in loadRequiredCookies + if (acceptedCookies[type.id] && typeof type.onAccept === 'function') { + if (typeof type.onAccept === 'function') { type.onAccept(); } + } + }); + } + + runRejectedCookieCallbacks() { + if (!this.config.cookieTypes) return; + + const rejectedCookies = this.getRejectedCookies(); + this.config.cookieTypes.forEach((type) => { + if (rejectedCookies[type.id] && typeof type.onReject === 'function') { + if (typeof type.onReject === 'function') { type.onReject(); } + } + }); + } + + /** + * Run through all of the cookie callbacks based on the current localStorage values + */ + runStoredCookiePreferenceCallbacks() { + this.config.cookieTypes.forEach((type) => { + const accepted = + localStorage.getItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`) === 'true'; + // Set localStorage and run accept/reject callbacks + if (accepted) { + if (typeof type.onAccept === 'function') { type.onAccept(); } + } else { + if (typeof type.onReject === 'function') { type.onReject(); } + } + }); + } + + loadRequiredCookies() { + if (!this.config.cookieTypes) return; + this.config.cookieTypes.forEach((cookie) => { + if (cookie.required && typeof cookie.onAccept === 'function') { + if (typeof cookie.onAccept === 'function') { cookie.onAccept(); } + } + }); + } + + // ---------------------------------------------------------------- + // Banner + // ---------------------------------------------------------------- + getBannerContent() { + const bannerDescription = + this.config.text?.banner?.description || + `

We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic.

`; + + // Accept button + const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all'; + const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; + const acceptAllButton = ``; + + // Reject button + const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential'; + const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; + const rejectNonEssentialButton = ``; + + // Preferences button + const preferencesButtonText = this.config.text?.banner?.preferencesButtonText || 'Preferences'; + const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; + const preferencesButton = ``; + + + // Silktide logo link + const silktideLogo = ` + + `; + + const bannerContent = ` + ${bannerDescription} +
+ ${acceptAllButton} + ${rejectNonEssentialButton} +
+ ${preferencesButton} + ${silktideLogo} +
+
+ `; + + return bannerContent; + } + + hasSetInitialCookieChoices() { + return !!localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`); + } + + createBanner() { + // Create banner element + this.banner = this.createWrapperChild(this.getBannerContent(), 'silktide-banner'); + + // Add positioning class from config + if (this.banner && this.config.position?.banner) { + this.banner.classList.add(this.config.position.banner); + } + + // Trigger optional onBannerOpen callback + if (this.banner && typeof this.config.onBannerOpen === 'function') { + this.config.onBannerOpen(); + } + } + + removeBanner() { + if (this.banner && this.banner.parentNode) { + this.banner.parentNode.removeChild(this.banner); + this.banner = null; + + // Trigger optional onBannerClose callback + if (typeof this.config.onBannerClose === 'function') { + this.config.onBannerClose(); + } + } + } + + shouldShowBanner() { + if (this.config.showBanner === false) { + return false; + } + return ( + localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`) === null + ); + } + + // ---------------------------------------------------------------- + // Modal + // ---------------------------------------------------------------- + getModalContent() { + const preferencesTitle = + this.config.text?.preferences?.title || 'Customize your cookie preferences'; + + const preferencesDescription = + this.config.text?.preferences?.description || + `

We respect your right to privacy. You can choose not to allow some types of cookies. Your cookie preferences will apply across our website.

`; + + // Preferences button + const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; + + const closeModalButton = ``; + + + const cookieTypes = this.config.cookieTypes || []; + const acceptedCookieMap = this.getAcceptedCookies(); + + // Accept button + const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all'; + const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; + const acceptAllButton = ``; + + // Reject button + const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential'; + const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; + const rejectNonEssentialButton = ``; + + // Credit link + const creditLinkText = this.config.text?.preferences?.creditLinkText || 'Get this banner for free'; + const creditLinkAccessibleLabel = this.config.text?.preferences?.creditLinkAccessibleLabel; + const creditLink = `${creditLinkText}`; + + + + const modalContent = ` +
+

${preferencesTitle}

+ ${closeModalButton} +
+ ${preferencesDescription} + + + `; + + return modalContent; + } + + createModal() { + // Create banner element + this.modal = this.createWrapperChild(this.getModalContent(), 'silktide-modal'); + } + + toggleModal(show) { + if (!this.modal) return; + + this.modal.style.display = show ? 'flex' : 'none'; + + if (show) { + this.showBackdrop(); + this.hideCookieIcon(); + this.removeBanner(); + this.preventBodyScroll(); + + // Focus the close button + const modalCloseButton = this.modal.querySelector('.modal-close'); + modalCloseButton.focus(); + + // Trigger optional onPreferencesOpen callback + if (typeof this.config.onPreferencesOpen === 'function') { + this.config.onPreferencesOpen(); + } + + this.updateCheckboxState(false); // read from storage when opening + } else { + // Set that an initial choice was made when closing the modal + this.setInitialCookieChoiceMade(); + + // Save current checkbox states to storage + this.updateCheckboxState(true); + + this.hideBackdrop(); + this.showCookieIcon(); + this.allowBodyScroll(); + + // Trigger optional onPreferencesClose callback + if (typeof this.config.onPreferencesClose === 'function') { + this.config.onPreferencesClose(); + } + } + } + + // ---------------------------------------------------------------- + // Cookie Icon + // ---------------------------------------------------------------- + getCookieIconContent() { + return ` + + + + `; + } + + createCookieIcon() { + this.cookieIcon = document.createElement('button'); + this.cookieIcon.id = 'silktide-cookie-icon'; + this.cookieIcon.innerHTML = this.getCookieIconContent(); + + if (this.config.text?.banner?.preferencesButtonAccessibleLabel) { + this.cookieIcon.ariaLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; + } + + // Ensure wrapper exists + if (!this.wrapper || !document.body.contains(this.wrapper)) { + this.createWrapper(); + } + + // Append child to wrapper + this.wrapper.appendChild(this.cookieIcon); + + // Add positioning class from config + if (this.cookieIcon && this.config.cookieIcon?.position) { + this.cookieIcon.classList.add(this.config.cookieIcon.position); + } + + // Add color scheme class from config + if (this.cookieIcon && this.config.cookieIcon?.colorScheme) { + this.cookieIcon.classList.add(this.config.cookieIcon.colorScheme); + } + } + + showCookieIcon() { + if (this.cookieIcon) { + this.cookieIcon.style.display = 'flex'; + } + } + + hideCookieIcon() { + if (this.cookieIcon) { + this.cookieIcon.style.display = 'none'; + } + } + + /** + * This runs if the user closes the modal without making a choice for the first time + * We apply the default values and the necessary values as default + */ + handleClosedWithNoChoice() { + this.config.cookieTypes.forEach((type) => { + let accepted = true; + // Set localStorage and run accept/reject callbacks + if (type.required == true) { + localStorage.setItem( + `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, + accepted.toString(), + ); + } else if (type.defaultValue) { + localStorage.setItem( + `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, + accepted.toString(), + ); + } else { + accepted = false; + localStorage.setItem( + `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, + accepted.toString(), + ); + } + + if (accepted) { + if (typeof type.onAccept === 'function') { type.onAccept(); } + } else { + if (typeof type.onReject === 'function') { type.onReject(); } + } + // set the flag to say that the cookie choice has been made + this.setInitialCookieChoiceMade(); + this.updateCheckboxState(); + }); + } + + // ---------------------------------------------------------------- + // Focusable Elements + // ---------------------------------------------------------------- + getFocusableElements(element) { + return element.querySelectorAll( + 'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ); + } + + // ---------------------------------------------------------------- + // Event Listeners + // ---------------------------------------------------------------- + setupEventListeners() { + // Check Banner exists before trying to add event listeners + if (this.banner) { + // Get the buttons + const acceptButton = this.banner.querySelector('.accept-all'); + const rejectButton = this.banner.querySelector('.reject-all'); + const preferencesButton = this.banner.querySelector('.preferences'); + + // Add event listeners to the buttons + acceptButton?.addEventListener('click', () => this.handleCookieChoice(true)); + rejectButton?.addEventListener('click', () => this.handleCookieChoice(false)); + preferencesButton?.addEventListener('click', () => { + this.showBackdrop(); + this.toggleModal(true); + }); + + // Focus Trap + const focusableElements = this.getFocusableElements(this.banner); + const firstFocusableEl = focusableElements[0]; + const lastFocusableEl = focusableElements[focusableElements.length - 1]; + + // Add keydown event listener to handle tab navigation + this.banner.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + if (e.shiftKey) { + if (document.activeElement === firstFocusableEl) { + lastFocusableEl.focus(); + e.preventDefault(); + } + } else { + if (document.activeElement === lastFocusableEl) { + firstFocusableEl.focus(); + e.preventDefault(); + } + } + } + }); + + // Set initial focus + if (this.config.mode !== 'wizard') { + acceptButton?.focus(); + } + } + + // Check Modal exists before trying to add event listeners + if (this.modal) { + const closeButton = this.modal.querySelector('.modal-close'); + const acceptAllButton = this.modal.querySelector('.preferences-accept-all'); + const rejectAllButton = this.modal.querySelector('.preferences-reject-all'); + + closeButton?.addEventListener('click', () => { + this.toggleModal(false); + + const hasMadeFirstChoice = this.hasSetInitialCookieChoices(); + + if (hasMadeFirstChoice) { + // run through the callbacks based on the current localStorage state + this.runStoredCookiePreferenceCallbacks(); + } else { + // handle the case where the user closes without making a choice for the first time + this.handleClosedWithNoChoice(); + } + }); + acceptAllButton?.addEventListener('click', () => this.handleCookieChoice(true)); + rejectAllButton?.addEventListener('click', () => this.handleCookieChoice(false)); + + // Banner Focus Trap + const focusableElements = this.getFocusableElements(this.modal); + const firstFocusableEl = focusableElements[0]; + const lastFocusableEl = focusableElements[focusableElements.length - 1]; + + this.modal.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + if (e.shiftKey) { + if (document.activeElement === firstFocusableEl) { + lastFocusableEl.focus(); + e.preventDefault(); + } + } else { + if (document.activeElement === lastFocusableEl) { + firstFocusableEl.focus(); + e.preventDefault(); + } + } + } + if (e.key === 'Escape') { + this.toggleModal(false); + } + }); + + closeButton?.focus(); + + // Update the checkbox event listeners + const preferencesSection = this.modal.querySelector('#cookie-preferences'); + const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); + + checkboxes.forEach(checkbox => { + checkbox.addEventListener('change', (event) => { + const [, cookieId] = event.target.id.split('cookies-'); + const isAccepted = event.target.checked; + const previousValue = localStorage.getItem( + `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}` + ) === 'true'; + + // Only proceed if the value has actually changed + if (isAccepted !== previousValue) { + // Find the corresponding cookie type + const cookieType = this.config.cookieTypes.find(type => type.id === cookieId); + + if (cookieType) { + // Update localStorage + localStorage.setItem( + `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, + isAccepted.toString() + ); + + // Run the appropriate callback only if the value changed + if (isAccepted && typeof cookieType.onAccept === 'function') { + cookieType.onAccept(); + } else if (!isAccepted && typeof cookieType.onReject === 'function') { + cookieType.onReject(); + } + } + } + }); + }); + } + + // Check Cookie Icon exists before trying to add event listeners + if (this.cookieIcon) { + + this.cookieIcon.addEventListener('click', () => { + // If modal is not found, create it + if (!this.modal) { + this.createModal(); + this.toggleModal(true); + this.hideCookieIcon(); + } + // If modal is hidden, show it + else if (this.modal.style.display === 'none' || this.modal.style.display === '') { + this.toggleModal(true); + this.hideCookieIcon(); + } + // If modal is visible, hide it + else { + this.toggleModal(false); + } + }); + } + } + + getBannerSuffix() { + if (this.config.bannerSuffix) { + return '_' + this.config.bannerSuffix; + } + return ''; + } + + preventBodyScroll() { + document.body.style.overflow = 'hidden'; + // Prevent iOS Safari scrolling + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + } + + allowBodyScroll() { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + } +} + +(function () { + window.silktideCookieBannerManager = {}; + + let config = {}; + let cookieBanner; + + function updateCookieBannerConfig(userConfig = {}) { + config = {...config, ...userConfig}; + + // If cookie banner exists, destroy and recreate it with new config + if (cookieBanner) { + cookieBanner.destroyCookieBanner(); // We'll need to add this method + cookieBanner = null; + } + + // Only initialize if document.body exists + if (document.body) { + initCookieBanner(); + } else { + // Wait for DOM to be ready + document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true}); + } + } + + function initCookieBanner() { + if (!cookieBanner) { + cookieBanner = new SilktideCookieBanner(config); // Pass config to the CookieBanner instance + } + } + + function injectScript(url, loadOption) { + // Check if script with this URL already exists + const existingScript = document.querySelector(`script[src="${url}"]`); + if (existingScript) { + return; // Script already exists, don't add it again + } + + const script = document.createElement('script'); + script.src = url; + + // Apply the async or defer attribute based on the loadOption parameter + if (loadOption === 'async') { + script.async = true; + } else if (loadOption === 'defer') { + script.defer = true; + } + + document.head.appendChild(script); + } + + window.silktideCookieBannerManager.initCookieBanner = initCookieBanner; + window.silktideCookieBannerManager.updateCookieBannerConfig = updateCookieBannerConfig; + window.silktideCookieBannerManager.injectScript = injectScript; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true}); + } else { + initCookieBanner(); + } +})(); \ No newline at end of file