@mtldev514/retro-portfolio-maker 1.0.14 β†’ 1.0.15

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.
package/README.md CHANGED
@@ -3,8 +3,11 @@
3
3
  [![npm version](https://badge.fury.io/js/%40retro-portfolio%2Fengine.svg)](https://www.npmjs.com/package/@mtldev514/retro-portfolio-maker)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Hello!
7
- **Site-as-a-Package** 🎨 Un moteur de portfolio rétro que vous installez via NPM. Vous fournissez vos données, le package fournit tout le reste.
6
+ This is what I always wanted. As someone with multiple interests, I always wanted to have a portfolio to display my many, many, many projects. I also have a very soft spot for the early 2000s.
7
+
8
+ I hope this portfolio manager will help you have a personal presence that speaks to your soul. Fully customizable. You can easily support many languages, and play with the theme to make it your own.
9
+
10
+ ---
8
11
 
9
12
  ## 🎯 Concept
10
13
 
package/bin/cli.js CHANGED
@@ -83,6 +83,39 @@ program
83
83
  }
84
84
  });
85
85
 
86
+ // Validate command - Check configuration files
87
+ program
88
+ .command('validate')
89
+ .description('Validate all configuration and data files')
90
+ .option('--path <dir>', 'Portfolio content directory', '.')
91
+ .action(async (options) => {
92
+ try {
93
+ const { spawn } = require('child_process');
94
+ const enginePath = path.join(__dirname, '..', 'engine', 'admin', 'scripts');
95
+ const validatorPath = path.join(enginePath, 'validate_config.py');
96
+
97
+ console.log(chalk.cyan('πŸ” Running configuration validator...\n'));
98
+
99
+ const python = spawn('python3', [validatorPath, '--path', options.path], {
100
+ stdio: 'inherit',
101
+ env: { ...process.env, PORTFOLIO_CONTENT_ROOT: options.path }
102
+ });
103
+
104
+ python.on('close', (code) => {
105
+ process.exit(code);
106
+ });
107
+
108
+ python.on('error', (err) => {
109
+ console.error(chalk.red('Error running validator:'), err.message);
110
+ console.log(chalk.yellow('\nMake sure Python 3 is installed and in your PATH'));
111
+ process.exit(1);
112
+ });
113
+ } catch (error) {
114
+ console.error(chalk.red('Error:'), error.message);
115
+ process.exit(1);
116
+ }
117
+ });
118
+
86
119
  // Deploy command - Deploy to GitHub Pages
87
120
  program
88
121
  .command('deploy')
@@ -11,7 +11,14 @@ from datetime import datetime
11
11
  import cloudinary
12
12
  import cloudinary.uploader
13
13
  import requests
14
+ import sys
14
15
  from dotenv import load_dotenv
16
+
17
+ # Ensure the current directory is in the path so we can import local modules
18
+ current_dir = os.path.dirname(os.path.abspath(__file__))
19
+ if current_dir not in sys.path:
20
+ sys.path.append(current_dir)
21
+
15
22
  from config_loader import config
16
23
 
17
24
  # Load environment variables
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Portfolio Configuration Validator
4
+ Validates all config files, data files, and translations for consistency and correctness.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Dict, List, Tuple, Any
12
+
13
+ # Ensure the current directory is in the path so we can import local modules
14
+ current_dir = os.path.dirname(os.path.abspath(__file__))
15
+ if current_dir not in sys.path:
16
+ sys.path.append(current_dir)
17
+
18
+ from config_loader import ConfigLoader
19
+
20
+ # ANSI color codes for terminal output
21
+ class Colors:
22
+ HEADER = '\033[95m'
23
+ OKBLUE = '\033[94m'
24
+ OKCYAN = '\033[96m'
25
+ OKGREEN = '\033[92m'
26
+ WARNING = '\033[93m'
27
+ FAIL = '\033[91m'
28
+ ENDC = '\033[0m'
29
+ BOLD = '\033[1m'
30
+ UNDERLINE = '\033[4m'
31
+
32
+
33
+ class ConfigValidator:
34
+ def __init__(self, content_root=None):
35
+ self.content_root = Path(content_root) if content_root else Path('.')
36
+ self.config_dir = self.content_root / 'config'
37
+ self.data_dir = self.content_root / 'data'
38
+ self.lang_dir = self.content_root / 'lang'
39
+
40
+ self.errors = []
41
+ self.warnings = []
42
+ self.info = []
43
+
44
+ self.config_loader = ConfigLoader(content_root)
45
+
46
+ def add_error(self, message: str):
47
+ """Add an error message"""
48
+ self.errors.append(f"{Colors.FAIL}βœ— ERROR:{Colors.ENDC} {message}")
49
+
50
+ def add_warning(self, message: str):
51
+ """Add a warning message"""
52
+ self.warnings.append(f"{Colors.WARNING}⚠ WARNING:{Colors.ENDC} {message}")
53
+
54
+ def add_info(self, message: str):
55
+ """Add an info message"""
56
+ self.info.append(f"{Colors.OKBLUE}β„Ή INFO:{Colors.ENDC} {message}")
57
+
58
+ def validate_json_file(self, file_path: Path) -> Tuple[bool, Any]:
59
+ """Validate that a file contains valid JSON"""
60
+ if not file_path.exists():
61
+ self.add_error(f"File not found: {file_path}")
62
+ return False, None
63
+
64
+ try:
65
+ with open(file_path, 'r', encoding='utf-8') as f:
66
+ data = json.load(f)
67
+ return True, data
68
+ except json.JSONDecodeError as e:
69
+ self.add_error(f"Invalid JSON in {file_path.name}: {e}")
70
+ return False, None
71
+ except Exception as e:
72
+ self.add_error(f"Error reading {file_path.name}: {e}")
73
+ return False, None
74
+
75
+ def validate_media_types(self) -> bool:
76
+ """Validate media-types.json"""
77
+ print(f"\n{Colors.HEADER}πŸ“‹ Validating Media Types...{Colors.ENDC}")
78
+
79
+ file_path = self.config_dir / 'media-types.json'
80
+ valid, data = self.validate_json_file(file_path)
81
+
82
+ if not valid:
83
+ return False
84
+
85
+ if 'mediaTypes' not in data:
86
+ self.add_error("media-types.json missing 'mediaTypes' array")
87
+ return False
88
+
89
+ media_types = data['mediaTypes']
90
+ required_fields = ['id', 'name', 'icon', 'viewer', 'acceptedFormats']
91
+
92
+ for i, mt in enumerate(media_types):
93
+ for field in required_fields:
94
+ if field not in mt:
95
+ self.add_error(f"Media type #{i} missing required field: {field}")
96
+
97
+ # Validate acceptedFormats is an array
98
+ if 'acceptedFormats' in mt and not isinstance(mt['acceptedFormats'], list):
99
+ self.add_error(f"Media type '{mt.get('id', i)}': acceptedFormats must be an array")
100
+
101
+ print(f"{Colors.OKGREEN}βœ“ Found {len(media_types)} media types{Colors.ENDC}")
102
+ return True
103
+
104
+ def validate_categories(self) -> bool:
105
+ """Validate categories.json (content types)"""
106
+ print(f"\n{Colors.HEADER}πŸ“ Validating Content Types...{Colors.ENDC}")
107
+
108
+ file_path = self.config_dir / 'categories.json'
109
+ valid, data = self.validate_json_file(file_path)
110
+
111
+ if not valid:
112
+ return False
113
+
114
+ # Support both 'contentTypes' and 'categories' keys
115
+ content_types = data.get('contentTypes', data.get('categories', []))
116
+
117
+ if not content_types:
118
+ self.add_error("categories.json missing content types array")
119
+ return False
120
+
121
+ required_fields = ['id', 'name', 'icon', 'mediaType', 'dataFile']
122
+
123
+ for i, ct in enumerate(content_types):
124
+ ct_id = ct.get('id', f'#{i}')
125
+
126
+ for field in required_fields:
127
+ if field not in ct:
128
+ self.add_error(f"Content type '{ct_id}' missing required field: {field}")
129
+
130
+ # Validate fields structure
131
+ if 'fields' in ct:
132
+ if 'required' not in ct['fields']:
133
+ self.add_warning(f"Content type '{ct_id}': missing 'fields.required' array")
134
+ if 'optional' not in ct['fields']:
135
+ self.add_warning(f"Content type '{ct_id}': missing 'fields.optional' array")
136
+
137
+ print(f"{Colors.OKGREEN}βœ“ Found {len(content_types)} content types{Colors.ENDC}")
138
+ return True
139
+
140
+ def validate_languages(self) -> bool:
141
+ """Validate languages.json"""
142
+ print(f"\n{Colors.HEADER}🌐 Validating Languages...{Colors.ENDC}")
143
+
144
+ file_path = self.config_dir / 'languages.json'
145
+ valid, data = self.validate_json_file(file_path)
146
+
147
+ if not valid:
148
+ return False
149
+
150
+ if 'supportedLanguages' not in data:
151
+ self.add_error("languages.json missing 'supportedLanguages' array")
152
+ return False
153
+
154
+ if 'defaultLanguage' not in data:
155
+ self.add_error("languages.json missing 'defaultLanguage' field")
156
+ return False
157
+
158
+ languages = data['supportedLanguages']
159
+ required_fields = ['code', 'name']
160
+
161
+ for lang in languages:
162
+ for field in required_fields:
163
+ if field not in lang:
164
+ self.add_error(f"Language entry missing required field: {field}")
165
+
166
+ print(f"{Colors.OKGREEN}βœ“ Found {len(languages)} supported languages{Colors.ENDC}")
167
+ return True
168
+
169
+ def validate_app_config(self) -> bool:
170
+ """Validate app.json"""
171
+ print(f"\n{Colors.HEADER}βš™οΈ Validating App Configuration...{Colors.ENDC}")
172
+
173
+ file_path = self.config_dir / 'app.json'
174
+ valid, data = self.validate_json_file(file_path)
175
+
176
+ if not valid:
177
+ return False
178
+
179
+ # Check recommended fields
180
+ recommended_fields = ['name', 'author', 'api', 'github']
181
+
182
+ for field in recommended_fields:
183
+ if field not in data:
184
+ self.add_warning(f"app.json missing recommended field: {field}")
185
+
186
+ print(f"{Colors.OKGREEN}βœ“ App configuration valid{Colors.ENDC}")
187
+ return True
188
+
189
+ def validate_data_files(self) -> bool:
190
+ """Validate all data files match category definitions"""
191
+ print(f"\n{Colors.HEADER}πŸ“Š Validating Data Files...{Colors.ENDC}")
192
+
193
+ # Load config first
194
+ if not self.config_loader.load_all():
195
+ self.add_error("Failed to load configuration")
196
+ return False
197
+
198
+ content_types = self.config_loader.get_content_types()
199
+
200
+ for ct in content_types:
201
+ ct_id = ct['id']
202
+ data_file = self.config_loader.get_category_data_file(ct_id)
203
+
204
+ # Check if data file exists
205
+ if not os.path.exists(data_file):
206
+ self.add_warning(f"Data file not found for '{ct_id}': {data_file}")
207
+ continue
208
+
209
+ # Validate JSON
210
+ valid, data = self.validate_json_file(Path(data_file))
211
+ if not valid:
212
+ continue
213
+
214
+ # Data can be array or object with 'items' key
215
+ if isinstance(data, dict) and 'items' in data:
216
+ items = data['items']
217
+ elif isinstance(data, list):
218
+ items = data
219
+ else:
220
+ self.add_error(f"Data file '{ct_id}' must be an array or object with 'items' key")
221
+ continue
222
+
223
+ # Validate required fields in each item
224
+ required_fields = ct.get('fields', {}).get('required', ['title', 'url'])
225
+
226
+ for i, item in enumerate(items):
227
+ for field in required_fields:
228
+ if field not in item:
229
+ self.add_warning(f"{ct_id}.json item #{i}: missing required field '{field}'")
230
+
231
+ # Check for ID field (recommended)
232
+ if 'id' not in item:
233
+ self.add_info(f"{ct_id}.json item #{i}: missing 'id' field (recommended)")
234
+
235
+ print(f"{Colors.OKGREEN}βœ“ {ct['icon']} {ct['name']}: {len(items)} items{Colors.ENDC}")
236
+
237
+ return True
238
+
239
+ def validate_translations(self) -> bool:
240
+ """Validate translation files"""
241
+ print(f"\n{Colors.HEADER}πŸ”€ Validating Translations...{Colors.ENDC}")
242
+
243
+ # Get language codes
244
+ lang_codes = self.config_loader.get_language_codes()
245
+
246
+ if not lang_codes:
247
+ self.add_error("No language codes found")
248
+ return False
249
+
250
+ # Load all translation files
251
+ translations = {}
252
+ for code in lang_codes:
253
+ lang_file = self.lang_dir / f'{code}.json'
254
+ valid, data = self.validate_json_file(lang_file)
255
+
256
+ if valid:
257
+ translations[code] = data
258
+ print(f"{Colors.OKGREEN}βœ“ {code}.json: {len(data)} keys{Colors.ENDC}")
259
+ else:
260
+ self.add_error(f"Failed to load translation file: {code}.json")
261
+
262
+ # Check for missing keys across languages
263
+ if len(translations) > 1:
264
+ all_keys = set()
265
+ for keys in translations.values():
266
+ all_keys.update(keys.keys())
267
+
268
+ for code, keys in translations.items():
269
+ missing = all_keys - set(keys.keys())
270
+ if missing:
271
+ self.add_warning(f"Language '{code}' missing {len(missing)} translation keys")
272
+ if len(missing) <= 5:
273
+ for key in missing:
274
+ self.add_info(f" Missing in '{code}': {key}")
275
+
276
+ return True
277
+
278
+ def validate_cross_references(self) -> bool:
279
+ """Validate cross-references between configs"""
280
+ print(f"\n{Colors.HEADER}πŸ”— Validating Cross-References...{Colors.ENDC}")
281
+
282
+ content_types = self.config_loader.get_content_types()
283
+ media_types = self.config_loader.get_media_types()
284
+
285
+ media_type_ids = {mt['id'] for mt in media_types}
286
+
287
+ for ct in content_types:
288
+ # Check if mediaType exists
289
+ if ct.get('mediaType') not in media_type_ids:
290
+ self.add_error(f"Content type '{ct['id']}' references unknown media type: {ct.get('mediaType')}")
291
+
292
+ print(f"{Colors.OKGREEN}βœ“ Cross-references valid{Colors.ENDC}")
293
+ return True
294
+
295
+ def run_validation(self) -> bool:
296
+ """Run all validations"""
297
+ print(f"\n{Colors.BOLD}{Colors.HEADER}{'='*60}{Colors.ENDC}")
298
+ print(f"{Colors.BOLD}{Colors.HEADER}πŸ” Portfolio Configuration Validator{Colors.ENDC}")
299
+ print(f"{Colors.BOLD}{Colors.HEADER}{'='*60}{Colors.ENDC}")
300
+ print(f"Content root: {self.content_root.absolute()}")
301
+
302
+ # Run all validations
303
+ validations = [
304
+ self.validate_media_types,
305
+ self.validate_categories,
306
+ self.validate_languages,
307
+ self.validate_app_config,
308
+ self.validate_data_files,
309
+ self.validate_translations,
310
+ self.validate_cross_references
311
+ ]
312
+
313
+ for validation in validations:
314
+ try:
315
+ validation()
316
+ except Exception as e:
317
+ self.add_error(f"Validation failed: {e}")
318
+
319
+ # Print summary
320
+ print(f"\n{Colors.BOLD}{Colors.HEADER}{'='*60}{Colors.ENDC}")
321
+ print(f"{Colors.BOLD}{Colors.HEADER}πŸ“ Validation Summary{Colors.ENDC}")
322
+ print(f"{Colors.BOLD}{Colors.HEADER}{'='*60}{Colors.ENDC}\n")
323
+
324
+ # Print all messages
325
+ for msg in self.errors:
326
+ print(msg)
327
+ for msg in self.warnings:
328
+ print(msg)
329
+ for msg in self.info:
330
+ print(msg)
331
+
332
+ # Print totals
333
+ print(f"\n{Colors.BOLD}Results:{Colors.ENDC}")
334
+ print(f"{Colors.FAIL} Errors: {len(self.errors)}{Colors.ENDC}")
335
+ print(f"{Colors.WARNING} Warnings: {len(self.warnings)}{Colors.ENDC}")
336
+ print(f"{Colors.OKBLUE} Info: {len(self.info)}{Colors.ENDC}")
337
+
338
+ if len(self.errors) == 0 and len(self.warnings) == 0:
339
+ print(f"\n{Colors.BOLD}{Colors.OKGREEN}βœ“ All validations passed! Your configuration is perfect! πŸŽ‰{Colors.ENDC}\n")
340
+ return True
341
+ elif len(self.errors) == 0:
342
+ print(f"\n{Colors.BOLD}{Colors.OKGREEN}βœ“ No critical errors found (warnings can be addressed later){Colors.ENDC}\n")
343
+ return True
344
+ else:
345
+ print(f"\n{Colors.BOLD}{Colors.FAIL}βœ— Validation failed with {len(self.errors)} error(s){Colors.ENDC}\n")
346
+ return False
347
+
348
+
349
+ def main():
350
+ import argparse
351
+
352
+ parser = argparse.ArgumentParser(description='Validate portfolio configuration files')
353
+ parser.add_argument('--path', help='Path to portfolio content directory', default='.')
354
+
355
+ args = parser.parse_args()
356
+
357
+ validator = ConfigValidator(args.path)
358
+ success = validator.run_validation()
359
+
360
+ sys.exit(0 if success else 1)
361
+
362
+
363
+ if __name__ == '__main__':
364
+ main()
package/engine/admin.html CHANGED
@@ -128,6 +128,17 @@
128
128
 
129
129
  <hr style="margin: 20px 0;">
130
130
 
131
+ <div class="field-row" style="margin-top: 20px;">
132
+ <button onclick="window.location.href='validator.html?auto=true';" style="width: 100%; height: 50px; font-weight: bold; cursor: pointer; background: linear-gradient(to bottom, #00aa00, #008800); color: white; border: 2px outset #fff;">
133
+ πŸ” VALIDATE CONFIGURATION
134
+ </button>
135
+ <p class="admin-muted" style="margin-top: 5px;">
136
+ * Check all config files, data files, and translations for errors
137
+ </p>
138
+ </div>
139
+
140
+ <hr style="margin: 20px 0;">
141
+
131
142
  <div class="field-row" style="margin-top: 20px;">
132
143
  <button onclick="syncGitHub()" style="width: 100%; height: 40px; font-weight: bold; cursor: pointer;">
133
144
  <img src="https://github.githubassets.com/favicons/favicon.svg" width="16"
@@ -0,0 +1,438 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>CONFIG VALIDATOR - PORTFOLIO</title>
6
+ <link rel="stylesheet" href="style.css">
7
+ <link rel="stylesheet" href="admin.css">
8
+ <link rel="stylesheet" href="fonts.css">
9
+ <script src="js/config-loader.js"></script>
10
+ <style>
11
+ .validator-container {
12
+ padding: 20px;
13
+ max-height: calc(100vh - 200px);
14
+ overflow-y: auto;
15
+ }
16
+
17
+ .validation-section {
18
+ background: white;
19
+ border: 2px inset var(--admin-win-border-dark);
20
+ padding: 15px;
21
+ margin-bottom: 15px;
22
+ }
23
+
24
+ .validation-section h3 {
25
+ margin: 0 0 10px 0;
26
+ padding-bottom: 8px;
27
+ border-bottom: 2px solid var(--admin-win-border-dark);
28
+ font-size: 13px;
29
+ }
30
+
31
+ .validation-result {
32
+ margin: 8px 0;
33
+ padding: 8px;
34
+ font-size: 11px;
35
+ border-left: 4px solid #ccc;
36
+ background: #f9f9f9;
37
+ }
38
+
39
+ .validation-result.success {
40
+ border-left-color: #00aa00;
41
+ background: #f0fff0;
42
+ }
43
+
44
+ .validation-result.error {
45
+ border-left-color: #cc0000;
46
+ background: #fff0f0;
47
+ }
48
+
49
+ .validation-result.warning {
50
+ border-left-color: #ff8800;
51
+ background: #fffaf0;
52
+ }
53
+
54
+ .validation-result.info {
55
+ border-left-color: #0080ff;
56
+ background: #f0f8ff;
57
+ }
58
+
59
+ .icon {
60
+ display: inline-block;
61
+ width: 16px;
62
+ font-weight: bold;
63
+ margin-right: 5px;
64
+ }
65
+
66
+ .summary {
67
+ background: var(--admin-win-bg);
68
+ border: 2px outset var(--admin-win-border-light);
69
+ padding: 15px;
70
+ margin-bottom: 20px;
71
+ font-weight: bold;
72
+ }
73
+
74
+ .summary.all-good {
75
+ background: linear-gradient(to bottom, #90ff90, #60ff60);
76
+ }
77
+
78
+ .summary.has-errors {
79
+ background: linear-gradient(to bottom, #ff9090, #ff6060);
80
+ }
81
+
82
+ .summary.has-warnings {
83
+ background: linear-gradient(to bottom, #ffdd90, #ffcc60);
84
+ }
85
+
86
+ .progress-bar {
87
+ width: 100%;
88
+ height: 20px;
89
+ background: white;
90
+ border: 2px inset var(--admin-win-border-dark);
91
+ margin: 10px 0;
92
+ }
93
+
94
+ .progress-bar-fill {
95
+ height: 100%;
96
+ background: linear-gradient(to right, #0080ff, #00a0ff);
97
+ transition: width 0.3s;
98
+ }
99
+ </style>
100
+ </head>
101
+
102
+ <body class="admin-page">
103
+ <div class="window">
104
+ <div class="title-bar">
105
+ <span>CONFIGURATION VALIDATOR</span>
106
+ <div class="title-bar-controls">
107
+ <button>_</button>
108
+ <button>[]</button>
109
+ <button onclick="window.location.href='admin.html#config'">X</button>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="menu-bar">
114
+ <button onclick="location.href='admin.html#config'">Β« Back to Admin</button>
115
+ <button onclick="runValidation()" id="validateBtn">πŸ” Run Validation</button>
116
+ <button onclick="exportReport()">πŸ“„ Export Report</button>
117
+ </div>
118
+
119
+ <div id="summary" class="summary" style="display: none;"></div>
120
+
121
+ <div id="progress" style="display: none; padding: 0 20px;">
122
+ <div class="progress-bar">
123
+ <div class="progress-bar-fill" id="progressBar" style="width: 0%"></div>
124
+ </div>
125
+ <p id="progressText" style="text-align: center; font-size: 11px;"></p>
126
+ </div>
127
+
128
+ <div class="validator-container" id="results">
129
+ <p align="center" style="color: #666; font-size: 12px;">
130
+ Click "Run Validation" to check your configuration files
131
+ </p>
132
+ </div>
133
+
134
+ <div class="status-bar">
135
+ <span id="statusText">Ready</span>
136
+ </div>
137
+ </div>
138
+
139
+ <script>
140
+ const API_URL = 'http://127.0.0.1:5001';
141
+
142
+ let validationResults = {
143
+ errors: [],
144
+ warnings: [],
145
+ info: [],
146
+ success: []
147
+ };
148
+
149
+ async function runValidation() {
150
+ const btn = document.getElementById('validateBtn');
151
+ const results = document.getElementById('results');
152
+ const progress = document.getElementById('progress');
153
+ const progressBar = document.getElementById('progressBar');
154
+ const progressText = document.getElementById('progressText');
155
+ const summary = document.getElementById('summary');
156
+
157
+ // Reset
158
+ validationResults = { errors: [], warnings: [], info: [], success: [] };
159
+ results.innerHTML = '';
160
+ summary.style.display = 'none';
161
+ progress.style.display = 'block';
162
+ btn.disabled = true;
163
+
164
+ const validations = [
165
+ { name: 'Media Types', fn: validateMediaTypes },
166
+ { name: 'Content Types', fn: validateContentTypes },
167
+ { name: 'Languages', fn: validateLanguages },
168
+ { name: 'App Config', fn: validateAppConfig },
169
+ { name: 'Data Files', fn: validateDataFiles },
170
+ { name: 'Cross-References', fn: validateCrossReferences }
171
+ ];
172
+
173
+ for (let i = 0; i < validations.length; i++) {
174
+ const validation = validations[i];
175
+ progressText.textContent = `Validating ${validation.name}...`;
176
+ progressBar.style.width = `${((i + 1) / validations.length) * 100}%`;
177
+
178
+ await validation.fn();
179
+ await new Promise(resolve => setTimeout(resolve, 200)); // Brief pause for UX
180
+ }
181
+
182
+ progress.style.display = 'none';
183
+ btn.disabled = false;
184
+
185
+ displayResults();
186
+ }
187
+
188
+ async function validateMediaTypes() {
189
+ const section = createSection('🎬 Media Types');
190
+
191
+ try {
192
+ await AppConfig.load();
193
+ const mediaTypes = AppConfig.getAllMediaTypes();
194
+
195
+ if (!mediaTypes || mediaTypes.length === 0) {
196
+ addError(section, 'No media types found');
197
+ return;
198
+ }
199
+
200
+ const requiredFields = ['id', 'name', 'icon', 'viewer', 'acceptedFormats'];
201
+
202
+ mediaTypes.forEach((mt, i) => {
203
+ requiredFields.forEach(field => {
204
+ if (!mt[field]) {
205
+ addError(section, `Media type #${i} missing required field: ${field}`);
206
+ }
207
+ });
208
+
209
+ if (mt.acceptedFormats && !Array.isArray(mt.acceptedFormats)) {
210
+ addError(section, `Media type '${mt.id}': acceptedFormats must be an array`);
211
+ }
212
+ });
213
+
214
+ addSuccess(section, `Found ${mediaTypes.length} media types`);
215
+ } catch (err) {
216
+ addError(section, `Failed to validate media types: ${err.message}`);
217
+ }
218
+ }
219
+
220
+ async function validateContentTypes() {
221
+ const section = createSection('πŸ“ Content Types');
222
+
223
+ try {
224
+ const contentTypes = AppConfig.getAllContentTypes();
225
+
226
+ if (!contentTypes || contentTypes.length === 0) {
227
+ addError(section, 'No content types found');
228
+ return;
229
+ }
230
+
231
+ const requiredFields = ['id', 'name', 'icon', 'mediaType', 'dataFile'];
232
+
233
+ contentTypes.forEach((ct, i) => {
234
+ requiredFields.forEach(field => {
235
+ if (!ct[field]) {
236
+ addError(section, `Content type '${ct.id || `#${i}`}' missing required field: ${field}`);
237
+ }
238
+ });
239
+
240
+ if (ct.fields) {
241
+ if (!ct.fields.required) {
242
+ addWarning(section, `Content type '${ct.id}': missing 'fields.required' array`);
243
+ }
244
+ if (!ct.fields.optional) {
245
+ addWarning(section, `Content type '${ct.id}': missing 'fields.optional' array`);
246
+ }
247
+ }
248
+ });
249
+
250
+ addSuccess(section, `Found ${contentTypes.length} content types`);
251
+ } catch (err) {
252
+ addError(section, `Failed to validate content types: ${err.message}`);
253
+ }
254
+ }
255
+
256
+ async function validateLanguages() {
257
+ const section = createSection('🌐 Languages');
258
+
259
+ try {
260
+ const langCodes = AppConfig.getLanguageCodes();
261
+
262
+ if (!langCodes || langCodes.length === 0) {
263
+ addError(section, 'No language codes found');
264
+ return;
265
+ }
266
+
267
+ addSuccess(section, `Found ${langCodes.length} supported languages: ${langCodes.join(', ')}`);
268
+
269
+ const defaultLang = AppConfig.getDefaultLanguage();
270
+ if (!defaultLang) {
271
+ addError(section, 'No default language specified');
272
+ } else {
273
+ addInfo(section, `Default language: ${defaultLang}`);
274
+ }
275
+ } catch (err) {
276
+ addError(section, `Failed to validate languages: ${err.message}`);
277
+ }
278
+ }
279
+
280
+ async function validateAppConfig() {
281
+ const section = createSection('βš™οΈ App Configuration');
282
+
283
+ try {
284
+ const appName = AppConfig.getSetting('name');
285
+ const author = AppConfig.getSetting('author');
286
+
287
+ if (!appName) addWarning(section, 'App name not set in app.json');
288
+ if (!author) addWarning(section, 'Author not set in app.json');
289
+
290
+ addSuccess(section, 'App configuration loaded');
291
+ } catch (err) {
292
+ addError(section, `Failed to validate app config: ${err.message}`);
293
+ }
294
+ }
295
+
296
+ async function validateDataFiles() {
297
+ const section = createSection('πŸ“Š Data Files');
298
+
299
+ try {
300
+ const res = await fetch(`${API_URL}/api/content`);
301
+ const allContent = await res.json();
302
+
303
+ const contentTypes = AppConfig.getAllContentTypes();
304
+
305
+ for (const ct of contentTypes) {
306
+ const items = allContent[ct.id] || [];
307
+ const requiredFields = ct.fields?.required || ['title', 'url'];
308
+
309
+ if (items.length === 0) {
310
+ addInfo(section, `${ct.icon} ${ct.name}: No items yet`);
311
+ continue;
312
+ }
313
+
314
+ // Validate items
315
+ items.forEach((item, i) => {
316
+ requiredFields.forEach(field => {
317
+ if (!item[field]) {
318
+ addWarning(section, `${ct.id}.json item #${i}: missing required field '${field}'`);
319
+ }
320
+ });
321
+
322
+ if (!item.id) {
323
+ addInfo(section, `${ct.id}.json item #${i}: missing 'id' field (recommended)`);
324
+ }
325
+ });
326
+
327
+ addSuccess(section, `${ct.icon} ${ct.name}: ${items.length} items`);
328
+ }
329
+ } catch (err) {
330
+ addError(section, `Failed to validate data files: ${err.message}`);
331
+ }
332
+ }
333
+
334
+ async function validateCrossReferences() {
335
+ const section = createSection('πŸ”— Cross-References');
336
+
337
+ try {
338
+ const contentTypes = AppConfig.getAllContentTypes();
339
+ const mediaTypes = AppConfig.getAllMediaTypes();
340
+
341
+ const mediaTypeIds = new Set(mediaTypes.map(mt => mt.id));
342
+
343
+ contentTypes.forEach(ct => {
344
+ if (!mediaTypeIds.has(ct.mediaType)) {
345
+ addError(section, `Content type '${ct.id}' references unknown media type: ${ct.mediaType}`);
346
+ }
347
+ });
348
+
349
+ addSuccess(section, 'All cross-references valid');
350
+ } catch (err) {
351
+ addError(section, `Failed to validate cross-references: ${err.message}`);
352
+ }
353
+ }
354
+
355
+ function createSection(title) {
356
+ const section = document.createElement('div');
357
+ section.className = 'validation-section';
358
+ section.innerHTML = `<h3>${title}</h3>`;
359
+ document.getElementById('results').appendChild(section);
360
+ return section;
361
+ }
362
+
363
+ function addResult(section, type, message) {
364
+ const result = document.createElement('div');
365
+ result.className = `validation-result ${type}`;
366
+
367
+ const icons = {
368
+ success: 'βœ“',
369
+ error: 'βœ—',
370
+ warning: '⚠',
371
+ info: 'β„Ή'
372
+ };
373
+
374
+ result.innerHTML = `<span class="icon">${icons[type]}</span>${message}`;
375
+ section.appendChild(result);
376
+
377
+ validationResults[type === 'success' ? 'success' : type + 's'].push(message);
378
+ }
379
+
380
+ function addSuccess(section, message) { addResult(section, 'success', message); }
381
+ function addError(section, message) { addResult(section, 'error', message); }
382
+ function addWarning(section, message) { addResult(section, 'warning', message); }
383
+ function addInfo(section, message) { addResult(section, 'info', message); }
384
+
385
+ function displayResults() {
386
+ const summary = document.getElementById('summary');
387
+ const errorCount = validationResults.errors.length;
388
+ const warningCount = validationResults.warnings.length;
389
+
390
+ summary.style.display = 'block';
391
+
392
+ if (errorCount === 0 && warningCount === 0) {
393
+ summary.className = 'summary all-good';
394
+ summary.innerHTML = 'βœ“ ALL VALIDATIONS PASSED! Your configuration is perfect! πŸŽ‰';
395
+ } else if (errorCount > 0) {
396
+ summary.className = 'summary has-errors';
397
+ summary.innerHTML = `βœ— ${errorCount} ERROR${errorCount > 1 ? 'S' : ''} FOUND${warningCount > 0 ? ` (${warningCount} warning${warningCount > 1 ? 's' : ''})` : ''}`;
398
+ } else {
399
+ summary.className = 'summary has-warnings';
400
+ summary.innerHTML = `⚠ ${warningCount} WARNING${warningCount > 1 ? 'S' : ''} (no critical errors)`;
401
+ }
402
+
403
+ document.getElementById('statusText').textContent = `Validation complete: ${errorCount} errors, ${warningCount} warnings`;
404
+ }
405
+
406
+ function exportReport() {
407
+ const report = {
408
+ timestamp: new Date().toISOString(),
409
+ summary: {
410
+ errors: validationResults.errors.length,
411
+ warnings: validationResults.warnings.length,
412
+ info: validationResults.info.length,
413
+ success: validationResults.success.length
414
+ },
415
+ results: validationResults
416
+ };
417
+
418
+ const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
419
+ const url = URL.createObjectURL(blob);
420
+ const a = document.createElement('a');
421
+ a.href = url;
422
+ a.download = `validation-report-${new Date().toISOString().split('T')[0]}.json`;
423
+ a.click();
424
+ URL.revokeObjectURL(url);
425
+ }
426
+
427
+ // Auto-run on load if ?auto=true
428
+ window.addEventListener('DOMContentLoaded', async () => {
429
+ await AppConfig.load();
430
+
431
+ const urlParams = new URLSearchParams(window.location.search);
432
+ if (urlParams.get('auto') === 'true') {
433
+ runValidation();
434
+ }
435
+ });
436
+ </script>
437
+ </body>
438
+ </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mtldev514/retro-portfolio-maker",
3
- "version": "1.0.14",
4
- "description": "Retro portfolio maker - Site-as-a-Package. Install the engine, provide your data, build your 90s-aesthetic portfolio.",
3
+ "version": "1.0.15",
4
+ "description": "A portfolio manager for multi-passionate creators with a soft spot for early 2000s aesthetics. Display your many projects with soul.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "retro-portfolio": "./bin/cli.js"