From c57f639a6528e6f98230ce9144981344cd0933ad Mon Sep 17 00:00:00 2001 From: Malek Kamel Date: Fri, 18 Oct 2024 17:57:22 +0300 Subject: [PATCH 1/5] Enhance Dashboard: Implement Comprehensive Support for JSON Objects - Added functionality to fully process and display JSON objects within the dashboard. - Updated data handling methods to accommodate nested structures and arrays. - Improved UI components to render JSON data dynamically. Add JSON manifest for generating platform-specific code from JSON files - JSON manifest that enables the generation of platform-specific code based on the provided JSON files. This enhancement streamlines the development process and ensures better code organization for different platforms. Enhance brand switching process by enabling resource copying - Implemented functionality to copy resources associated with a brand during the switching process. - Utilized a resources manifest to streamline the resource management. - This improvement ensures a smoother transition between brands, maintaining consistency and reducing manual resource handling. --- Gemfile | 3 +- Gemfile.lock | 32 +- solara/lib/core/brands/brand_switcher.rb | 59 ++- .../lib/core/dashboard/brand/BrandDetail.js | 36 +- .../dashboard/brand/BrandDetailController.js | 256 +--------- .../core/dashboard/brand/BrandDetailModel.js | 18 +- .../core/dashboard/brand/BrandDetailView.js | 216 +------- .../dashboard/brand/SectionsFormManager.js | 232 +++++++++ solara/lib/core/dashboard/brand/brand.html | 364 +++++++------- .../brand/source/BrandLocalSource.js | 7 +- .../brand/source/BrandRemoteSource.js | 169 ++----- solara/lib/core/dashboard/brands/Brands.js | 31 ++ .../core/dashboard/brands/BrandsController.js | 5 - .../lib/core/dashboard/brands/BrandsView.js | 4 +- solara/lib/core/dashboard/brands/brands.html | 123 +++-- .../core/dashboard/component/AddFieldSheet.js | 175 ------- .../dashboard/component/AliasesBottomSheet.js | 12 +- .../component/BrandOptionsBottomSheet.js | 8 +- .../dashboard/component/ConfirmationDialog.js | 25 +- .../core/dashboard/component/EditJsonSheet.js | 160 ++++++ .../dashboard/component/MessageBottomSheet.js | 10 +- .../component/OnboardBrandBottomSheet.js | 8 +- .../core/dashboard/handler/base_handler.rb | 1 + .../handler/brand_configurations_manager.rb | 73 --- .../dashboard/handler/edit_section_handler.rb | 6 +- .../doctor/schema/brand_configurations.json | 8 - .../platform/global/resources_manifest.json | 30 ++ .../doctor/schema/platform/json_manifest.json | 57 +++ .../android_template_validation_config.yml | 36 +- .../flutter_template_validation_config.yml | 31 +- .../ios_template_validation_config.yml | 36 +- .../validator/template/template_validator.rb | 18 +- .../lib/core/scripts/brand_config_manager.rb | 2 +- .../scripts/brand_configurations_manager.rb | 41 ++ solara/lib/core/scripts/code_generator.rb | 460 +++++++++++++----- solara/lib/core/scripts/file_path.rb | 22 +- solara/lib/core/scripts/gitignore_manager.rb | 14 +- .../core/scripts/json_manifest_processor.rb | 95 ++++ .../ios/infoplist_string_catalog_manager.rb | 12 +- .../scripts/resource_manifest_processor.rb | 151 ++++++ .../lib/core/scripts/solara_status_manager.rb | 2 +- solara/lib/core/scripts/theme_generator.rb | 263 +--------- solara/lib/core/solara_configurator.rb | 2 +- .../brands/global/resources_manifest.json | 10 + .../template/brands/json/Json-Manifest.md | 61 +++ .../template/brands/json/json_manifest.json | 18 + .../core/template/brands/shared/theme.json | 242 +++++++-- .../config/android_template_config.json | 50 ++ .../config/flutter_template_config.json | 35 ++ .../template/config/ios_template_config.json | 50 ++ solara/lib/core/template/configurations.json | 46 ++ .../template/project_template_generator.rb | 2 + solara/lib/solara.rb | 19 + solara/lib/spec/solara_cli_spec.rb | 42 ++ 54 files changed, 2358 insertions(+), 1530 deletions(-) create mode 100644 solara/lib/core/dashboard/brand/SectionsFormManager.js delete mode 100644 solara/lib/core/dashboard/component/AddFieldSheet.js create mode 100644 solara/lib/core/dashboard/component/EditJsonSheet.js delete mode 100644 solara/lib/core/dashboard/handler/brand_configurations_manager.rb create mode 100644 solara/lib/core/doctor/schema/platform/global/resources_manifest.json create mode 100644 solara/lib/core/doctor/schema/platform/json_manifest.json create mode 100644 solara/lib/core/scripts/brand_configurations_manager.rb create mode 100644 solara/lib/core/scripts/json_manifest_processor.rb create mode 100644 solara/lib/core/scripts/resource_manifest_processor.rb create mode 100644 solara/lib/core/template/brands/global/resources_manifest.json create mode 100644 solara/lib/core/template/brands/json/Json-Manifest.md create mode 100644 solara/lib/core/template/brands/json/json_manifest.json create mode 100644 solara/lib/core/template/configurations.json diff --git a/Gemfile b/Gemfile index cfd9b74..b4e2a20 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,3 @@ source "https://rubygems.org" -gem 'solara', '0.2.4' -gem "rspec", "~>3.13.0" \ No newline at end of file +gemspec diff --git a/Gemfile.lock b/Gemfile.lock index ee1e85e..8ea1e78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,15 @@ +PATH + remote: . + specs: + solara (0.4.0) + cgi (~> 0.4.1) + colorize (~> 1.1.0) + json-schema (~> 4.3.1) + plist (~> 3.7.1) + thor (~> 1.0) + webrick (~> 1.8.1) + xcodeproj (~> 1.25.0) + GEM remote: https://rubygems.org/ specs: @@ -20,6 +32,7 @@ GEM nkf (0.2.0) plist (3.7.1) public_suffix (6.0.1) + rake (13.2.1) rexml (3.3.8) rspec (3.13.0) rspec-core (~> 3.13.0) @@ -30,18 +43,10 @@ GEM rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.1) - solara (0.2.4) - cgi (~> 0.4.1) - colorize (~> 1.1.0) - json-schema (~> 4.3.1) - plist (~> 3.7.1) - thor (~> 1.0) - webrick (~> 1.8.1) - xcodeproj (~> 1.25.0) thor (1.3.2) webrick (1.8.2) xcodeproj (1.25.1) @@ -53,16 +58,13 @@ GEM rexml (>= 3.3.6, < 4.0) PLATFORMS - arm64-darwin-22 - x64-mingw-ucrt - x64-mingw32 x86_64-darwin-23 - x86_64-linux - x86_64-mingw32 DEPENDENCIES + bundler (~> 2.0) + rake (~> 13.0) rspec (~> 3.13.0) - solara (= 0.2.4) + solara! BUNDLED WITH 2.5.18 diff --git a/solara/lib/core/brands/brand_switcher.rb b/solara/lib/core/brands/brand_switcher.rb index da819dc..aaddbc7 100644 --- a/solara/lib/core/brands/brand_switcher.rb +++ b/solara/lib/core/brands/brand_switcher.rb @@ -30,6 +30,9 @@ def start def switch BrandFontSwitcher.new(@brand_key).switch + ResourceManifestSwitcher.new(@brand_key).switch + JsonManifestSwitcher.new(@brand_key).switch + case @platform when Platform::Flutter IOSBrandSwitcher.new(@brand_key).switch @@ -42,6 +45,60 @@ def switch else raise ArgumentError, "Invalid platform: #{@platform}" end + + end + +end + +class ResourceManifestSwitcher + def initialize(brand_key) + @brand_key = brand_key + end + + def switch + Solara.logger.start_step("Process resource manifest: #{FilePath.resources_manifest}") + brand_resource_copier = ResourceManifestProcessor.new(@brand_key) + brand_resource_copier.copy + Solara.logger.debug("#{@brand_key} resources copied successfully according to the manifest: #{FilePath.resources_manifest}.") + Solara.logger.end_step("Process resource manifest: #{FilePath.resources_manifest}") + end + +end + +class JsonManifestSwitcher + def initialize(brand_key) + @brand_key = brand_key + @manifest_path = FilePath.brand_json_dir(brand_key) + end + + def switch + Solara.logger.start_step("Process JSON manifest: #{@manifest_path}") + + + case SolaraSettingsManager.instance.platform + when Platform::Flutter + process_maifest(Language::Dart, FilePath.flutter_lib_artifacts) + process_maifest(Language::Dart, FilePath.brand_global_json_dir) + when Platform::IOS + process_maifest(Language::Swift, FilePath.ios_project_root_artifacts) + process_maifest(Language::Swift, FilePath.brand_global_json_dir) + when Platform::Android + process_maifest(Language::Kotlin, FilePath.android_project_java_artifacts ) + process_maifest(Language::Kotlin, FilePath.brand_global_json_dir) + else + raise ArgumentError, "Invalid platform: #{@platform}" + end + + Solara.logger.end_step("Process JSON manifest: #{@manifest_path}") + end + + def process_maifest(language, output_path) + processor = JsonManifestProcessor.new( + @manifest_path, + language, + output_path + ) + processor.process end end @@ -225,7 +282,7 @@ def generate_swift def generate_theme(name, language, platform) Solara.logger.start_step("Generate #{name} for #{platform}") - theme_manager = ThemeGeneratorManager.new(FilePath.brand_theme(@brand_key)) + theme_manager = ThemeGenerator.new(FilePath.brand_theme(@brand_key)) theme_manager.generate(language, FilePath.generated_config(name, platform)) Solara.logger.end_step("Generate #{name} for #{platform}") diff --git a/solara/lib/core/dashboard/brand/BrandDetail.js b/solara/lib/core/dashboard/brand/BrandDetail.js index 163b9dc..2a746a3 100644 --- a/solara/lib/core/dashboard/brand/BrandDetail.js +++ b/solara/lib/core/dashboard/brand/BrandDetail.js @@ -2,6 +2,37 @@ import BrandDetailModel from './BrandDetailModel.js'; import BrandDetailView from './BrandDetailView.js'; import BrandDetailController from './BrandDetailController.js'; +const modeToggle = document.getElementById('modeToggle'); +const body = document.body; +const icon = modeToggle.querySelector('i'); + +function applyMode(mode) { + if (mode === 'dark') { + body.classList.add('dark-mode'); + icon.classList.remove('fa-sun'); + icon.classList.add('fa-moon'); + } else { + body.classList.remove('dark-mode'); + icon.classList.remove('fa-moon'); + icon.classList.add('fa-sun'); + } +} + +const savedMode = localStorage.getItem('mode'); +if (savedMode) { + applyMode(savedMode); +} else { + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + applyMode(systemPrefersDark ? 'dark' : 'light'); +} + +modeToggle.addEventListener('click', () => { + const currentMode = body.classList.contains('dark-mode') ? 'dark' : 'light'; + const newMode = currentMode === 'dark' ? 'light' : 'dark'; + applyMode(newMode); + localStorage.setItem('mode', newMode); +}); + window.onload = function () { document.getElementById('loadingOverlay').style.display = 'none'; }; @@ -20,10 +51,11 @@ window.addEventListener('scroll', function () { } lastScrollTop = scrollTop; }); - + document.addEventListener('DOMContentLoaded', async () => { const model = new BrandDetailModel(); const view = new BrandDetailView(model); const controller = new BrandDetailController(model, view); await controller.initializeApp(); -}); \ No newline at end of file +}); + diff --git a/solara/lib/core/dashboard/brand/BrandDetailController.js b/solara/lib/core/dashboard/brand/BrandDetailController.js index a00c22f..86aa00d 100644 --- a/solara/lib/core/dashboard/brand/BrandDetailController.js +++ b/solara/lib/core/dashboard/brand/BrandDetailController.js @@ -1,15 +1,10 @@ import {DataSource} from './BrandDetailModel.js'; -import InfoPlistStringCatalogManager from "./InfoPlistStringCatalogManager.js"; class BrandDetailController { constructor(model, view) { this.model = model; this.view = view; - this.onSectionChanged = this.onSectionChanged.bind(this); - this.deleteField = this.deleteField.bind(this); this.initializeEventListeners(); - this.view.setOnSectionChangedHandler(this.onSectionChanged.bind(this)); - this.view.setOnDeleteFieldHandler(this.deleteField.bind(this)); } async initializeApp() { @@ -80,7 +75,7 @@ class BrandDetailController { async addNewBrand() { this.view.showOnboardBrandForm(async (key, name) => { - const configurations = await this.model.createNewBrandConfogurations() + const configurations = await this.model.createNewBrandConfigurations() await this.addBrand(key, name, configurations) }) } @@ -97,10 +92,10 @@ class BrandDetailController { await this.onLoadSections(response.result); const {isCurrentBrand, contentChanged} = await this.model.fetchCurrentBrand(); - if (!isCurrentBrand) { + if (isCurrentBrand) { + this.view.setupSyncBrandButton(contentChanged ? '#ff4136' : '#4A90E2'); + } else { this.view.showSwitchButton(); - } else if (contentChanged) { - this.view.showApplyChangesButton(); } await this.checkBrandHealth(); @@ -128,7 +123,12 @@ class BrandDetailController { window.location.href = `../brands/brands.html?source=${this.model.source}`; }); - document.getElementById('applyChangesButton').addEventListener('click', () => this.switchToBrand()); + document.getElementById('syncBrandButton').addEventListener( + 'click', + async () => { + await this.switchToBrand() + await this.view.toast("Synced successfully") + }); document.getElementById('switchButton').addEventListener('click', () => this.switchToBrand()); this.view.exportBrandBtn.addEventListener('click', () => this.exportBrand()); @@ -141,188 +141,37 @@ class BrandDetailController { const sectionItems = configuraationsResult.configurations - for (let i = 0; i < sectionItems.length; i++) { - const sectionData = sectionItems[i]; - - if (sectionData.key === 'theme.json') { - this.createThemeSections(sectionData) - } else if (sectionData.key === 'InfoPlist.xcstrings') { - this.createSection( - sectionData.key, - sectionData, - new InfoPlistStringCatalogManager(sectionData.content).extractLocalizations(), - sectionData.name, - sectionData.inputType) - } else { - this.createSection(sectionData.key, sectionData, sectionData.content, sectionData.name, sectionData.inputType) - } - } + this.view.sectionsFormManager.display( + sectionItems, + (section, container) => { + this.onSectionChanged(section, container) + }) + } catch (error) { console.error('Error loading configurations:', error); alert(error.message); } } - createThemeSections(sectionData) { - this.createSection(`${sectionData.key}_colors`, - sectionData, - sectionData.content.colors, - 'Theme Colors', - 'color', - 'colors') - this.createSection(`${sectionData.key}_typography`, - sectionData, - sectionData.content.typography, - 'Theme Typography', - 'text', - 'typography') - this.createSection(`${sectionData.key}_spacing`, - sectionData, - sectionData.content.spacing, - 'Theme Spacing', 'text', - 'spacing') - this.createSection( - `${sectionData.key}_borderRadius`, - sectionData, - sectionData.content.borderRadius, - 'Theme Border Radius', - 'text', - 'borderRadius') - this.createSection( - `${sectionData.key}_elevation`, - sectionData, - sectionData.content.elevation, - 'Theme Elevation', - 'text', - 'elevation') - } - - createSection(id, sectionData, content, sectionName, inputType, propertiesGroupName = null) { - const sectionElement = this.view.createSection(sectionData.key, sectionName, inputType); - sectionElement.id = id; - sectionElement.dataset.propertiesGroupName = propertiesGroupName - - this.view.sectionsContainer.appendChild(sectionElement); - - this.view.populateJsonFields(sectionData, sectionElement, content, inputType); + async onSectionChanged(section, container) { + if (this.model.isRemote()) return - const addButton = document.createElement('button'); - addButton.textContent = 'Add Field'; - addButton.className = 'add-field-btn'; - addButton.onclick = () => this.addNewField(sectionData, sectionElement, inputType); - sectionElement.appendChild(addButton); - } - - async onSectionChanged(sectionItem, sectionElement) { try { - const configuration = await this.getSectionData(sectionElement.dataset.key) - await this.model.saveSection(sectionItem, configuration); - this.view.showApplyChangesButton(); + await this.model.saveSection(container.dataset.key, section.content); await this.checkBrandHealth(); + await this.switchToBrand(true) } catch (error) { console.error('Error saving section:', error); alert(error.message); } } - collectJsonData(container) { - const data = {}; - const level = container.dataset.level - - const jsonObjects = container.querySelectorAll(`.json-object-${level}`); - jsonObjects.forEach(jsonObject => { - const jsonObjectLabel = jsonObject.querySelector('label').textContent; - data[jsonObjectLabel] = this.collectJsonData(jsonObject); - - }); - - const group = container.querySelectorAll(`.json-array-${level}`); - group.forEach(arrayItem => { - const label = arrayItem.querySelector('label').textContent; - const items = arrayItem.querySelectorAll(`.json-array-item-${level}`); - const indexed = arrayItem.querySelectorAll(`.json-array-item-indexed-${level}`).length !== 0; - - data[label] = Array.from(items).map((item, index) => { - const values = this.collectJsonData(item) - if (indexed) { - return values[index] - } - return values; - }); - }); - - const result = this.collectJsonFields(container, level); - - Object.keys(data).forEach(key => { - result[key] = data[key]; - }); - return result - } - - collectJsonFields(container, level) { - const result = {}; - - const inputFields = container.querySelectorAll(`.json-field-${level}`); - inputFields.forEach(inputField => { - const values = this.getInputFieldsData(inputField); - Object.keys(values).forEach(key => { - result[key] = values[key]; - }); - }) - return result - } - - getInputFieldsData(container) { - const data = {}; - - const inputs = container.querySelectorAll('input'); - inputs.forEach(input => { - if (input.type === 'checkbox') { - data[input.id] = input.checked; - } else { - let value = input.value; - if (input.type === 'color') { - value = `#${value.substring(1).toUpperCase()}`; - } else if (!isNaN(value) && value.trim() !== '') { - // Convert to number if it's a valid number string - value = parseFloat(value); - } - data[input.id] = value; - } - }); - - return data; - } - - addNewField(sectionItem, sectionElement, inputType) { - this.view.showAddFieldForm(sectionItem, sectionElement, inputType); - } - - deleteField(sectionItem, fieldContainer) { - this.view.showConfirmationDialog( - 'Are you sure you want to delete this item?', - () => { - const sectionElement = fieldContainer.closest('.section'); - fieldContainer.remove(); - this.onSectionChanged(sectionItem, sectionElement); - } - ); - } - - getArrayValue(container) { - const arrayItems = container.querySelectorAll('.array-item-input'); - return Array.from(arrayItems).map(item => item.value); - } - - async switchToBrand() { + async switchToBrand(silentError = false) { try { await this.model.switchToBrand(); - const applyChangesButton = document.getElementById('applyChangesButton'); - applyChangesButton.style.display = 'none'; - location.reload(); } catch (error) { console.error('Error switching to brand:', error); - alert(error.message); + if (!silentError) alert(error.message); } } @@ -350,72 +199,13 @@ class BrandDetailController { } } - async getSectionData(sectionKey) { - try { - const container = this.view.sectionsContainer; - - const sectionElements = Array.from(container.querySelectorAll('.section')) - .filter(element => element.dataset.key === sectionKey); - - if (sectionElements.length === 1) { - return this.collectJsonData(sectionElements[0]) - } - - const configurations = sectionElements.map(sectionElement => { - const propertiesGroupName = sectionElement.dataset.propertiesGroupName; - const config = this.collectJsonData(sectionElement); - - if (propertiesGroupName !== "null") { - return {[propertiesGroupName]: config}; - } else { - return config; - } - }); - - return configurations.reduce((acc, config) => { - const key = Object.keys(config)[0]; // Get the key from the configuration item - acc[key] = config[key]; // Assign the value to the dynamic object - return acc; - }, {}); - - } catch (error) { - console.error('Error downloading brand:', error); - alert(error.message); - } - } - async exportBrand() { try { - const sectionsContainer = this.view.sectionsContainer; + const result = this.view.sectionsFormManager.data(); const brandKey = this.view.sectionsContainer.dataset.brandKey; const brandName = this.view.sectionsContainer.dataset.brandName; - const sectionElements = Array.from(sectionsContainer.querySelectorAll('.section')); - - const uniqueSections = new Map(); - - // The theme section has been divided into multiple categories (e.g., colors, typography). - // In the getSectionData function, we merge these sections. To prevent duplication when - // processing the sections, we will ensure that each section key is processed only once. - await Promise.all(sectionElements.map(async sectionElement => { - const key = sectionElement.dataset.key; - // Check if the key already exists in the map - if (!uniqueSections.has(key)) { - const configurations = await this.getSectionData(key); - uniqueSections.set( - key, { - key: key, - name: sectionElement.dataset.name, - inputType: sectionElement.dataset.inputType, - content: configurations - }); - } - })); - - // Convert the map values to an array - const result = Array.from(uniqueSections.values()); - this.model.exportBrand(brandKey, brandName, result); } catch (error) { diff --git a/solara/lib/core/dashboard/brand/BrandDetailModel.js b/solara/lib/core/dashboard/brand/BrandDetailModel.js index a101eb4..e53e65e 100644 --- a/solara/lib/core/dashboard/brand/BrandDetailModel.js +++ b/solara/lib/core/dashboard/brand/BrandDetailModel.js @@ -17,6 +17,14 @@ class BrandDetailModel { this.source = sourceFromUrl === DataSource.LOCAL ? DataSource.LOCAL : DataSource.REMOTE; } + isLocal() { + return this.source === DataSource.LOCAL + } + + isRemote() { + return this.source === DataSource.REMOTE + } + getQueryFromUrl(name) { return this.localSource.getQueryFromUrl(name); } @@ -29,10 +37,10 @@ class BrandDetailModel { return await this.localSource.fetchCurrentBrand(this.brandKey); } - async saveSection(sectionItem, configuration) { + async saveSection(key, configuration) { switch (this.source) { case DataSource.LOCAL: - return await this.localSource.saveSection(sectionItem, configuration, this.brandKey); + return await this.localSource.saveSection(key, configuration, this.brandKey); case DataSource.REMOTE: // Saving is not supported remotely. Instead, user can export the brand. return @@ -48,7 +56,7 @@ class BrandDetailModel { async checkBrandHealth() { switch (this.source) { case DataSource.LOCAL: - return await this.localSource.checkBrandHealth(this.brandKey); + return await this.localSource.checkBrandHealth(this.brandKey); case DataSource.REMOTE: // Checking health is not supported remotely yet. return @@ -57,8 +65,8 @@ class BrandDetailModel { } } - async createNewBrandConfogurations() { - return await this.remoteSource.createNewBrandConfogurations(); + async createNewBrandConfigurations() { + return await this.remoteSource.createNewBrandConfigurations(); } async createBrandConfigurationsFromDirectory(dirHandle) { diff --git a/solara/lib/core/dashboard/brand/BrandDetailView.js b/solara/lib/core/dashboard/brand/BrandDetailView.js index 7a1aded..ec271a2 100644 --- a/solara/lib/core/dashboard/brand/BrandDetailView.js +++ b/solara/lib/core/dashboard/brand/BrandDetailView.js @@ -1,6 +1,6 @@ import {DataSource} from './BrandDetailModel.js'; +import SectionsFormManager from './SectionsFormManager.js'; import '../component/OnboardBrandBottomSheet.js'; -import '../component/AddFieldSheet.js'; import '../component/ConfirmationDialog.js'; import '../component/MessageBottomSheet.js'; @@ -15,7 +15,6 @@ class BrandDetailView { this.addBrandContainer = document.getElementById('add-brand-container'); this.sectionsContainer = document.getElementById('sections'); - this.addFieldSheet = document.getElementById('addFieldSheet'); this.confirmationDialog = document.getElementById('confirmationDialog'); this.messageBottomSheet = document.getElementById('messageBottomSheet'); @@ -24,8 +23,9 @@ class BrandDetailView { this.addNewBrandBtn = document.getElementById('newBrandBtn'); this.exportBrandBtn = document.getElementById('exportBrandBtn'); this.allBrandsButton = document.getElementById('allBrandsButton'); - + this.syncBrandButton = document.getElementById('syncBrandButton'); this.onboardSheet = document.getElementById('onboardBottomSheet'); + this.sectionsFormManager = new SectionsFormManager(); this.initializeApp(); } @@ -67,199 +67,12 @@ class BrandDetailView { } } - createSection(key, name, inputType) { - const section = document.createElement('div'); - section.className = 'section'; - - section.dataset.key = key - section.dataset.name = name - section.dataset.inputType = inputType - - const titleContainer = document.createElement('div'); - titleContainer.className = 'section-title-container'; - - const title = document.createElement('h2'); - title.textContent = name; - titleContainer.appendChild(title); - - const subtitleElement = document.createElement('p'); - subtitleElement.className = 'section-subtitle'; - subtitleElement.textContent = key; - titleContainer.appendChild(subtitleElement); - - section.appendChild(titleContainer); - - return section; - } - - populateJsonFields(data, container, content, inputType, level = 0) { - container.dataset.key = data.key - container.dataset.level = `${level}` - - for (const [key, value] of Object.entries(content)) { - if (Array.isArray(value)) { - this.populateJsonArray(key, value, data, container, content, inputType, level) - continue - } - - if (value !== null && typeof value === 'object') { - this.populateJsonObject(key, value, data, container, content, inputType, level) - continue - } - - const fieldElement = this.createJsonField(data, key, value, inputType, level) - container.appendChild(fieldElement); - } - } - - populateJsonArray(key, value, data, container, content, inputType, level) { - const arrayContainer = document.createElement('div'); - arrayContainer.className = 'json-array'; - arrayContainer.classList.add(`json-array-${level}`); - - const labelContainer = document.createElement('div'); - labelContainer.className = 'json-array-label-group'; - const label = document.createElement('label'); - label.textContent = key; - labelContainer.appendChild(label); - - // TODO: to be implemented later - if (false) { - const addButton = document.createElement('button'); - addButton.className = 'add-array-item'; - addButton.textContent = '+'; - let lastItemIndex = value.length - 1 - addButton.addEventListener('click', () => { - const itemContainer = document.createElement('div'); - itemContainer.className = 'json-array-item'; - - lastItemIndex += 1 - let fieldKey = `${key}[${lastItemIndex}]` - - const indexed = container.querySelectorAll(`.json-array-item-indexed-${level}`).length !== 0; - if (indexed) { - fieldKey = lastItemIndex - } - const field = this.createJsonField(data, fieldKey, '', inputType, level + 1) - field.classList.add(`json-array-item-${level}`); - itemContainer.appendChild(field); - - arrayContainer.insertBefore(itemContainer, arrayContainer.lastElementChild); - - this.onSectionChanged(data, container.closest('.section')); - }); - } - arrayContainer.appendChild(labelContainer); - - value.forEach((item, index) => { - const itemContainer = document.createElement('div'); - itemContainer.className = 'json-array-item'; - itemContainer.classList.add(`json-array-item-${level}`); - if (typeof item === 'object' && item !== null) { - this.populateJsonFields(data, itemContainer, item, inputType, level + 1); - } else { - itemContainer.dataset.level = `${level + 1}` - const field = this.createJsonField(data, `${index}`, item, inputType, level + 1) - itemContainer.classList.add(`json-array-item-indexed-${level}`); - itemContainer.appendChild(field); - } - arrayContainer.appendChild(itemContainer); - }); - - // TODO: to be implemented later - // arrayContainer.appendChild(addButton); - container.appendChild(arrayContainer); - } - - populateJsonObject(key, value, data, container, content, inputType, level) { - const objectContainer = document.createElement('div'); - objectContainer.className = 'json-object'; - objectContainer.classList.add(`json-object-${level}`); - const objectLabel = document.createElement('label'); - objectLabel.className = 'json-object-title'; - objectLabel.textContent = key; - objectContainer.appendChild(objectLabel); - - this.populateJsonFields(data, objectContainer, value, inputType, level + 1); - - container.appendChild(objectContainer); - } - - createJsonField(data, key, value, inputType, level) { - const fieldInputType = typeof value === 'boolean' ? 'boolean' : inputType; - const container = document.createElement('div'); - container.className = 'input-group'; - const label = document.createElement('label'); - label.textContent = key; - container.appendChild(label); - - const inputWrapper = document.createElement('div'); - inputWrapper.className = 'input-wrapper'; - - if (fieldInputType === 'boolean') { - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.id = key; - checkbox.checked = value; - - const checkboxLabel = document.createElement('label'); - checkboxLabel.className = 'checkbox-label'; - checkboxLabel.htmlFor = key; - checkboxLabel.textContent = value ? 'True' : 'False'; - - checkbox.addEventListener('change', () => { - checkboxLabel.textContent = checkbox.checked ? 'True' : 'False'; - this.onSectionChanged(data, container.closest('.section')); - }); - - inputWrapper.appendChild(checkbox); - inputWrapper.appendChild(checkboxLabel); - } else { - const input = document.createElement('input'); - input.type = fieldInputType; - input.id = key; - - console.log(value) - if (fieldInputType === 'color') { - input.value = value.startsWith('#') ? value : `#${value.substring(4)}`; - } else { - input.value = value; - } - - inputWrapper.appendChild(input); - } - - const deleteIcon = document.createElement('span'); - deleteIcon.className = 'delete-icon'; - deleteIcon.textContent = '×'; - deleteIcon.onclick = () => this.onDeleteField(data, container); - inputWrapper.appendChild(deleteIcon); - - container.appendChild(inputWrapper); - - container.addEventListener('change', () => this.onSectionChanged(data, container.closest('.section'))); - - container.classList.add(`json-field-${level}`); - - return container; - } - - showAddFieldForm(data, sectionElement, inputType) { - this.addFieldSheet.show(inputType, (name, value) => { - const newField = this.createJsonField(data, name, value, inputType, 0); - sectionElement.insertBefore(newField, sectionElement.lastElementChild); - this.onSectionChanged(data, sectionElement); - }) - } - showConfirmationDialog(message, onConfirm) { this.confirmationDialog.showDialog(message, onConfirm); } - showApplyChangesButton() { - const applyChangesButton = document.getElementById('applyChangesButton'); - applyChangesButton.style.display = 'block'; - this.header.style.backgroundColor = '#ff4136'; + setupSyncBrandButton(color) { + this.syncBrandButton.style.display = 'block'; } showSwitchButton() { @@ -276,14 +89,6 @@ class BrandDetailView { this.messageBottomSheet.showMessage(message); } - setOnSectionChangedHandler(handler) { - this.onSectionChanged = handler; - } - - setOnDeleteFieldHandler(handler) { - this.onDeleteField = handler; - } - showOnboardBrandForm(onSubmit) { this.onboardSheet.show('Brand Details', 'Add Brand', onSubmit); } @@ -309,6 +114,17 @@ class BrandDetailView { this.onboardSheet.hide(); } + async toast(message) { + const toastElement = document.getElementById('toast'); + toastElement.textContent = message; + toastElement.style.display = 'block'; + + setTimeout(() => { + toastElement.style.display = 'none'; + }, 3000); + } + + } export default BrandDetailView; \ No newline at end of file diff --git a/solara/lib/core/dashboard/brand/SectionsFormManager.js b/solara/lib/core/dashboard/brand/SectionsFormManager.js new file mode 100644 index 0000000..086c211 --- /dev/null +++ b/solara/lib/core/dashboard/brand/SectionsFormManager.js @@ -0,0 +1,232 @@ +import '../component/EditJsonSheet.js'; + +class SectionsFormManager { + + constructor() { + this.sections = []; + this.sectionsContainer = document.getElementById('sections'); + } + + display(sections, onChange) { + this.sections = sections.map((section) => { + return new SectionItemManager( + section, + this.createSection(section), + onChange) + }) + this.sections.forEach((item => { + item.displayJSONCards() + })) + } + + createSection(section) { + const sectionElement = document.createElement('div'); + sectionElement.className = 'section'; + + sectionElement.dataset.key = section.key + sectionElement.dataset.name = section.name + + const titleContainer = document.createElement('div'); + titleContainer.className = 'section-title-container'; + + const title = document.createElement('h2'); + title.className = "section-title"; + title.textContent = section.name; + titleContainer.appendChild(title); + + sectionElement.appendChild(titleContainer); + + sectionElement.id = section.key; + + this.sectionsContainer.appendChild(sectionElement); + + return sectionElement + } + + data() { + const data = this.sections.map((section) => { + return section.section + }) + return Array.from(data); + } + +} + +class SectionItemManager { + constructor(section, container, onChange) { + this.section = section + this.container = container + this.onChange = onChange + this.editJsonSheet = document.getElementById('editJsonSheet'); + } + + displayJSONCards() { + let cardContent = this.container.querySelector('.card-content') + if (cardContent !== null) this.container.innerHTML = '' + this.container.appendChild(this.createCard(this.section.content, 'root', null, this.section.key)); + } + + createCard(obj, key, parent, cardTitle) { + const card = document.createElement('div'); + card.className = 'card'; + + const header = document.createElement('div'); + header.className = 'card-header'; + header.textContent = key === 'root' ? cardTitle : key; + header.onclick = () => { + if (key !== 'root') { + this.editKey(parent, key) + return + } + this.editJsonSheet.show( + JSON.stringify(this.section.content, null, 2), + cardTitle, + (value) => { + this.section.content = value + this.displayJSONCards() + this.notifyChange() + }) + }; + + const actions = document.createElement('div'); + actions.className = 'card-actions'; + + const addBtn = document.createElement('button'); + addBtn.className = 'add-property-btn'; + addBtn.innerHTML = ''; + addBtn.onclick = () => this.addProperty(obj); + actions.appendChild(addBtn); + + if (key !== 'root') { + const deleteCardBtn = document.createElement('button'); + deleteCardBtn.className = 'delete-btn'; + deleteCardBtn.innerHTML = ''; + deleteCardBtn.onclick = () => this.confirmDeleteProperty(parent, key); + actions.appendChild(deleteCardBtn); + } + + header.appendChild(actions); + card.appendChild(header); + + const content = document.createElement('div'); + content.className = 'card-content'; + + const isArray = Array.isArray(obj) + + for (const [k, v] of Object.entries(obj)) { + const item = document.createElement('div'); + + const cardValueContainer = document.createElement('div'); + cardValueContainer.className = 'card-value-container'; + item.appendChild(cardValueContainer); + + const itemKey = document.createElement('span'); + itemKey.className = 'card-key'; + itemKey.onclick = () => { + if (isArray) return + this.editKey(obj, k) + }; + cardValueContainer.appendChild(itemKey); + + if (typeof v === 'object' && v !== null) { + item.appendChild(this.createCard(v, k, obj, null)); + itemKey.textContent = isArray ? `${key}[${k}]` : '' + } else { + item.className = 'card-item'; + itemKey.textContent = k.replace(/_/g, ' ') + + if (this.isColorValue(v)) { + const itemValue = document.createElement('input'); + itemValue.type = 'color'; + itemValue.className = 'card-value'; + itemValue.value = v; + itemValue.onchange = () => this.updateValue(obj, k, itemValue.value); + cardValueContainer.appendChild(itemValue); + } else { + const itemValue = document.createElement('textarea'); + itemValue.className = 'card-value'; + itemValue.value = v; + itemValue.onchange = () => this.updateValue(obj, k, itemValue.value); + cardValueContainer.appendChild(itemValue); + } + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'delete-btn'; + deleteBtn.innerHTML = ''; + deleteBtn.onclick = () => this.confirmDeleteProperty(obj, k); + cardValueContainer.appendChild(deleteBtn); + } + + content.appendChild(item); + } + + card.appendChild(content); + return card; + } + + isColorValue(value) { + // Check if the value is a valid color (hex with opacity, RGBA, or RGB) + const hexPattern = /^#([0-9A-F]{3}){1,2}([0-9A-F]{2})?$/i; // 3, 6, or 8 hex digits + const rgbaPattern = /^rgba?\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}\s*,?\s*(0|1|0?\.\d+|1?\.\d+)\s*\)$/; + + return hexPattern.test(value) || rgbaPattern.test(value); + } + + editKey(obj, oldKey) { + const newKey = prompt('Edit property name:', oldKey); + if (newKey && newKey !== oldKey) { + obj[newKey] = obj[oldKey]; + delete obj[oldKey]; + this.displayJSONCards(); + } + this.notifyChange() + } + + updateValue(obj, key, value) { + try { + obj[key] = JSON.parse(value); + } catch { + obj[key] = value; + } + this.displayJSONCards(); + this.notifyChange() + } + + confirmDeleteProperty(obj, key) { + const confirmationDialog = document.getElementById('confirmationDialog'); + confirmationDialog.showDialog(`Are you sure you need to delete: ${key}?`, + async () => { + this.deleteProperty(obj, key) + }); + } + + deleteProperty(obj, key) { + if (Array.isArray(obj)) { + obj.splice(key, 1); + } else { + delete obj[key]; + } + this.displayJSONCards(); + this.notifyChange() + } + + addProperty(obj) { + if (Array.isArray(obj)) { + obj.push(''); + } else { + const key = prompt('Enter new property name:'); + if (key) { + obj[key] = ''; + } + } + this.displayJSONCards(); + this.notifyChange() + } + + notifyChange() { + this.onChange(this.section, this.container) + } +} + + +export default SectionsFormManager; diff --git a/solara/lib/core/dashboard/brand/brand.html b/solara/lib/core/dashboard/brand/brand.html index d50e514..b0a9d19 100644 --- a/solara/lib/core/dashboard/brand/brand.html +++ b/solara/lib/core/dashboard/brand/brand.html @@ -15,7 +15,34 @@ --border-color: #E1E4E8; --delete-color: #dc3545; --field-shadow: 0 1.4px 3.5px rgba(0, 0, 0, 0.1); + + --card-item-bg: rgba(255, 255, 255, 0.05); + --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + --section-shadow: 0 1.4px 7px rgba(0, 0, 0, 0.1); + --delete-btn-color: #ff6b6b; + --delete-btn-hover: #ff4757; + --add-property-btn-color: #5ecd73; + --logo-shadow-color: rgba(255, 255, 255, 0.2); + --hover: rgba(0, 123, 255, 0.5); + } + + body.dark-mode { + --primary-color: #2C3E50; + --secondary-color: #34495E; + --background-color: #1A1A1A; + --text-color: #F5F5F5; + --border-color: #4A4A4A; + --delete-color: #E74C3C; + --field-shadow: 0 1.4px 3.5px rgba(255, 255, 255, 0.1); + --card-item-bg: rgba(255, 255, 255, 0.1); + --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + --section-shadow: 0 4px 6px rgba(255, 255, 255, 0.1); + --delete-btn-color: #ff8f8f; + --delete-btn-hover: #ff6b6b; + --add-property-btn-color: #7eed8e; + --hover: rgba(255, 255, 255, 0.2); } + body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); @@ -24,7 +51,23 @@ margin: 0; padding-top: 77px; font-size: 12.6px; + transition: background-color 0.3s, color 0.3s; + } + + .mode-toggle { + background: none; + border: none; + color: white; + font-size: 1.5em; + cursor: pointer; + margin-left: 15px; + transition: color 0.3s; + } + + .mode-toggle:hover { + color: var(--hover); } + .container { display: table; width: 100%; @@ -66,17 +109,15 @@ } .index li { padding: 5.6px; - border-bottom: 1px solid #eee; margin-bottom: 7px; text-align: left; } - .index li:last-child { border-bottom: none; } .index a { - color: #0066cc; + color: var(--text-color); text-decoration: none; } .index a:hover { @@ -85,67 +126,30 @@ .index-item { list-style: none; padding: 7px; - background-color: white; - border-radius: 3.5px; - box-shadow: 0 1.4px 3.5px rgba(0, 0, 0, 0.2); - transition: box-shadow 0.3s; + font-size: 14px; } .index-item:hover { - box-shadow: 0 2.8px 7px rgba(0, 0, 0, 0.3); - } - .sections { - width: 90%; - padding: 14px; + background-color: var(--hover); } h1 { margin: 0; font-size: 1.75em; } - .section { - background-color: white; - border-radius: 5.6px; - box-shadow: 0 1.4px 7px rgba(0, 0, 0, 0.1); - margin-bottom: 21px; - padding: 14px; - } + h2 { - color: var(--primary-color); + color: var(--text-color); border-bottom: 1.4px solid var(--border-color); padding-bottom: 7px; margin-top: 0; font-size: 1.4em; } - .input-group { - min-width: 80%; - margin-bottom: 10px; - display: flex; - align-items: center; - background-color: white; - border-radius: 5.6px; - box-shadow: var(--field-shadow); - padding: 7px; - transition: box-shadow 0.3s ease; - } - .input-group:hover { - box-shadow: 0 2.8px 5.6px rgba(0, 0, 0, 0.15); - } - .input-wrapper { - display: flex; - align-items: center; - flex-grow: 1; - } - .input-wrapper input[type="checkbox"] { - margin-right: 7px; - flex-grow: 0; - } - .checkbox-label { - flex-grow: 1; - } - .input-wrapper label[for] { - margin-right: 7px; + + h3 { + color: var(--primary-color); } + label { display: inline-block; margin-right: 7px; @@ -154,12 +158,11 @@ flex-shrink: 0; } input, select { - flex-grow: 1; - padding: 7px; - border: 0.7px solid var(--border-color); - border-radius: 2.8px; - font-size: 11.2px; + background-color: var(--background-color); + color: var(--text-color); + border: 1px solid var(--border-color); } + input[type="color"] { height: 35px; padding: 1.4px; @@ -175,59 +178,7 @@ transition: background-color 0.3s ease; } button:hover { - background-color: #3CC2A3; - } - - .delete-icon { - color: var(--delete-color); - cursor: pointer; - font-weight: bold; - font-size: 14px; - line-height: 1; - padding: 7px 10.5px; - margin-left: 7px; - border-radius: 2.8px; - transition: background-color 0.3s ease; - } - .delete-icon:hover { - color: #fff; - background-color: var(--delete-color); - } - .section-title-container { - margin-bottom: 0.7em; - } - .section-title-container h2 { - margin-bottom: 0.14em; - } - .section-subtitle { - font-size: 1.0em; - color: #666; - margin-bottom: 22.4px; - } - - @media (max-width: 768px) { - .sections { - padding: 7px; - } - .input-group { - flex-direction: column; - align-items: flex-start; - } - label { - margin-bottom: 3.5px; - } - .input-wrapper { - width: 100%; - } - } - - .add-array-item { - background-color: #4CAF50; - color: white; - border: none; - padding: 3.5px 7px; - border-radius: 2.8px; - cursor: pointer; + background-color: var(--hover); } .logo { @@ -242,6 +193,7 @@ } .header-container { + width: 100%; display: flex; align-items: center; justify-content: center; @@ -258,7 +210,6 @@ align-items: center; justify-content: center; } - header { background-color: var(--primary-color); color: white; @@ -282,11 +233,6 @@ transform: translateY(-100%); } - h1 { - margin: 0; - font-size: 1.75em; - } - .action-buttons { width: 100%; background-color: var(--primary-color); @@ -296,36 +242,14 @@ transition: background-color 0.3s ease, opacity 0.3s ease; } - .add-field-btn { - min-width: 105px; - background-color: var(--primary-color); - color: white; - margin: 7px; - font-size: 12.6px; - transition: background-color 0.3s ease, opacity 0.3s ease; - } - - .apply-changes-button { - width: 100%; - background-color: #ff4136; - color: white; - margin: 7px; - font-size: 12.6px; - transition: background-color 0.3s ease, opacity 0.3s ease; - } - .action-buttons button:hover { - background-color: #3A7BC8; + color: var(--hover); } #switchButton { display: none; } - #applyChangesButton { - display: none; - } - #error-button { position: fixed; bottom: 14px; @@ -385,7 +309,8 @@ left: 50%; transform: translate(-50%, -50%); text-align: center; - background-color: white; + background-color: var(--background-color); + color: var(--text-color); padding: 28px; border-radius: 14px; box-shadow: 0 2.8px 14px rgba(0, 0, 0, 0.1); @@ -412,7 +337,7 @@ .add-brand-container h2 { font-size: 12.6px; - color: #333; + color: var(--text-color); margin-bottom: 21px; animation: slideDown 0.5s ease-out 0.3s both; } @@ -434,13 +359,12 @@ } .add-brand-container button:hover { - background-color: #3A7BC8; transform: scale(1.05); } .button-message { font-size: 12.6px; - color: #666; + color: var(--text-color); margin-bottom: 3.5px; opacity: 0; animation: fadeInMessage 0.5s ease-out forwards; @@ -482,7 +406,7 @@ left: 0; width: 100%; height: 100%; - background-color: black; + background-color: var(--background-color); display: flex; align-items: center; justify-content: center; @@ -493,69 +417,155 @@ width: 105px; height: 105px; margin-right: 14px; - filter: drop-shadow(2.1px 2.1px 2.1px rgba(0, 0, 0, 0.3)); + filter: drop-shadow(2.1px 2.1px 2.1px var(--logo-shadow-color)); transition: transform 0.3s ease; } - .json-object { - margin-bottom: 10px; - border: 3px solid #1e88e5; /* Blue border */ - padding: 10px; + .sections { + width: 90%; + padding: 14px; + } + .section { + background-color: var(--background-color); + border-radius: 5.6px; + box-shadow: var(--section-shadow); + margin-bottom: 21px; + padding: 14px; + } + .section-title { + color: var(--text-color); + } + .section-title-container { + margin-bottom: 0.7em; + } + .section-title-container h2 { + margin-bottom: 0.14em; + } + + @media (max-width: 768px) { + .sections { + padding: 7px; + } + label { + margin-bottom: 3.5px; + } + } + .card { + background-color: var(--background-color); + color: var(--text-color); } - .json-object-title { + .card-header { + display: flex; + align-items: center; font-weight: bold; - flex-shrink: 0; - color: black; - font-size: 16px; margin-bottom: 10px; } - - .json-array { - border-left: 2px solid #ff9800; /* Orange border */ - padding: 10px; + .card-content { + margin-left: 15px; } - - .json-array-item { - margin-left: 20px; + .card-item { + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 5px; + margin-bottom: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; + background: var(--card-item-bg); + border-radius: 10px; + box-shadow: var(--card-shadow); } - - .json-array-label-group { - min-width: 80%; - margin-bottom: 17.5px; + .card-item:hover { + transform: translateY(-1.5px); + } + .card-key { + min-width: 20%; + font-weight: bold; + cursor: pointer; + text-align: left; display: flex; + justify-content: flex-start; + padding-right: 10px; + font-size: 14px; + margin-bottom: 5px; + margin-right: 10px; + hyphens: auto; + word-break: break-word; + } + .card-value-container { + display: flex; + flex-direction: row; align-items: center; - background-color: white; - border-radius: 5.6px; - box-shadow: var(--field-shadow); - padding: 7px; - transition: box-shadow 0.3s ease; + width: 100%; } - .json-array-label-group:hover { - box-shadow: 0 2.8px 5.6px rgba(0, 0, 0, 0.15); + .card-value { + flex: 1 1 80%; + width: 100%; + display: flex; + min-height: 25px; + border: 1px solid var(--border-color); + border-radius: 3px; + margin-bottom: 5px; + resize: vertical; + flex-direction: column; + padding-top: 15px; + padding-left: 10px; + padding-right: 10px; + background-color: var(--background-color); + color: var(--text-color); + } + .card-actions { + display: flex; + align-items: center; + justify-content: flex-start; + } + .delete-btn { + background-color: transparent; + color: var(--delete-btn-color); + border: none; + cursor: pointer; + font-size: 16px; + margin-left: 5px; + } + .delete-btn:hover { + color: var(--delete-btn-hover); + } + .add-property-btn { + background: none; + border: none; + color: var(--add-property-btn-color); + cursor: pointer; + font-size: 16px; } - - +
+
+ +
+
- + @@ -581,11 +591,11 @@

Solara Logo

Solara simplifies the management of your brand configurations, allowing you to access and update them anytime, anywhere.

You can select a JSON file containing brand configurations that were exported using Solara.
- -
Alternatively, upload from a folder that includes the brand's JSON files.
- -
You also have the option to create new brand configurations.
- + +
Alternatively, upload from a folder that includes the brand's JSON files.
+ +
You also have the option to create new brand configurations.
+
@@ -601,7 +611,7 @@

Solara simplifies the management of your brand configurations, allowing you - + diff --git a/solara/lib/core/dashboard/brand/source/BrandLocalSource.js b/solara/lib/core/dashboard/brand/source/BrandLocalSource.js index 3acd7f3..b1e1b29 100644 --- a/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +++ b/solara/lib/core/dashboard/brand/source/BrandLocalSource.js @@ -15,7 +15,6 @@ class BrandLocalSource { } const url = `/brand/details?brand_key=${encodeURIComponent(brandKey)}`; - console.log('Fetching configurations from:', url); const response = await fetch(url); const result = await response.json(); @@ -44,13 +43,13 @@ class BrandLocalSource { } } - async saveSection(sectionItem, configuration, brandKey) { + async saveSection(key, configuration, brandKey) { if (this.savingInProgress) return; this.savingInProgress = true; const dataToSend = { brand_key: brandKey, - key: sectionItem.key, + key: key, data: configuration }; @@ -69,7 +68,6 @@ class BrandLocalSource { throw new Error(result.error); } - console.log(`${sectionItem.name} configuration saved successfully!`); return true; } catch (error) { console.error('Error saving configuration:', error); @@ -95,7 +93,6 @@ class BrandLocalSource { throw new Error(result.error); } - console.log('Switch to brand result:', result); return true; } catch (error) { console.error('Error switching to brand:', error); diff --git a/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js b/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js index c95c09b..66951f5 100644 --- a/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +++ b/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js @@ -4,124 +4,36 @@ class BrandRemoteSource { constructor() { } - async createNewBrandConfogurations() { - const configurations_template = ` - [ - { - "key": "theme.json", - "name": "Theme Configuration", - "inputType": "color", - "content": { - "colors": { - "primary": "#CAAC16", - "secondary": "#5AC8FA", - "background": "#FFFFFF", - "surface": "#F2F2F7", - "error": "#FF3B30", - "onPrimary": "#FFFFFF", - "onSecondary": "#000000", - "onBackground": "#000000", - "onSurface": "#000000", - "onError": "#FFFFFF" - }, - "typography": { - "fontFamily": { - "regular": "", - "medium": "", - "bold": "" - }, - "fontSize": { - "small": 12, - "medium": 16, - "large": 20, - "extraLarge": 24 + async createNewBrandConfigurations() { + const url = 'https://raw.githubusercontent.com/Solara-Kit/Solara/refs/heads/develop/solara/lib/core/template/configurations.json'; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error('Network response was not ok: ' + response.statusText); + } + + const configurations = await response.json(); + const contentPromises = configurations.configurations.map(async (config) => { + const contentResponse = await fetch(config.url); + if (!contentResponse.ok) { + throw new Error('Failed to fetch content for ' + config.key); + } + const content = await contentResponse.json(); + return { + key: config.key, + name: config.name, + content: content + }; + }); + + // Wait for all content fetch promises to resolve + return await Promise.all(contentPromises); + + } catch (error) { + console.error('There was a problem with the fetch operation:', error); + return null; // Return null in case of error } - }, - "spacing": { - "small": 8, - "medium": 16, - "large": 24, - "extraLarge": 32 - }, - "borderRadius": { - "small": 4, - "medium": 8, - "large": 12 - }, - "elevation": { - "none": 0, - "low": 2, - "medium": 4, - "high": 8 - } - } - }, - { - "key": "brand_config.json", - "name": "Brand Configuration", - "inputType": "text", - "content": {} - }, - { - "key": "android_config.json", - "name": "Android Configuration", - "inputType": "text", - "content": { - "applicationId": "", - "versionName": "1.0.0", - "versionCode": 1, - "sourceSets": [] - } - }, - { - "key": "android_signing.json", - "name": "Android Signing", - "inputType": "text", - "content": { - "storeFile": "", - "keyAlias": "", - "storePassword": "", - "keyPassword": "" - } - }, - { - "key": "ios_config.json", - "name": "iOS Configuration", - "inputType": "text", - "content": { - "PRODUCT_BUNDLE_IDENTIFIER": "", - "MARKETING_VERSION": "1.0.0", - "BUNDLE_VERSION": 1, - "APL_MRCH_ID": "" - } - }, - { - "key": "ios_signing.json", - "name": "iOS Signing", - "inputType": "text", - "content": { - "CODE_SIGN_IDENTITY": "", - "DEVELOPMENT_TEAM": "", - "PROVISIONING_PROFILE_SPECIFIER": "", - "CODE_SIGN_STYLE": "Automatic", - "CODE_SIGN_ENTITLEMENTS": "" - } - }, - { - "key": "ios_signing.json", - "name": "iOS Signing", - "inputType": "text", - "content": { - "CODE_SIGN_IDENTITY": "", - "DEVELOPMENT_TEAM": "", - "PROVISIONING_PROFILE_SPECIFIER": "", - "CODE_SIGN_STYLE": "Automatic", - "CODE_SIGN_ENTITLEMENTS": "" - } - } -] -`; - return JSON.parse(configurations_template); } async getBrandConfigurationsJsonFromDirectory(dirHandle) { @@ -161,9 +73,7 @@ class BrandRemoteSource { if (!response.ok) { throw new Error('Network response was not ok: ' + response.statusText); } - const data = await response.json(); - console.log(data); - return data; // Return the data instead of null + return await response.json(); } catch (error) { console.error('There was a problem with the fetch operation:', error); return null; // Return null in case of error @@ -175,33 +85,27 @@ class BrandRemoteSource { const expectedFiles = [ { key: 'theme.json', - name: 'Theme Configuration', - input_type: 'color' + name: 'Theme Configuration' }, { key: 'brand_config.json', - name: 'Brand Configuration', - input_type: 'text' + name: 'Brand Configuration' }, { key: 'android_config.json', - name: 'Android Configuration', - input_type: 'text' + name: 'Android Configuration' }, { key: 'android_signing.json', - name: 'Android Signing', - input_type: 'text' + name: 'Android Signing' }, { key: 'ios_config.json', - name: 'iOS Configuration', - input_type: 'text' + name: 'iOS Configuration' }, { key: 'ios_signing.json', - name: 'iOS Signing', - input_type: 'text' + name: 'iOS Signing' } ]; @@ -212,7 +116,6 @@ class BrandRemoteSource { configList.push({ key: file.key, name: file.name, - inputType: file.input_type, content: JSON.parse(fileContent) }); } diff --git a/solara/lib/core/dashboard/brands/Brands.js b/solara/lib/core/dashboard/brands/Brands.js index 6b77977..07ab249 100644 --- a/solara/lib/core/dashboard/brands/Brands.js +++ b/solara/lib/core/dashboard/brands/Brands.js @@ -2,6 +2,37 @@ import BrandsModel from './BrandsModel.js'; import BrandsView from './BrandsView.js'; import BrandsController from './BrandsController.js'; +const modeToggle = document.getElementById('modeToggle'); +const body = document.body; +const icon = modeToggle.querySelector('i'); + +function applyMode(mode) { + if (mode === 'dark') { + body.classList.add('dark-mode'); + icon.classList.remove('fa-sun'); + icon.classList.add('fa-moon'); + } else { + body.classList.remove('dark-mode'); + icon.classList.remove('fa-moon'); + icon.classList.add('fa-sun'); + } +} + +const savedMode = localStorage.getItem('mode'); +if (savedMode) { + applyMode(savedMode); +} else { + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + applyMode(systemPrefersDark ? 'dark' : 'light'); +} + +modeToggle.addEventListener('click', () => { + const currentMode = body.classList.contains('dark-mode') ? 'dark' : 'light'; + const newMode = currentMode === 'dark' ? 'light' : 'dark'; + applyMode(newMode); + localStorage.setItem('mode', newMode); +}); + document.addEventListener('DOMContentLoaded', async () => { const model = new BrandsModel(); const view = new BrandsView(model.source); diff --git a/solara/lib/core/dashboard/brands/BrandsController.js b/solara/lib/core/dashboard/brands/BrandsController.js index f2e1154..d752734 100644 --- a/solara/lib/core/dashboard/brands/BrandsController.js +++ b/solara/lib/core/dashboard/brands/BrandsController.js @@ -24,27 +24,22 @@ class BrandsController { .addEventListener('input', (e) => this.filterBrands(e.target.value)); this.view.brandOptionsSheet.addEventListener('clone', (event) => { - console.log('Clone', event.detail); this.handleCloneOption(event) }); this.view.brandOptionsSheet.addEventListener('offboard', (event) => { - console.log('Offboard', event.detail); this.handleOffboardOption(event) }); this.view.brandOptionsSheet.addEventListener('doctor', (event) => { - console.log('Doctor', event.detail); this.handleDoctorOption(event) }); this.view.brandOptionsSheet.addEventListener('aliases', (event) => { - console.log('Aliases', event.detail); this.handleAliasesOption(event) }); this.view.brandOptionsSheet.addEventListener('settings', (event) => { - console.log('Settings', event.detail); this.handleSettingsOption(event) }); } diff --git a/solara/lib/core/dashboard/brands/BrandsView.js b/solara/lib/core/dashboard/brands/BrandsView.js index 4dbf2a7..ddac2c4 100644 --- a/solara/lib/core/dashboard/brands/BrandsView.js +++ b/solara/lib/core/dashboard/brands/BrandsView.js @@ -40,8 +40,8 @@ class BrandsView { }); const switchButton = brandItem.querySelector('.switch-button'); - if (isCurrent && brand.content_changed) { - switchButton.textContent = "Apply Changes"; + if (isCurrent) { + switchButton.textContent = "Sync"; switchButton.style.display = "block"; } else if (isCurrent && !brand.content_changed) { switchButton.style.display = "none"; diff --git a/solara/lib/core/dashboard/brands/brands.html b/solara/lib/core/dashboard/brands/brands.html index bdc9053..f763416 100644 --- a/solara/lib/core/dashboard/brands/brands.html +++ b/solara/lib/core/dashboard/brands/brands.html @@ -12,9 +12,34 @@ --background-color: #F5F7FA; --text-color: #333; --border-color: #E1E4E8; - --offboard-color: #dc3545; + --delete-color: #dc3545; --field-shadow: 0 1.4px 3.5px rgba(0, 0, 0, 0.1); + --card-item-bg: rgba(255, 255, 255, 0.05); + --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + --section-shadow: 0 1.4px 7px rgba(0, 0, 0, 0.1); + --delete-btn-color: #ff6b6b; + --delete-btn-hover: #ff4757; + --add-property-btn-color: #5ecd73; + --logo-shadow-color: rgba(255, 255, 255, 0.2); + --hover: rgba(0, 123, 255, 0.5); } + + body.dark-mode { + --primary-color: #2C3E50; + --secondary-color: #34495E; + --background-color: #1A1A1A; + --text-color: #F5F5F5; + --border-color: #4A4A4A; + --delete-color: #E74C3C; + --field-shadow: 0 1.4px 3.5px rgba(255, 255, 255, 0.1); + --card-item-bg: rgba(255, 255, 255, 0.1); + --section-shadow: 0 4px 6px rgba(255, 255, 255, 0.1); + --delete-btn-color: #ff8f8f; + --delete-btn-hover: #ff6b6b; + --add-property-btn-color: #7eed8e; + --hover: rgba(255, 255, 255, 0.2); + } + body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); @@ -33,14 +58,29 @@ margin: 0; font-size: 1.75em; } + + .mode-toggle { + background: none; + border: none; + color: white; + font-size: 1.5em; + cursor: pointer; + margin-left: 15px; + transition: color 0.3s; + } + + .mode-toggle:hover { + color: var(--hover); + } + .brand-list { max-width: 490px; margin: 0 auto; } .brand-item { - background-color: #fff; + background-color: var(--card-item-bg); border-radius: 8.4px; - box-shadow: 0 2.8px 4.2px rgba(0, 0, 0, 0.1); + box-shadow: var(--card-shadow); padding: 14px; display: flex; align-items: center; @@ -52,7 +92,6 @@ } .brand-item:hover { transform: translateY(-3.5px); - box-shadow: 0 4.2px 8.4px rgba(0, 0, 0, 0.15); } .brand-image { width: 56px; @@ -63,7 +102,8 @@ margin-right: 14px; border-radius: 8.4px; overflow: hidden; - box-shadow: 0 1.4px 3.5px rgba(0, 0, 0, 0.1); + box-shadow: var(--field-shadow); + padding: 5px; } .brand-image img { width: 100%; @@ -77,11 +117,11 @@ font-size: 14px; font-weight: bold; margin-bottom: 3.5px; - color: var(--primary-color); + color: var(--text-color); } .brand-key { font-size: 9.8px; - color: #7f8c8d; + color: var(--text-color); } .brand-actions { display: flex; @@ -100,18 +140,18 @@ min-width: 91px; } .switch-button:hover { - background-color: #3a7bc8; + background-color: var(--hover); } .overflow-menu { cursor: pointer; font-size: 14px; - color: #7f8c8d; + color: var(--text-color); transition: color 0.3s ease; padding: 7px; margin: -7px; } .overflow-menu:hover { - color: var(--primary-color); + color: var(--hover); } .current-brand, .brands-list { @@ -123,13 +163,7 @@ .current-brand h2, .brands-list h2 { margin: 0 0 14px 0; font-size: 1.26em; - color: var(--primary-color); - } - .section-title { - font-size: 1.05em; - color: var(--primary-color); - margin-bottom: 14px; - text-align: center; + color: var(--text-color); } .brands-list-header { display: flex; @@ -150,7 +184,7 @@ min-width: 91px; } .onboard-brand-button:hover { - background-color: #3a7bc8; + background-color: var(--hover); } .onboard-brand-form { display: flex; @@ -172,6 +206,8 @@ border: 1px solid var(--border-color); border-radius: 3.5px; font-size: 11.2px; + background-color: var(--background-color); + color: var(--text-color); } .question-icon { margin-left: 3.5px; @@ -194,13 +230,17 @@ align-items: center; justify-content: center; } + .header-content { + display: flex; + align-items: center; + justify-content: center; + } header { background-color: var(--primary-color); color: white; text-align: center; padding: 7px 0; - box-shadow: 0 1.4px 7px rgba(0, 0, 0, 0.1); display: flex; justify-content: center; align-items: center; @@ -217,21 +257,6 @@ .scroll-down header { transform: translateY(-100%); } - .logo { - width: 35px; - height: 35px; - margin-right: 14px; - filter: drop-shadow(2.1px 2.1px 2.1px rgba(0, 0, 0, 0.3)); - transition: transform 0.3s ease; - } - .logo:hover { - transform: scale(1.07); - } - h1 { - margin: 0; - font-size: 1.75em; - } - .search-container { margin-top: 14px; margin-bottom: 14px; @@ -245,13 +270,8 @@ font-size: 11.2px; width: 100%; max-width: 469px; - } - - .brands-list-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 14px; + background-color: var(--background-color); + color: var(--text-color); } #error-button { @@ -268,7 +288,7 @@ cursor: pointer; justify-content: center; align-items: center; - box-shadow: 0 1.4px 7px rgba(0, 0, 0, 0.2); + box-shadow: var(--section-shadow); transition: transform 0.2s; display: none; } @@ -295,14 +315,18 @@ - +
- -

Solara Dashboard

+
+ +

Solara Dashboard

+ +
-
-