Expand cache

This commit is contained in:
James Cole
2023-08-27 07:45:09 +02:00
parent 66cc3f48bc
commit 83d94cb792
12 changed files with 261 additions and 61 deletions

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -38,6 +39,7 @@ use Illuminate\Support\Collection;
class AccountTransformer extends AbstractTransformer class AccountTransformer extends AbstractTransformer
{ {
private array $accountMeta; private array $accountMeta;
private array $accountTypes;
private array $balances; private array $balances;
private array $convertedBalances; private array $convertedBalances;
private array $currencies; private array $currencies;
@@ -51,6 +53,7 @@ class AccountTransformer extends AbstractTransformer
{ {
$this->currencies = []; $this->currencies = [];
$this->accountMeta = []; $this->accountMeta = [];
$this->accountTypes = [];
$this->balances = app('steam')->balancesByAccounts($objects, $this->getDate()); $this->balances = app('steam')->balancesByAccounts($objects, $this->getDate());
$this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate()); $this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate());
$repository = app(CurrencyRepositoryInterface::class); $repository = app(CurrencyRepositoryInterface::class);
@@ -72,6 +75,15 @@ class AccountTransformer extends AbstractTransformer
$id = (int)$entry->account_id; $id = (int)$entry->account_id;
$this->accountMeta[$id][$entry->name] = $entry->data; $this->accountMeta[$id][$entry->name] = $entry->data;
} }
// get account types:
// select accounts.id, account_types.type from account_types left join accounts on accounts.account_type_id = account_types.id;
$accountTypes = AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id')
->whereIn('accounts.id', $accountIds)
->get(['accounts.id', 'account_types.type']);
/** @var AccountType $row */
foreach ($accountTypes as $row) {
$this->accountTypes[(int)$row->id] = (string)config(sprintf('firefly.shortNamesByFullName.%s', $row->type));
}
} }
/** /**
@@ -96,10 +108,13 @@ class AccountTransformer extends AbstractTransformer
*/ */
public function transform(Account $account): array public function transform(Account $account): array
{ {
//$fullType = $account->accountType->type;
//$accountType = (string) config(sprintf('firefly.shortNamesByFullName.%s', $fullType));
$id = (int)$account->id; $id = (int)$account->id;
// various meta
$accountRole = $this->accountMeta[$id]['account_role'] ?? null;
$accountType = $this->accountTypes[$id];
$order = (int)$account->order;
// no currency? use default // no currency? use default
$currency = $this->default; $currency = $this->default;
if (0 !== (int)$this->accountMeta[$id]['currency_id']) { if (0 !== (int)$this->accountMeta[$id]['currency_id']) {
@@ -109,16 +124,21 @@ class AccountTransformer extends AbstractTransformer
$balance = $this->balances[$id] ?? null; $balance = $this->balances[$id] ?? null;
$nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null; $nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null;
// no order for some accounts:
if (!in_array(strtolower($accountType), ['liability', 'liabilities', 'asset'], true)) {
$order = null;
}
return [ return [
'id' => (string)$account->id, 'id' => (string)$account->id,
'created_at' => $account->created_at->toAtomString(), 'created_at' => $account->created_at->toAtomString(),
'updated_at' => $account->updated_at->toAtomString(), 'updated_at' => $account->updated_at->toAtomString(),
'active' => $account->active, 'active' => $account->active,
//'order' => $order, 'order' => $order,
'name' => $account->name, 'name' => $account->name,
'iban' => '' === $account->iban ? null : $account->iban, 'iban' => '' === $account->iban ? null : $account->iban,
// 'type' => strtolower($accountType), 'type' => strtolower($accountType),
// 'account_role' => $accountRole, 'account_role' => $accountRole,
'currency_id' => (string)$currency->id, 'currency_id' => (string)$currency->id,
'currency_code' => $currency->code, 'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol, 'currency_symbol' => $currency->symbol,

File diff suppressed because one or more lines are too long

View File

@@ -30,10 +30,10 @@
"integrity": "sha384-B73JAwYNSgI4rwb14zwxigHgAkg1Ms+j6+9sJoDpiL11+VW5RjQCLfIh0RVoi0h6" "integrity": "sha384-B73JAwYNSgI4rwb14zwxigHgAkg1Ms+j6+9sJoDpiL11+VW5RjQCLfIh0RVoi0h6"
}, },
"resources/assets/v2/pages/dashboard/dashboard.js": { "resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-d2c915f3.js", "file": "assets/dashboard-7d4913ce.js",
"isEntry": true, "isEntry": true,
"src": "resources/assets/v2/pages/dashboard/dashboard.js", "src": "resources/assets/v2/pages/dashboard/dashboard.js",
"integrity": "sha384-wep3ofduNkA43fzF5MbbAUj1EzVhslvRGK/dxI0d8o1zCEpFfz2xSCi8EMezy4Ia" "integrity": "sha384-D/XG0fkIuVUkCWnAa6ylWQBCrMwuNJAeD65RK/ZFXnpkJvwmd9zn3IxTlqS/RyE8"
}, },
"resources/assets/v2/sass/app.scss": { "resources/assets/v2/sass/app.scss": {
"file": "assets/app-28a195fd.css", "file": "assets/app-28a195fd.css",

View File

@@ -53,18 +53,17 @@ export default () => ({
let options = window.store.get(CHART_CACHE_KEY); let options = window.store.get(CHART_CACHE_KEY);
this.drawChart(options); this.drawChart(options);
this.loading = false; this.loading = false;
return;
} }
if (!cacheValid || typeof cachedData === 'undefined') { const dashboard = new Dashboard();
const dashboard = new Dashboard(); dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => { this.chartData = response.data;
this.chartData = response.data; // cache generated options:
// cache generated options: let options = this.generateOptions(this.chartData);
let options = this.generateOptions(this.chartData); window.store.set(CHART_CACHE_KEY, options);
window.store.set(CHART_CACHE_KEY, options); this.drawChart(options);
this.drawChart(options); this.loading = false;
this.loading = false; });
});
}
}, },
generateOptions(data) { generateOptions(data) {
@@ -150,14 +149,11 @@ export default () => ({
chart = new Chart(document.querySelector("#account-chart"), options); chart = new Chart(document.querySelector("#account-chart"), options);
}, },
loadAccounts() { loadAccounts() {
// console.log('loadAccounts');
if (true === this.loadingAccounts) { if (true === this.loadingAccounts) {
// console.log('loadAccounts CANCELLED');
return; return;
} }
this.loadingAccounts = true; this.loadingAccounts = true;
if (this.accountList.length > 0) { if (this.accountList.length > 0) {
// console.log('NO need to load account data');
this.loadingAccounts = false; this.loadingAccounts = false;
return; return;
} }
@@ -217,6 +213,7 @@ export default () => ({
// console.log(parent); // console.log(parent);
accounts.push({ accounts.push({
name: parent.attributes.name, name: parent.attributes.name,
order: parent.attributes.order,
id: parent.id, id: parent.id,
balance_raw: parseFloat(parent.attributes.current_balance), balance_raw: parseFloat(parent.attributes.current_balance),
balance: formatMoney(parent.attributes.current_balance, parent.attributes.currency_code), balance: formatMoney(parent.attributes.current_balance, parent.attributes.currency_code),
@@ -224,8 +221,11 @@ export default () => ({
native_balance: formatMoney(parent.attributes.native_current_balance, parent.attributes.native_code), native_balance: formatMoney(parent.attributes.native_current_balance, parent.attributes.native_code),
groups: groups, groups: groups,
}); });
console.log(parent.attributes);
count++; count++;
if (count === totalAccounts) { if (count === totalAccounts) {
accounts.sort((a, b) => a.order - b.order); // b - a for reverse sort
this.accountList = accounts; this.accountList = accounts;
this.loadingAccounts = false; this.loadingAccounts = false;
window.store.set(ACCOUNTS_CACHE_KEY, accounts); window.store.set(ACCOUNTS_CACHE_KEY, accounts);

View File

@@ -24,7 +24,7 @@ import {getVariable} from "../../store/get-variable.js";
import formatMoney from "../../util/format-money.js"; import formatMoney from "../../util/format-money.js";
let afterPromises = false; let afterPromises = false;
const CACHE_KEY = 'dashboard-boxes-data';
export default () => ({ export default () => ({
balanceBox: {amounts: [], subtitles: []}, balanceBox: {amounts: [], subtitles: []},
billBox: {paid: [], unpaid: []}, billBox: {paid: [], unpaid: []},
@@ -35,6 +35,16 @@ export default () => ({
boxData: null, boxData: null,
boxOptions: null, boxOptions: null,
getFreshData() { getFreshData() {
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(CACHE_KEY);
if (cacheValid && typeof cachedData !== 'undefined') {
this.boxData = cachedData;
this.generateOptions(this.boxData);
return;
}
// get stuff // get stuff
let getter = new Summary(); let getter = new Summary();
let start = new Date(window.store.get('start')); let start = new Date(window.store.get('start'));
@@ -42,6 +52,7 @@ export default () => ({
getter.get(format(start, 'yyyy-MM-dd'), format(end, 'yyyy-MM-dd'), null).then((response) => { getter.get(format(start, 'yyyy-MM-dd'), format(end, 'yyyy-MM-dd'), null).then((response) => {
this.boxData = response.data; this.boxData = response.data;
window.store.set(CACHE_KEY, response.data);
this.generateOptions(this.boxData); this.generateOptions(this.boxData);
//this.drawChart(); //this.drawChart();
}); });

View File

@@ -32,7 +32,7 @@ let afterPromises = false;
let i18n; // for translating items in the chart. let i18n; // for translating items in the chart.
const CACHE_KEY = 'dashboard-budgets-chart';
export default () => ({ export default () => ({
loading: false, loading: false,
autoConversion: false, autoConversion: false,
@@ -58,10 +58,21 @@ export default () => ({
chart = new Chart(document.querySelector("#budget-chart"), options); chart = new Chart(document.querySelector("#budget-chart"), options);
}, },
getFreshData() { getFreshData() {
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(CACHE_KEY);
if (cacheValid && typeof cachedData !== 'undefined') {
chartData = cachedData; // save chart data for later.
this.drawChart(this.generateOptions(chartData));
this.loading = false;
return;
}
const dashboard = new Dashboard(); const dashboard = new Dashboard();
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => { dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
chartData = response.data; // save chart data for later. chartData = response.data; // save chart data for later.
this.drawChart(this.generateOptions(response.data)); this.drawChart(this.generateOptions(chartData));
window.store.set(CACHE_KEY, chartData);
this.loading = false; this.loading = false;
}); });
}, },
@@ -93,19 +104,19 @@ export default () => ({
label: i18n.t('firefly.spent'), label: i18n.t('firefly.spent'),
data: [], data: [],
borderWidth: 1, borderWidth: 1,
stack: 1 stack: 1,
}, },
{ {
label: i18n.t('firefly.left'), label: i18n.t('firefly.left'),
data: [], data: [],
borderWidth: 1, borderWidth: 1,
stack: 1 stack: 1,
}, },
{ {
label: i18n.t('firefly.overspent'), label: i18n.t('firefly.overspent'),
data: [], data: [],
borderWidth: 1, borderWidth: 1,
stack: 1 stack: 1,
} }
] ]
}; };
@@ -158,13 +169,13 @@ export default () => ({
i18n = new I18n(); i18n = new I18n();
i18n.locale = values[1]; i18n.locale = values[1];
loadTranslations(i18n, values[1]); loadTranslations(i18n, values[1]).then(() => {
this.autoConversion = values[0];
this.autoConversion = values[0]; afterPromises = true;
afterPromises = true; if (false === this.loading) {
if (false === this.loading) { this.loadChart();
this.loadChart(); }
} });
}); });
window.store.observe('end', () => { window.store.observe('end', () => {
if (!afterPromises) { if (!afterPromises) {
@@ -172,7 +183,7 @@ export default () => ({
} }
// console.log('boxes observe end'); // console.log('boxes observe end');
if (false === this.loading) { if (false === this.loading) {
this.chartData = null; chartData = null;
this.loadChart(); this.loadChart();
} }
}); });

View File

@@ -28,6 +28,8 @@ let chart = null;
let chartData = null; let chartData = null;
let afterPromises = false; let afterPromises = false;
const CACHE_KEY = 'dashboard-categories-chart';
export default () => ({ export default () => ({
loading: false, loading: false,
autoConversion: false, autoConversion: false,
@@ -140,10 +142,21 @@ export default () => ({
}, },
getFreshData() { getFreshData() {
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(CACHE_KEY);
if (cacheValid && typeof cachedData !== 'undefined') {
chartData = cachedData; // save chart data for later.
this.drawChart(this.generateOptions(chartData));
this.loading = false;
return;
}
const dashboard = new Dashboard(); const dashboard = new Dashboard();
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => { dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
chartData = response.data; // save chart data for later. chartData = response.data; // save chart data for later.
this.drawChart(this.generateOptions(response.data)); this.drawChart(this.generateOptions(response.data));
window.store.set(CACHE_KEY, chartData);
this.loading = false; this.loading = false;
}); });
}, },

View File

@@ -25,6 +25,7 @@ import {loadTranslations} from "../../support/load-translations.js";
let apiData = {}; let apiData = {};
let afterPromises = false; let afterPromises = false;
let i18n; let i18n;
const CACHE_KEY = 'dashboard-piggies-data';
export default () => ({ export default () => ({
loading: false, loading: false,
@@ -32,6 +33,16 @@ export default () => ({
sankeyGrouping: 'account', sankeyGrouping: 'account',
piggies: [], piggies: [],
getFreshData() { getFreshData() {
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(CACHE_KEY);
if (cacheValid && typeof cachedData !== 'undefined') {
apiData = cachedData;
this.parsePiggies();
this.loading = false;
return;
}
let params = { let params = {
start: window.store.get('start').slice(0, 10), start: window.store.get('start').slice(0, 10),
end: window.store.get('end').slice(0, 10), end: window.store.get('end').slice(0, 10),
@@ -49,6 +60,7 @@ export default () => ({
this.downloadPiggyBanks(params); this.downloadPiggyBanks(params);
return; return;
} }
window.store.set(CACHE_KEY, apiData);
this.parsePiggies(); this.parsePiggies();
this.loading = false; this.loading = false;
}); });
@@ -114,12 +126,14 @@ export default () => ({
i18n = new I18n(); i18n = new I18n();
i18n.locale = values[1]; i18n.locale = values[1];
loadTranslations(i18n, values[1]); loadTranslations(i18n, values[1]).then(() => {
// console.log('piggies after promises');
afterPromises = true;
this.autoConversion = values[0];
this.loadPiggyBanks();
});
// console.log('piggies after promises');
afterPromises = true;
this.autoConversion = values[0];
this.loadPiggyBanks();
}); });
window.store.observe('end', () => { window.store.observe('end', () => {
if (!afterPromises) { if (!afterPromises) {

View File

@@ -27,6 +27,7 @@ import {I18n} from "i18n-js";
Chart.register({SankeyController, Flow}); Chart.register({SankeyController, Flow});
const CACHE_KEY = 'dashboard-sankey-data';
let i18n; let i18n;
let currencies = []; let currencies = [];
let afterPromises = false; let afterPromises = false;
@@ -256,7 +257,7 @@ export default () => ({
let dataSet = let dataSet =
// sankey chart has one data set. // sankey chart has one data set.
{ {
label: 'My sankey', label: 'Firefly III dashboard sankey chart',
data: [], data: [],
colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from), colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from),
colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to), colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to),
@@ -284,6 +285,17 @@ export default () => ({
}, },
getFreshData() { getFreshData() {
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(CACHE_KEY);
if (cacheValid && typeof cachedData !== 'undefined') {
transactions = cachedData;
this.drawChart(this.generateOptions());
this.loading = false;
return;
}
let params = { let params = {
start: window.store.get('start').slice(0, 10), start: window.store.get('start').slice(0, 10),
end: window.store.get('end').slice(0, 10), end: window.store.get('end').slice(0, 10),
@@ -305,9 +317,7 @@ export default () => ({
this.downloadTransactions(params); this.downloadTransactions(params);
return; return;
} }
// continue to next step. window.store.set(CACHE_KEY, transactions);
//console.log('Final page!');
//console.log(transactions);
this.drawChart(this.generateOptions()); this.drawChart(this.generateOptions());
this.loading = false; this.loading = false;
}); });
@@ -347,13 +357,13 @@ export default () => ({
translations.expense_account = i18n.t('firefly.expense_account'); translations.expense_account = i18n.t('firefly.expense_account');
translations.revenue_account = i18n.t('firefly.revenue_account'); translations.revenue_account = i18n.t('firefly.revenue_account');
translations.budget = i18n.t('firefly.budget'); translations.budget = i18n.t('firefly.budget');
// console.log('sankey after promises');
afterPromises = true;
this.autoConversion = values[0];
this.loadChart();
}); });
// console.log('sankey after promises');
afterPromises = true;
this.autoConversion = values[0];
this.loadChart();
}); });
window.store.observe('end', () => { window.store.observe('end', () => {
if (!afterPromises) { if (!afterPromises) {

View File

@@ -25,6 +25,8 @@ import {Chart} from 'chart.js';
import {I18n} from "i18n-js"; import {I18n} from "i18n-js";
import {loadTranslations} from "../../support/load-translations.js"; import {loadTranslations} from "../../support/load-translations.js";
const CACHE_KEY = 'dashboard-subscriptions-data';
let chart = null; let chart = null;
let chartData = null; let chartData = null;
let afterPromises = false; let afterPromises = false;
@@ -54,6 +56,17 @@ export default () => ({
chart = new Chart(document.querySelector("#subscriptions-chart"), options); chart = new Chart(document.querySelector("#subscriptions-chart"), options);
}, },
getFreshData() { getFreshData() {
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(CACHE_KEY);
if (cacheValid && typeof cachedData !== 'undefined') {
this.drawChart(this.generateOptions(cachedData));
this.loading = false;
return;
}
const getter = new Get(); const getter = new Get();
let params = { let params = {
start: format(new Date(window.store.get('start')), 'y-MM-dd'), start: format(new Date(window.store.get('start')), 'y-MM-dd'),
@@ -65,6 +78,7 @@ export default () => ({
getter.unpaid(params).then((response) => { getter.unpaid(params).then((response) => {
let unpaidData = response.data; let unpaidData = response.data;
let chartData = {paid: paidData, unpaid: unpaidData}; let chartData = {paid: paidData, unpaid: unpaidData};
window.store.set(CACHE_KEY, chartData);
this.drawChart(this.generateOptions(chartData)); this.drawChart(this.generateOptions(chartData));
this.loading = false; this.loading = false;
}); });
@@ -138,12 +152,13 @@ export default () => ({
i18n = new I18n(); i18n = new I18n();
i18n.locale = values[1]; i18n.locale = values[1];
loadTranslations(i18n, values[1]); loadTranslations(i18n, values[1]).then(() => {
if (false === this.loading) {
this.loadChart();
}
});
if (false === this.loading) {
this.loadChart();
}
}); });
window.store.observe('end', () => { window.store.observe('end', () => {
if (!afterPromises) { if (!afterPromises) {

View File

@@ -0,0 +1,103 @@
/*
* get-colors.js
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// some default colors in dark and light:
import {Color} from '@kurkle/color';
// base colors for most things
let red = new Color('#dc3545'); // same as bootstrap danger
let green = new Color('#198754'); // same as bootstrap success.
let blue = new Color('#0d6efd'); // bootstrap blue.
// four other colors:
let orange = new Color('#fd7e14'); // bootstrap orange.
let index = 0;
// or cycle through X colors:
if ('light' === window.theme) {
red.lighten(0.3).clearer(0.3);
green.lighten(0.3).clearer(0.3);
blue.lighten(0.3).clearer(0.3);
orange.lighten(0.3).clearer(0.3);
}
let allColors = [red, green, blue, orange];
function getColors(type, field) {
index++;
let colors = {
borderColor: red.rgbString(),
backgroundColor: red.rgbString(),
};
let border;
switch (type) {
default:
let currentIndex = (Math.ceil(index / 2) % allColors.length) - 1;
border = new Color(allColors[currentIndex].rgbString());
border.darken(0.4);
colors = {
borderColor: border.rgbString(),
backgroundColor: allColors[currentIndex].rgbString(),
};
break;
case 'spent':
border = new Color(blue.rgbString());
border.darken(0.4);
colors = {
borderColor: border.rgbString(),
backgroundColor: blue.rgbString(),
};
break;
case 'left':
border = new Color(green.rgbString());
border.darken(0.4);
colors = {
borderColor: border.rgbString(),
backgroundColor: green.rgbString(),
};
break;
case 'overspent':
border = new Color(red.rgbString());
border.darken(0.4);
colors = {
borderColor: border.rgbString(),
backgroundColor: red.rgbString(),
};
break;
}
if ('border' === field) {
return colors.borderColor;
}
if ('background' === field) {
return colors.backgroundColor;
}
return '#FF0000'; // panic!
}
export {getColors};

View File

@@ -32,13 +32,16 @@
const setTheme = theme => { const setTheme = theme => {
if (theme === 'browser' && window.matchMedia('(prefers-color-scheme: dark)').matches) { if (theme === 'browser' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark') document.documentElement.setAttribute('data-bs-theme', 'dark')
window.theme = 'dark';
return; return;
} }
if (theme === 'browser' && window.matchMedia('(prefers-color-scheme: light)').matches) { if (theme === 'browser' && window.matchMedia('(prefers-color-scheme: light)').matches) {
window.theme = 'light';
document.documentElement.setAttribute('data-bs-theme', 'light') document.documentElement.setAttribute('data-bs-theme', 'light')
return; return;
} }
document.documentElement.setAttribute('data-bs-theme', theme) document.documentElement.setAttribute('data-bs-theme', theme)
window.theme = theme;
} }
setTheme(getPreferredTheme()) setTheme(getPreferredTheme())