@mtldev514/retro-portfolio-maker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +409 -0
  3. package/bin/cli.js +103 -0
  4. package/engine/admin/admin_api.py +221 -0
  5. package/engine/admin/scripts/backup.sh +116 -0
  6. package/engine/admin/scripts/config_loader.py +180 -0
  7. package/engine/admin/scripts/init.sh +141 -0
  8. package/engine/admin/scripts/manager.py +308 -0
  9. package/engine/admin/scripts/restore.sh +121 -0
  10. package/engine/admin/scripts/server.py +41 -0
  11. package/engine/admin/scripts/update.sh +321 -0
  12. package/engine/admin/scripts/validate_json.py +62 -0
  13. package/engine/admin.css +720 -0
  14. package/engine/admin.html +801 -0
  15. package/engine/fonts.css +37 -0
  16. package/engine/index.html +186 -0
  17. package/engine/js/config-loader.js +370 -0
  18. package/engine/js/config.js +173 -0
  19. package/engine/js/counter.js +17 -0
  20. package/engine/js/effects.js +97 -0
  21. package/engine/js/i18n.js +68 -0
  22. package/engine/js/init.js +107 -0
  23. package/engine/js/media.js +264 -0
  24. package/engine/js/render.js +282 -0
  25. package/engine/js/router.js +133 -0
  26. package/engine/js/sparkle.js +123 -0
  27. package/engine/js/themes.js +607 -0
  28. package/engine/style.css +2037 -0
  29. package/index.js +35 -0
  30. package/package.json +48 -0
  31. package/scripts/admin.js +63 -0
  32. package/scripts/build.js +142 -0
  33. package/scripts/init.js +369 -0
  34. package/scripts/init.js.bak +331 -0
  35. package/scripts/init.js.bak2 +331 -0
  36. package/scripts/init.js.bak3 +331 -0
  37. package/scripts/post-install.js +16 -0
  38. package/scripts/serve.js +54 -0
  39. package/templates/.env.example +10 -0
  40. package/templates/user-portfolio/.github/workflows/deploy.yml +57 -0
  41. package/templates/user-portfolio/config/app.json +36 -0
  42. package/templates/user-portfolio/config/categories.json +241 -0
  43. package/templates/user-portfolio/config/languages.json +15 -0
  44. package/templates/user-portfolio/config/media-types.json +59 -0
  45. package/templates/user-portfolio/data/painting.json +3 -0
  46. package/templates/user-portfolio/data/projects.json +3 -0
  47. package/templates/user-portfolio/lang/en.json +114 -0
  48. package/templates/user-portfolio/lang/fr.json +114 -0
@@ -0,0 +1,37 @@
1
+ /* ═══ Typography — independent of theme/style ═══ */
2
+
3
+ /* Base */
4
+ body {
5
+ font-size: 14px;
6
+ line-height: 1.5;
7
+ }
8
+
9
+ /* Gallery cards */
10
+ .gallery-item h3 {
11
+ font-size: 0.95em;
12
+ margin: 4px 5px 0;
13
+ line-height: 1.3;
14
+ }
15
+
16
+ .gallery-item .gallery-subtitle {
17
+ font-size: 0.85em;
18
+ margin: 1px 5px 0;
19
+ line-height: 1.3;
20
+ }
21
+
22
+ /* Dates — switch from monospace to primary font */
23
+ .item-date {
24
+ font-size: 0.75em;
25
+ font-family: var(--font-primary);
26
+ }
27
+
28
+ /* UI controls — bump from below-legibility sizes */
29
+ .sort-btn { font-size: 11px; }
30
+
31
+ /* Winamp — minimal bumps to preserve skin authenticity */
32
+ .winamp-title { font-size: 10px; }
33
+ .winamp-pl-item { font-size: 11px; }
34
+
35
+ /* Terminal — sharper text */
36
+ .terminal-body { font-size: 12px; }
37
+ .terminal-line { text-shadow: 0 0 2px var(--term-glow, rgba(0,255,0,0.2)); }
@@ -0,0 +1,186 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title data-i18n="header_title">alex a montreal</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="stylesheet" href="fonts.css">
10
+
11
+ <!-- Load configuration first (supports remote/local sources) -->
12
+ <script src="js/config-loader.js"></script>
13
+ <script src="js/i18n.js" defer></script>
14
+ <script src="js/themes.js" defer></script>
15
+ <script src="js/render.js" defer></script>
16
+ <script src="js/router.js" defer></script>
17
+ <script src="js/media.js" defer></script>
18
+ <script src="js/sparkle.js" defer></script>
19
+ <script src="js/effects.js" defer></script>
20
+ <!-- Initialize app after all modules loaded -->
21
+ <script src="js/init.js" defer></script>
22
+ </head>
23
+
24
+ <body>
25
+ <div id="rotate-overlay">
26
+ <div class="rotate-content">
27
+ <div class="rotate-icon">📺↩️</div>
28
+ <h2>ROTATE YOUR DEVICE!</h2>
29
+ <p>This website was designed for<br><b>DESKTOP COMPUTERS</b></p>
30
+ <p class="rotate-sub">Please turn your phone sideways<br>for the full retro experience!</p>
31
+ <div class="rotate-ascii">
32
+ ┌─────────────────┐<br>
33
+ │&nbsp;&nbsp;&nbsp;╔══════════╗&nbsp;&nbsp;&nbsp;│<br>
34
+ │&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;TURN&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;&nbsp;│<br>
35
+ │&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;&nbsp;ME!&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;&nbsp;│<br>
36
+ │&nbsp;&nbsp;&nbsp;╚══════════╝&nbsp;&nbsp;&nbsp;│<br>
37
+ └─────────────────┘<br>
38
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;⟲
39
+ </div>
40
+ <p class="rotate-blink">*** LANDSCAPE MODE REQUIRED ***</p>
41
+ </div>
42
+ </div>
43
+ <div class="container">
44
+ <header>
45
+ <div class="settings-switcher">
46
+ <button class="settings-btn">⚙</button>
47
+ <div class="settings-dropdown">
48
+ <div class="settings-section-label">Effects</div>
49
+ <div class="settings-option" onclick="togglePartyMode()">
50
+ <span id="party-mode-indicator">✨</span> Party Mode
51
+ </div>
52
+ <div class="settings-divider"></div>
53
+ <div class="settings-section-label">Theme</div>
54
+ <div class="settings-option" onclick="themes.changeTheme('jr16')"><span class="theme-icon">🌿</span>
55
+ JR-16</div>
56
+ <div class="settings-option" onclick="themes.changeTheme('beton')"><span
57
+ class="theme-icon">🌫️</span> Béton</div>
58
+ <div class="settings-option" onclick="themes.changeTheme('ciment')"><span
59
+ class="theme-icon">🪨</span> Ciment</div>
60
+ <div class="settings-option" onclick="themes.changeTheme('bubblegum')"><span
61
+ class="theme-icon">🍬</span> Bubble Gum</div>
62
+ <div class="settings-divider"></div>
63
+ <div class="settings-section-label">Language</div>
64
+ <div class="settings-option" onclick="i18n.changeLang('en')"><span class="lang-flag">🇬🇧🇨🇦</span>
65
+ English</div>
66
+ <div class="settings-option" onclick="i18n.changeLang('fr')"><span class="lang-flag">⚜️🇨🇦</span>
67
+ French</div>
68
+ </div>
69
+ </div>
70
+ <h1 id="page-title" data-i18n="header_title">alex a montreal</h1>
71
+ </header>
72
+
73
+ <div class="marquee-container">
74
+ <marquee scrollamount="5" data-i18n="marquee"></marquee>
75
+ </div>
76
+
77
+ <div class="content">
78
+ <aside class="sidebar">
79
+ <div class="winamp">
80
+ <div class="winamp-titlebar">
81
+ <span class="winamp-grip"></span>
82
+ <span class="winamp-title">Radyo</span>
83
+ <span class="winamp-grip"></span>
84
+ </div>
85
+ <div class="winamp-display">
86
+ <div class="winamp-time-row">
87
+ <span class="winamp-time" id="winamp-time">00:00</span>
88
+ <span class="winamp-time-sep">/</span>
89
+ <span class="winamp-duration" id="winamp-duration">00:00</span>
90
+ </div>
91
+ <div class="winamp-ticker">
92
+ <span class="radio-track-name" data-i18n="sidebar_radio_title">Your Radio</span> - Winamp 2.91
93
+ </div>
94
+ <div class="winamp-viz" id="winamp-viz"></div>
95
+ <div class="winamp-info">
96
+ <div class="winamp-bitrate"><span class="winamp-kbps">192</span>kbps</div>
97
+ <div class="winamp-freq"><span class="winamp-khz">44</span>kHz</div>
98
+ </div>
99
+ </div>
100
+ <div class="winamp-transport">
101
+ <button class="winamp-btn radio-prev" title="Previous"><svg class="icon" viewBox="0 0 24 24">
102
+ <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" transform="scale(-1,1) translate(-24,0)" />
103
+ </svg></button>
104
+ <button class="winamp-btn radio-playpause" title="Play"><svg class="icon" viewBox="0 0 24 24">
105
+ <path d="M8 5v14l11-7z" />
106
+ </svg></button>
107
+ <button class="winamp-btn radio-next" title="Next"><svg class="icon" viewBox="0 0 24 24">
108
+ <path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
109
+ </svg></button>
110
+ </div>
111
+ <div class="winamp-volume-row">
112
+ <svg class="icon winamp-vol-icon" viewBox="0 0 24 24">
113
+ <path d="M3 9v6h4l5 5V4L7 9H3z" />
114
+ </svg>
115
+ <input type="range" class="winamp-volume radio-volume" value="80" min="0" max="100">
116
+ <svg class="icon winamp-vol-icon" viewBox="0 0 24 24">
117
+ <path
118
+ d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
119
+ </svg>
120
+ </div>
121
+ <div class="winamp-seek-row">
122
+ <input type="range" class="winamp-seek" id="winamp-seek" value="0" min="0" max="100">
123
+ </div>
124
+ <div class="winamp-playlist-titlebar">
125
+ <span class="winamp-grip-sm"></span>
126
+ <span class="winamp-pl-title">PLAYLIST</span>
127
+ <span class="winamp-grip-sm"></span>
128
+ </div>
129
+ <div class="winamp-playlist" id="radio-tracklist">
130
+ <div class="winamp-pl-item winamp-pl-empty" data-i18n="sidebar_radio_loading">Loading tracks...
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </aside>
135
+
136
+ <main id="app">
137
+ <div id="filter-nav" class="filter-bar">
138
+ <button class="filter-btn active" data-filter="all" data-i18n="filter_all">All</button>
139
+ <button class="filter-btn" data-filter="painting" data-i18n="nav_painting">Painting</button>
140
+ <button class="filter-btn" data-filter="drawing" data-i18n="nav_drawing">Drawing</button>
141
+ <button class="filter-btn" data-filter="photography" data-i18n="nav_photo">Photography</button>
142
+ <button class="filter-btn" data-filter="sculpting" data-i18n="nav_sculpting">Sculpting</button>
143
+ <button class="filter-btn" data-filter="music" data-i18n="nav_music">Music</button>
144
+ <button class="filter-btn" data-filter="projects" data-i18n="nav_projects">Code</button>
145
+ <span class="sort-controls">
146
+ <button class="sort-btn active" data-sort="desc" title="Newest first">&#9660;</button>
147
+ <button class="sort-btn" data-sort="asc" title="Oldest first">&#9650;</button>
148
+ </span>
149
+ </div>
150
+ <!-- Gallery grid loaded by render.js -->
151
+ </main>
152
+ </div>
153
+
154
+ <footer class="footer-terminal">
155
+ <div class="terminal-window">
156
+ <div class="terminal-titlebar">
157
+ <div class="terminal-titlebar-btns">
158
+ <span class="terminal-tbtn terminal-tbtn-close"></span>
159
+ <span class="terminal-tbtn terminal-tbtn-min"></span>
160
+ <span class="terminal-tbtn terminal-tbtn-max"></span>
161
+ </div>
162
+ <span class="terminal-titlebar-text">user@host:~</span>
163
+ </div>
164
+ <div class="terminal-body">
165
+ <div class="terminal-line"><span class="terminal-prompt">$</span> <span
166
+ data-i18n="footer_copy">&copy; 2026 alex a montreal</span> &mdash; <a
167
+ href="https://github.com/mtldev514" target="_blank" title="GitHub"><svg class="icon"
168
+ viewBox="0 0 24 24">
169
+ <path
170
+ d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1.1-.8.1-.7.1-.7 1.2.1 1.9 1.3 1.9 1.3 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z" />
171
+ </svg> github</a> · <a href="https://www.linkedin.com/in/alexcatus/?locale=fr_FR"
172
+ target="_blank" title="LinkedIn"><svg class="icon" viewBox="0 0 24 24">
173
+ <path
174
+ d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z" />
175
+ </svg> linkedin</a> &mdash; <span class="terminal-dim"><span
176
+ data-i18n="footer_visitors">Visits:</span> <span
177
+ class="visitor-counter">......</span></span> <span class="terminal-cursor">█</span>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </footer>
182
+ </div>
183
+ <script src="js/counter.js"></script>
184
+ </body>
185
+
186
+ </html>
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Advanced Configuration Loader
3
+ * Supports loading from local directories OR remote GitHub repository
4
+ */
5
+
6
+ const ConfigLoader = {
7
+ source: null,
8
+ cache: {},
9
+
10
+ /**
11
+ * Initialize and load configuration source
12
+ */
13
+ async init() {
14
+ try {
15
+ // Check for override version from global config or query params
16
+ const urlParams = new URLSearchParams(window.location.search);
17
+ const overrideMode = urlParams.get('mode') || window.RETRO_CONFIG?.mode;
18
+ const overrideData = urlParams.get('dataDir') || window.RETRO_CONFIG?.dataDir;
19
+
20
+ // Load config-source.json to determine where to load from
21
+ let response;
22
+ try {
23
+ response = await fetch('config-source.json');
24
+ } catch (e) {
25
+ console.warn('⚠️ No config-source.json found, using defaults');
26
+ }
27
+
28
+ this.source = response ? await response.json() : {
29
+ mode: overrideMode || 'local',
30
+ local: { configDir: 'config', dataDir: overrideData || 'data', langDir: 'lang' }
31
+ };
32
+
33
+ if (overrideMode) this.source.mode = overrideMode;
34
+ if (overrideData) {
35
+ if (!this.source.local) this.source.local = {};
36
+ this.source.local.dataDir = overrideData;
37
+ }
38
+
39
+ console.log(`📦 Config mode: ${this.source.mode}`);
40
+
41
+ if (this.source.mode === 'remote' && this.source.remote.enabled) {
42
+ console.log(`🌐 Loading from remote: ${this.source.remote.repo}`);
43
+ return this.loadRemote();
44
+ } else if (this.source.mode === 'hybrid') {
45
+ console.log('🔄 Trying remote, fallback to local');
46
+ try {
47
+ return await this.loadRemote();
48
+ } catch (error) {
49
+ console.warn('⚠️ Remote failed, using local:', error.message);
50
+ return this.loadLocal();
51
+ }
52
+ } else {
53
+ console.log('💾 Loading from local directories');
54
+ return this.loadLocal();
55
+ }
56
+ } catch (error) {
57
+ console.error('❌ Error loading config-source.json:', error);
58
+ console.log('🔄 Falling back to local mode');
59
+ return this.loadLocal();
60
+ }
61
+ },
62
+
63
+ /**
64
+ * Load configuration from local directories
65
+ */
66
+ async loadLocal() {
67
+ const configDir = this.source?.local?.configDir || 'config';
68
+ const dataDir = this.source?.local?.dataDir || 'data';
69
+ const langDir = this.source?.local?.langDir || 'lang';
70
+
71
+ try {
72
+ const [app, languages, categories, mediaTypes] = await Promise.all([
73
+ fetch(`${configDir}/app.json`).then(r => r.json()),
74
+ fetch(`${configDir}/languages.json`).then(r => r.json()),
75
+ fetch(`${configDir}/categories.json`).then(r => r.json()),
76
+ fetch(`${configDir}/media-types.json`).then(r => r.json())
77
+ ]);
78
+
79
+ return {
80
+ app,
81
+ languages,
82
+ categories,
83
+ mediaTypes,
84
+ source: 'local',
85
+ paths: { configDir, dataDir, langDir }
86
+ };
87
+ } catch (error) {
88
+ console.error('❌ Failed to load local config:', error);
89
+ throw error;
90
+ }
91
+ },
92
+
93
+ /**
94
+ * Load configuration from remote GitHub repository
95
+ */
96
+ async loadRemote() {
97
+ if (!this.source?.remote?.enabled) {
98
+ throw new Error('Remote config not enabled');
99
+ }
100
+
101
+ const baseUrl = this.source.remote.baseUrl;
102
+
103
+ // Check cache first
104
+ if (this.source.cache?.enabled && this.isCacheValid()) {
105
+ console.log('💨 Using cached remote config');
106
+ return this.cache.data;
107
+ }
108
+
109
+ try {
110
+ const [app, languages, categories, mediaTypes] = await Promise.all([
111
+ this.fetchRemote(`${baseUrl}config/app.json`),
112
+ this.fetchRemote(`${baseUrl}config/languages.json`),
113
+ this.fetchRemote(`${baseUrl}config/categories.json`),
114
+ this.fetchRemote(`${baseUrl}config/media-types.json`)
115
+ ]);
116
+
117
+ const data = {
118
+ app,
119
+ languages,
120
+ categories,
121
+ mediaTypes,
122
+ source: 'remote',
123
+ paths: {
124
+ configDir: baseUrl + 'config',
125
+ dataDir: baseUrl + 'data',
126
+ langDir: baseUrl + 'lang'
127
+ }
128
+ };
129
+
130
+ // Cache the result
131
+ if (this.source.cache?.enabled) {
132
+ this.cache = {
133
+ data,
134
+ timestamp: Date.now(),
135
+ duration: this.source.cache.duration * 1000
136
+ };
137
+ }
138
+
139
+ return data;
140
+ } catch (error) {
141
+ console.error('❌ Failed to load remote config:', error);
142
+ throw error;
143
+ }
144
+ },
145
+
146
+ /**
147
+ * Fetch from remote with error handling
148
+ */
149
+ async fetchRemote(url) {
150
+ const response = await fetch(url);
151
+ if (!response.ok) {
152
+ throw new Error(`HTTP ${response.status}: ${url}`);
153
+ }
154
+ return response.json();
155
+ },
156
+
157
+ /**
158
+ * Check if cache is still valid
159
+ */
160
+ isCacheValid() {
161
+ if (!this.cache.timestamp) return false;
162
+ const age = Date.now() - this.cache.timestamp;
163
+ return age < this.cache.duration;
164
+ },
165
+
166
+ /**
167
+ * Clear cache
168
+ */
169
+ clearCache() {
170
+ this.cache = {};
171
+ },
172
+
173
+ /**
174
+ * Get data file path (adjusts for local vs remote)
175
+ */
176
+ getDataPath(filename) {
177
+ if (!this.cache.data) return `data/${filename}`;
178
+ return `${this.cache.data.paths.dataDir}/${filename}`;
179
+ },
180
+
181
+ /**
182
+ * Get language file path
183
+ */
184
+ getLangPath(langCode) {
185
+ if (!this.cache.data) return `lang/${langCode}.json`;
186
+ return `${this.cache.data.paths.langDir}/${langCode}.json`;
187
+ },
188
+
189
+ /**
190
+ * Get engine asset path
191
+ */
192
+ getEnginePath(path) {
193
+ const baseUrl = window.RETRO_ENGINE_URL || '';
194
+ return baseUrl + path.replace(/^\//, '');
195
+ }
196
+ };
197
+
198
+ // Enhanced AppConfig that uses ConfigLoader
199
+ const AppConfig = {
200
+ app: null,
201
+ languages: null,
202
+ categories: null,
203
+ mediaTypes: null,
204
+ loaded: false,
205
+ source: 'local',
206
+ paths: null,
207
+
208
+ /**
209
+ * Load all configuration files
210
+ */
211
+ async load() {
212
+ try {
213
+ const config = await ConfigLoader.init();
214
+
215
+ this.app = config.app;
216
+ this.languages = config.languages;
217
+ this.categories = config.categories;
218
+ this.mediaTypes = config.mediaTypes;
219
+ this.source = config.source;
220
+ this.paths = config.paths;
221
+ this.loaded = true;
222
+
223
+ console.log(`✅ Configuration loaded from ${config.source}`);
224
+ return true;
225
+ } catch (error) {
226
+ console.error('❌ Failed to load configuration:', error);
227
+ return false;
228
+ }
229
+ },
230
+
231
+ /**
232
+ * Get API base URL
233
+ */
234
+ getApiUrl() {
235
+ return this.app?.api?.baseUrl || 'http://127.0.0.1:5001';
236
+ },
237
+
238
+ /**
239
+ * Get supported language codes
240
+ */
241
+ getLanguageCodes() {
242
+ return this.languages?.supportedLanguages.map(l => l.code) || ['en'];
243
+ },
244
+
245
+ /**
246
+ * Get default language
247
+ */
248
+ getDefaultLanguage() {
249
+ return this.languages?.defaultLanguage || 'en';
250
+ },
251
+
252
+ /**
253
+ * Get all content types
254
+ */
255
+ getAllContentTypes() {
256
+ return this.categories?.contentTypes || this.categories?.categories || [];
257
+ },
258
+
259
+ /**
260
+ * Get content type configuration by ID
261
+ */
262
+ getContentType(contentTypeId) {
263
+ return this.getAllContentTypes().find(c => c.id === contentTypeId);
264
+ },
265
+
266
+ /**
267
+ * Get category configuration by ID (legacy, returns content type)
268
+ */
269
+ getCategory(categoryId) {
270
+ return this.getContentType(categoryId);
271
+ },
272
+
273
+ /**
274
+ * Get all categories (legacy, returns content types)
275
+ */
276
+ getAllCategories() {
277
+ return this.getAllContentTypes();
278
+ },
279
+
280
+ /**
281
+ * Get all media types
282
+ */
283
+ getAllMediaTypes() {
284
+ return this.mediaTypes?.mediaTypes || [];
285
+ },
286
+
287
+ /**
288
+ * Get media type configuration by ID
289
+ */
290
+ getMediaType(mediaTypeId) {
291
+ return this.getAllMediaTypes().find(m => m.id === mediaTypeId);
292
+ },
293
+
294
+ /**
295
+ * Get content types by media type
296
+ */
297
+ getContentTypesByMedia(mediaTypeId) {
298
+ return this.getAllContentTypes().filter(ct => ct.mediaType === mediaTypeId);
299
+ },
300
+
301
+ /**
302
+ * Get categories that support galleries (based on media type)
303
+ */
304
+ getGalleryCategories() {
305
+ const galleryTypes = [];
306
+ for (const ct of this.getAllContentTypes()) {
307
+ const mediaType = this.getMediaType(ct.mediaType);
308
+ if (mediaType && mediaType.supportsGallery) {
309
+ galleryTypes.push(ct.id);
310
+ }
311
+ }
312
+ return galleryTypes;
313
+ },
314
+
315
+ /**
316
+ * Get data file path for a category
317
+ */
318
+ getCategoryDataFile(categoryId) {
319
+ const category = this.getCategory(categoryId);
320
+ if (!category) return `data/${categoryId}.json`;
321
+
322
+ // If remote, return full URL, otherwise relative path
323
+ const fileName = category.dataFile.split('/').pop();
324
+ return ConfigLoader.getDataPath(fileName);
325
+ },
326
+
327
+ /**
328
+ * Get optional fields for a category
329
+ */
330
+ getCategoryFields(categoryId) {
331
+ const category = this.getCategory(categoryId);
332
+ return category?.fields?.optional || [];
333
+ },
334
+
335
+ /**
336
+ * Check if a field exists for a category
337
+ */
338
+ categoryHasField(categoryId, fieldName) {
339
+ const fields = this.getCategoryFields(categoryId);
340
+ return fields.some(f => f.name === fieldName);
341
+ },
342
+
343
+ /**
344
+ * Create multilingual object with all supported languages
345
+ */
346
+ createMultilingualObject(value) {
347
+ const obj = {};
348
+ this.getLanguageCodes().forEach(code => {
349
+ obj[code] = value;
350
+ });
351
+ return obj;
352
+ },
353
+
354
+ /**
355
+ * Get app setting
356
+ */
357
+ getSetting(path) {
358
+ const parts = path.split('.');
359
+ let value = this.app;
360
+ for (const part of parts) {
361
+ value = value?.[part];
362
+ if (value === undefined) return null;
363
+ }
364
+ return value;
365
+ }
366
+ };
367
+
368
+ // Export for use in other modules
369
+ window.AppConfig = AppConfig;
370
+ window.ConfigLoader = ConfigLoader;