@mtldev514/retro-portfolio-engine 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 (43) hide show
  1. package/README.md +408 -0
  2. package/bin/cli.js +103 -0
  3. package/engine/admin/admin.css +720 -0
  4. package/engine/admin/admin.html +801 -0
  5. package/engine/admin/admin_api.py +230 -0
  6. package/engine/admin/scripts/backup.sh +116 -0
  7. package/engine/admin/scripts/config_loader.py +180 -0
  8. package/engine/admin/scripts/init.sh +141 -0
  9. package/engine/admin/scripts/manager.py +308 -0
  10. package/engine/admin/scripts/restore.sh +121 -0
  11. package/engine/admin/scripts/server.py +41 -0
  12. package/engine/admin/scripts/update.sh +321 -0
  13. package/engine/admin/scripts/validate_json.py +62 -0
  14. package/engine/fonts.css +37 -0
  15. package/engine/index.html +190 -0
  16. package/engine/js/config-loader.js +370 -0
  17. package/engine/js/config.js +173 -0
  18. package/engine/js/counter.js +17 -0
  19. package/engine/js/effects.js +97 -0
  20. package/engine/js/i18n.js +68 -0
  21. package/engine/js/init.js +107 -0
  22. package/engine/js/media.js +264 -0
  23. package/engine/js/render.js +282 -0
  24. package/engine/js/router.js +133 -0
  25. package/engine/js/sparkle.js +123 -0
  26. package/engine/js/themes.js +607 -0
  27. package/engine/style.css +2037 -0
  28. package/index.js +35 -0
  29. package/package.json +48 -0
  30. package/scripts/admin.js +67 -0
  31. package/scripts/build.js +142 -0
  32. package/scripts/init.js +237 -0
  33. package/scripts/post-install.js +16 -0
  34. package/scripts/serve.js +54 -0
  35. package/templates/user-portfolio/.github/workflows/deploy.yml +57 -0
  36. package/templates/user-portfolio/config/app.json +36 -0
  37. package/templates/user-portfolio/config/categories.json +241 -0
  38. package/templates/user-portfolio/config/languages.json +15 -0
  39. package/templates/user-portfolio/config/media-types.json +59 -0
  40. package/templates/user-portfolio/data/painting.json +3 -0
  41. package/templates/user-portfolio/data/projects.json +3 -0
  42. package/templates/user-portfolio/lang/en.json +114 -0
  43. package/templates/user-portfolio/lang/fr.json +114 -0
@@ -0,0 +1,230 @@
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_cors import CORS
3
+ import os
4
+ import sys
5
+ import json
6
+
7
+ # Get user data directories from environment variables
8
+ # These will be set by the npm admin script
9
+ USER_DATA_DIR = os.environ.get('DATA_DIR', '../../data')
10
+ USER_CONFIG_DIR = os.environ.get('CONFIG_DIR', '../../config')
11
+ USER_LANG_DIR = os.environ.get('LANG_DIR', '../../lang')
12
+ PORT = int(os.environ.get('PORT', 5001))
13
+
14
+ # Add scripts directory to path
15
+ SCRIPT_DIR = os.path.join(os.path.dirname(__file__), 'scripts')
16
+ sys.path.append(SCRIPT_DIR)
17
+
18
+ # Import manager and config loader
19
+ import manager
20
+ from config_loader import config
21
+
22
+ # Override config paths to use user directories
23
+ config.CONFIG_DIR = USER_CONFIG_DIR
24
+ config.DATA_DIR = USER_DATA_DIR
25
+ config.LANG_DIR = USER_LANG_DIR
26
+
27
+ # Load configuration from user directories
28
+ config.load_all()
29
+
30
+ app = Flask(__name__,
31
+ static_folder='.',
32
+ static_url_path='')
33
+ CORS(app)
34
+
35
+ UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', 'temp_uploads')
36
+ if not os.path.exists(UPLOAD_FOLDER):
37
+ os.makedirs(UPLOAD_FOLDER)
38
+
39
+ # Serve admin.html
40
+ @app.route('/admin.html')
41
+ def serve_admin():
42
+ return send_from_directory('.', 'admin.html')
43
+
44
+ @app.route('/admin.css')
45
+ def serve_admin_css():
46
+ return send_from_directory('.', 'admin.css')
47
+
48
+ @app.route('/api/upload', methods=['POST'])
49
+ def upload_file():
50
+ if 'file' not in request.files:
51
+ return jsonify({"error": "No file part"}), 400
52
+
53
+ file = request.files['file']
54
+ if file.filename == '':
55
+ return jsonify({"error": "No selected file"}), 400
56
+
57
+ title = request.form.get('title')
58
+ category = request.form.get('category')
59
+ medium = request.form.get('medium')
60
+ genre = request.form.get('genre')
61
+ description = request.form.get('description')
62
+ created = request.form.get('created')
63
+
64
+ if not title or not category:
65
+ return jsonify({"error": "Title and Category are required"}), 400
66
+
67
+ # Save file temporarily
68
+ temp_path = os.path.join(UPLOAD_FOLDER, file.filename)
69
+ file.save(temp_path)
70
+
71
+ try:
72
+ # Use manager logic to upload to Cloudinary and update JSON
73
+ result = manager.upload_and_save(
74
+ temp_path,
75
+ title,
76
+ category,
77
+ medium=medium,
78
+ genre=genre,
79
+ description=description,
80
+ created=created
81
+ )
82
+ return jsonify({"success": True, "data": result})
83
+ except Exception as e:
84
+ return jsonify({"error": str(e)}), 500
85
+ finally:
86
+ # Cleanup temp file
87
+ if os.path.exists(temp_path):
88
+ os.remove(temp_path)
89
+
90
+ @app.route('/api/upload-bulk', methods=['POST'])
91
+ def upload_bulk():
92
+ """Handle bulk file uploads"""
93
+ results = []
94
+ errors = []
95
+ file_keys = [k for k in request.files if k.startswith('file_')]
96
+ file_keys.sort(key=lambda k: int(k.split('_')[1]))
97
+
98
+ for key in file_keys:
99
+ idx = key.split('_')[1]
100
+ file = request.files[key]
101
+ title = request.form.get(f'title_{idx}', file.filename)
102
+ category = request.form.get(f'category_{idx}')
103
+ medium = request.form.get(f'medium_{idx}')
104
+ genre = request.form.get(f'genre_{idx}')
105
+ description = request.form.get(f'description_{idx}')
106
+ created = request.form.get(f'created_{idx}')
107
+
108
+ if not category:
109
+ errors.append({"file": file.filename, "error": "Missing category"})
110
+ continue
111
+
112
+ temp_path = os.path.join(UPLOAD_FOLDER, file.filename)
113
+ file.save(temp_path)
114
+
115
+ try:
116
+ result = manager.upload_and_save(
117
+ temp_path, title, category,
118
+ medium=medium, genre=genre,
119
+ description=description, created=created
120
+ )
121
+ results.append({"file": file.filename, "success": True, "data": result})
122
+ except Exception as e:
123
+ errors.append({"file": file.filename, "error": str(e)})
124
+ finally:
125
+ if os.path.exists(temp_path):
126
+ os.remove(temp_path)
127
+
128
+ return jsonify({
129
+ "success": len(results),
130
+ "errors": len(errors),
131
+ "results": results,
132
+ "errorDetails": errors
133
+ })
134
+
135
+ @app.route('/api/content/<category>', methods=['GET'])
136
+ def get_content(category):
137
+ """Get all content for a category"""
138
+ try:
139
+ data_file = os.path.join(USER_DATA_DIR, f'{category}.json')
140
+ if not os.path.exists(data_file):
141
+ return jsonify({"items": []})
142
+
143
+ with open(data_file, 'r', encoding='utf-8') as f:
144
+ data = json.load(f)
145
+ return jsonify(data)
146
+ except Exception as e:
147
+ return jsonify({"error": str(e)}), 500
148
+
149
+ @app.route('/api/content/<category>', methods=['POST'])
150
+ def save_content(category):
151
+ """Save content for a category"""
152
+ try:
153
+ data = request.json
154
+ data_file = os.path.join(USER_DATA_DIR, f'{category}.json')
155
+
156
+ with open(data_file, 'w', encoding='utf-8') as f:
157
+ json.dump(data, f, indent=2, ensure_ascii=False)
158
+
159
+ return jsonify({"success": True})
160
+ except Exception as e:
161
+ return jsonify({"error": str(e)}), 500
162
+
163
+ @app.route('/api/translations/<lang>', methods=['GET'])
164
+ def get_translations(lang):
165
+ """Get translations for a language"""
166
+ try:
167
+ lang_file = os.path.join(USER_LANG_DIR, f'{lang}.json')
168
+ if not os.path.exists(lang_file):
169
+ return jsonify({})
170
+
171
+ with open(lang_file, 'r', encoding='utf-8') as f:
172
+ data = json.load(f)
173
+ return jsonify(data)
174
+ except Exception as e:
175
+ return jsonify({"error": str(e)}), 500
176
+
177
+ @app.route('/api/translations/<lang>', methods=['POST'])
178
+ def save_translations(lang):
179
+ """Save translations for a language"""
180
+ try:
181
+ data = request.json
182
+ lang_file = os.path.join(USER_LANG_DIR, f'{lang}.json')
183
+
184
+ with open(lang_file, 'w', encoding='utf-8') as f:
185
+ json.dump(data, f, indent=2, ensure_ascii=False)
186
+
187
+ return jsonify({"success": True})
188
+ except Exception as e:
189
+ return jsonify({"error": str(e)}), 500
190
+
191
+ @app.route('/api/config/<config_name>', methods=['GET'])
192
+ def get_config(config_name):
193
+ """Get configuration"""
194
+ try:
195
+ config_file = os.path.join(USER_CONFIG_DIR, f'{config_name}.json')
196
+ if not os.path.exists(config_file):
197
+ return jsonify({})
198
+
199
+ with open(config_file, 'r', encoding='utf-8') as f:
200
+ data = json.load(f)
201
+ return jsonify(data)
202
+ except Exception as e:
203
+ return jsonify({"error": str(e)}), 500
204
+
205
+ @app.route('/api/config/<config_name>', methods=['POST'])
206
+ def save_config(config_name):
207
+ """Save configuration"""
208
+ try:
209
+ data = request.json
210
+ config_file = os.path.join(USER_CONFIG_DIR, f'{config_name}.json')
211
+
212
+ with open(config_file, 'w', encoding='utf-8') as f:
213
+ json.dump(data, f, indent=2, ensure_ascii=False)
214
+
215
+ # Reload config after save
216
+ config.load_all()
217
+
218
+ return jsonify({"success": True})
219
+ except Exception as e:
220
+ return jsonify({"error": str(e)}), 500
221
+
222
+ if __name__ == '__main__':
223
+ print(f"🔧 Admin API starting...")
224
+ print(f" Data dir: {os.path.abspath(USER_DATA_DIR)}")
225
+ print(f" Config dir: {os.path.abspath(USER_CONFIG_DIR)}")
226
+ print(f" Lang dir: {os.path.abspath(USER_LANG_DIR)}")
227
+ print(f" Port: {PORT}")
228
+ print(f"\n✨ Admin interface: http://localhost:{PORT}/admin.html\n")
229
+
230
+ app.run(host='0.0.0.0', port=PORT, debug=True)
@@ -0,0 +1,116 @@
1
+ #!/bin/bash
2
+
3
+ # Backup Personal Data
4
+ # Creates a timestamped backup of your personal configuration and content
5
+
6
+ set -e
7
+
8
+ echo ""
9
+ echo "═══════════════════════════════════════════════════════════"
10
+ echo " 📦 Backup Personal Data"
11
+ echo "═══════════════════════════════════════════════════════════"
12
+ echo ""
13
+
14
+ # Create backup directory with timestamp
15
+ BACKUP_DIR=".backup-personal"
16
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
17
+ BACKUP_PATH="$BACKUP_DIR/$TIMESTAMP"
18
+
19
+ echo "Creating backup at: $BACKUP_PATH"
20
+ echo ""
21
+
22
+ # Create backup directory structure
23
+ mkdir -p "$BACKUP_PATH"
24
+
25
+ # Track what we backed up
26
+ BACKED_UP=0
27
+
28
+ # Backup config
29
+ if [ -d "config" ]; then
30
+ cp -r config "$BACKUP_PATH/"
31
+ echo " ✓ Backed up config/ ($(find config -type f | wc -l | xargs) files)"
32
+ BACKED_UP=$((BACKED_UP + 1))
33
+ else
34
+ echo " ⚠️ config/ not found"
35
+ fi
36
+
37
+ # Backup data
38
+ if [ -d "data" ]; then
39
+ cp -r data "$BACKUP_PATH/"
40
+ echo " ✓ Backed up data/ ($(find data -type f | wc -l | xargs) files)"
41
+ BACKED_UP=$((BACKED_UP + 1))
42
+ else
43
+ echo " ⚠️ data/ not found"
44
+ fi
45
+
46
+ # Backup lang
47
+ if [ -d "lang" ]; then
48
+ cp -r lang "$BACKUP_PATH/"
49
+ echo " ✓ Backed up lang/ ($(find lang -type f | wc -l | xargs) files)"
50
+ BACKED_UP=$((BACKED_UP + 1))
51
+ else
52
+ echo " ⚠️ lang/ not found"
53
+ fi
54
+
55
+ # Backup .env
56
+ if [ -f ".env" ]; then
57
+ cp .env "$BACKUP_PATH/"
58
+ echo " ✓ Backed up .env"
59
+ BACKED_UP=$((BACKED_UP + 1))
60
+ else
61
+ echo " ⚠️ .env not found"
62
+ fi
63
+
64
+
65
+ # Create backup info file
66
+ cat > "$BACKUP_PATH/BACKUP_INFO.txt" << EOF
67
+ Backup Information
68
+ ==================
69
+
70
+ Date: $(date)
71
+ Location: $BACKUP_PATH
72
+ Hostname: $(hostname)
73
+ User: $(whoami)
74
+
75
+ Contents:
76
+ $(ls -la "$BACKUP_PATH")
77
+
78
+ To restore this backup:
79
+ ./scripts/restore.sh $TIMESTAMP
80
+
81
+ Or manually:
82
+ cp -r $BACKUP_PATH/config ./
83
+ cp -r $BACKUP_PATH/data ./
84
+ cp -r $BACKUP_PATH/lang ./
85
+ cp $BACKUP_PATH/.env ./
86
+ EOF
87
+
88
+ echo ""
89
+ echo "═══════════════════════════════════════════════════════════"
90
+
91
+ if [ $BACKED_UP -gt 0 ]; then
92
+ echo " ✅ Backup Complete!"
93
+ echo "═══════════════════════════════════════════════════════════"
94
+ echo ""
95
+ echo "📂 Backup location: $BACKUP_PATH"
96
+ echo "📊 Items backed up: $BACKED_UP"
97
+ echo ""
98
+ echo "To restore:"
99
+ echo " ./scripts/restore.sh $TIMESTAMP"
100
+ echo ""
101
+ else
102
+ echo " ⚠️ Nothing to backup"
103
+ echo "═══════════════════════════════════════════════════════════"
104
+ echo ""
105
+ echo "No personal data found. Have you run ./init.sh yet?"
106
+ echo ""
107
+ fi
108
+
109
+ # Clean up old backups (keep last 5)
110
+ echo "Cleaning up old backups (keeping last 5)..."
111
+ ls -t "$BACKUP_DIR" | tail -n +6 | while read old_backup; do
112
+ rm -rf "$BACKUP_DIR/$old_backup"
113
+ echo " Removed old backup: $old_backup"
114
+ done
115
+
116
+ echo ""
@@ -0,0 +1,180 @@
1
+ """
2
+ Configuration Loader for Backend
3
+ Loads all configuration files and makes them available to Python scripts
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+
10
+
11
+ class ConfigLoader:
12
+ """Centralized configuration loader"""
13
+
14
+ def __init__(self, content_root=None):
15
+ if content_root is None:
16
+ content_root = os.environ.get('PORTFOLIO_CONTENT_ROOT', '.')
17
+
18
+ self.content_root = Path(content_root).resolve()
19
+ self.config_dir = self.content_root / 'config'
20
+ self.data_dir = self.content_root / 'data'
21
+ self.lang_dir = self.content_root / 'lang'
22
+
23
+ self.app_config = None
24
+ self.languages_config = None
25
+ self.categories_config = None
26
+ self.media_types_config = None
27
+
28
+ def load_all(self):
29
+ """Load all configuration files"""
30
+ try:
31
+ # Ensure directories exist
32
+ for d in [self.config_dir, self.data_dir, self.lang_dir]:
33
+ if not d.exists():
34
+ try:
35
+ os.makedirs(d)
36
+ except OSError:
37
+ pass # Might be read-only or we just can't create it
38
+
39
+ with open(self.config_dir / 'app.json', 'r', encoding='utf-8') as f:
40
+ self.app_config = json.load(f)
41
+
42
+ with open(self.config_dir / 'languages.json', 'r', encoding='utf-8') as f:
43
+ self.languages_config = json.load(f)
44
+
45
+ with open(self.config_dir / 'categories.json', 'r', encoding='utf-8') as f:
46
+ self.categories_config = json.load(f)
47
+
48
+ with open(self.config_dir / 'media-types.json', 'r', encoding='utf-8') as f:
49
+ self.media_types_config = json.load(f)
50
+
51
+ print(f'✅ Configuration loaded from {self.content_root}')
52
+ return True
53
+ except Exception as e:
54
+ print(f'❌ Failed to load configuration from {self.content_root}: {e}')
55
+ return False
56
+
57
+ # ... (getters) ...
58
+
59
+ def get_category_data_file(self, category_id):
60
+ """Get absolute data file path for a category"""
61
+ cat = self.get_content_type(category_id)
62
+ filename = f'{category_id}.json'
63
+ if cat and 'dataFile' in cat:
64
+ # If dataFile is specified, use it (removing data/ prefix if present to avoid doubling)
65
+ filename = cat['dataFile'].replace('data/', '')
66
+
67
+ return str(self.data_dir / filename)
68
+
69
+ def get_port(self):
70
+ """Get API port"""
71
+ return self.app_config.get('api', {}).get('port', 5001)
72
+
73
+ def get_host(self):
74
+ """Get API host"""
75
+ return self.app_config.get('api', {}).get('host', '127.0.0.1')
76
+
77
+ def get_language_codes(self):
78
+ """Get list of supported language codes"""
79
+ return [lang['code'] for lang in self.languages_config.get('supportedLanguages', [])]
80
+
81
+ def get_default_language(self):
82
+ """Get default language code"""
83
+ return self.languages_config.get('defaultLanguage', 'en')
84
+
85
+ def get_content_types(self):
86
+ """Get all content type configurations (new name for categories)"""
87
+ return self.categories_config.get('contentTypes', self.categories_config.get('categories', []))
88
+
89
+ def get_categories(self):
90
+ """Get all category configurations (legacy method, now returns content types)"""
91
+ return self.get_content_types()
92
+
93
+ def get_content_type(self, content_type_id):
94
+ """Get specific content type configuration"""
95
+ for ct in self.get_content_types():
96
+ if ct['id'] == content_type_id:
97
+ return ct
98
+ return None
99
+
100
+ def get_category(self, category_id):
101
+ """Get specific category configuration (legacy method)"""
102
+ return self.get_content_type(category_id)
103
+
104
+ def get_category_data_file(self, category_id):
105
+ """Get absolute data file path for a category"""
106
+ cat = self.get_content_type(category_id)
107
+ filename = f'{category_id}.json'
108
+ if cat and 'dataFile' in cat:
109
+ # If dataFile is specified, ensure it's relative to data_dir
110
+ # We strip 'data/' prefix if it was hardcoded in the config
111
+ clean_name = cat['dataFile'].replace('data/', '')
112
+ return str(self.data_dir / clean_name)
113
+ return str(self.data_dir / filename)
114
+
115
+ def get_category_map(self):
116
+ """Get mapping of category ID to absolute data file path"""
117
+ return {cat['id']: self.get_category_data_file(cat['id']) for cat in self.get_content_types()}
118
+
119
+ def get_gallery_categories(self):
120
+ """Get list of categories that support galleries (based on media type)"""
121
+ gallery_types = []
122
+ for ct in self.get_content_types():
123
+ media_type = self.get_media_type(ct.get('mediaType'))
124
+ if media_type and media_type.get('supportsGallery', False):
125
+ gallery_types.append(ct['id'])
126
+ return gallery_types
127
+
128
+ def get_media_types(self):
129
+ """Get all media type configurations"""
130
+ if self.media_types_config is None:
131
+ return []
132
+ return self.media_types_config.get('mediaTypes', [])
133
+
134
+ def get_media_type(self, media_type_id):
135
+ """Get specific media type configuration"""
136
+ for mt in self.get_media_types():
137
+ if mt['id'] == media_type_id:
138
+ return mt
139
+ return None
140
+
141
+ def get_content_types_by_media(self, media_type_id):
142
+ """Get all content types that use a specific media type"""
143
+ return [ct for ct in self.get_content_types() if ct.get('mediaType') == media_type_id]
144
+
145
+ def get_github_config(self):
146
+ """Get GitHub configuration"""
147
+ return self.app_config.get('github', {})
148
+
149
+ def get_github_repo(self):
150
+ """Get full GitHub repository path (username/repo)"""
151
+ github = self.get_github_config()
152
+ username = github.get('username', '')
153
+ repo_name = github.get('repoName', '')
154
+ if username and repo_name:
155
+ return f"{username}/{repo_name}"
156
+ # Fallback to old 'repo' key if it exists
157
+ return github.get('repo', 'yourusername/retro-portfolio')
158
+
159
+ def get_path(self, path_key):
160
+ """Get configured path (dataDir, langDir, etc.)"""
161
+ return self.app_config.get('paths', {}).get(path_key, path_key)
162
+
163
+ def create_multilingual_object(self, value):
164
+ """Create a multilingual object with all supported languages"""
165
+ return {code: value for code in self.get_language_codes()}
166
+
167
+ def get_setting(self, path):
168
+ """Get app setting by dot-notation path (e.g., 'app.name')"""
169
+ parts = path.split('.')
170
+ value = self.app_config
171
+ for part in parts:
172
+ if isinstance(value, dict):
173
+ value = value.get(part)
174
+ else:
175
+ return None
176
+ return value
177
+
178
+
179
+ # Global instance
180
+ config = ConfigLoader()
@@ -0,0 +1,141 @@
1
+ #!/bin/bash
2
+
3
+ # Portfolio Initialization Script
4
+ # Copies .example files to create your working configuration
5
+
6
+ set -e
7
+
8
+ echo ""
9
+ echo "═══════════════════════════════════════════════════════════"
10
+ echo " 🎨 Retro Portfolio - Initialization"
11
+ echo "═══════════════════════════════════════════════════════════"
12
+ echo ""
13
+
14
+ # Check if already initialized
15
+ if [ -d "config" ] && [ -f "config/app.json" ]; then
16
+ echo "⚠️ Portfolio appears to be already initialized."
17
+ echo ""
18
+ read -p "Reinitialize and overwrite existing files? (y/N): " -n 1 -r
19
+ echo
20
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
21
+ echo "Aborted."
22
+ exit 0
23
+ fi
24
+ fi
25
+
26
+ echo "📁 Creating directory structure..."
27
+
28
+ # Create directories
29
+ mkdir -p config
30
+ mkdir -p data
31
+ mkdir -p lang
32
+
33
+ echo "📋 Copying configuration examples..."
34
+
35
+ # Copy config files
36
+ if [ -d "config.example" ]; then
37
+ for file in config.example/*.example; do
38
+ if [ -f "$file" ]; then
39
+ basename=$(basename "$file" .example)
40
+ cp "$file" "config/$basename"
41
+ echo " ✓ config/$basename"
42
+ fi
43
+ done
44
+ else
45
+ echo " ⚠️ config.example/ not found, skipping"
46
+ fi
47
+
48
+ echo ""
49
+ echo "📋 Copying data examples..."
50
+
51
+ # Copy data files
52
+ if [ -d "data.example" ]; then
53
+ for file in data.example/*.example; do
54
+ if [ -f "$file" ]; then
55
+ basename=$(basename "$file" .example)
56
+ cp "$file" "data/$basename"
57
+ echo " ✓ data/$basename"
58
+ fi
59
+ done
60
+ else
61
+ echo " ⚠️ data.example/ not found, skipping"
62
+ fi
63
+
64
+ echo ""
65
+ echo "🌐 Copying language files..."
66
+
67
+ # Copy lang files
68
+ if [ -d "lang.example" ]; then
69
+ for file in lang.example/*.example; do
70
+ if [ -f "$file" ]; then
71
+ basename=$(basename "$file" .example)
72
+ cp "$file" "lang/$basename"
73
+ echo " ✓ lang/$basename"
74
+ fi
75
+ done
76
+ else
77
+ echo " ⚠️ lang.example/ not found, skipping"
78
+ fi
79
+
80
+ echo ""
81
+ echo "🔐 Setting up environment..."
82
+
83
+ # Copy .env if doesn't exist
84
+ if [ ! -f ".env" ]; then
85
+ if [ -f ".env.example" ]; then
86
+ cp .env.example .env
87
+ echo " ✓ .env created from .env.example"
88
+ echo " ⚠️ IMPORTANT: Edit .env with your Cloudinary credentials!"
89
+ else
90
+ echo " ⚠️ .env.example not found"
91
+ fi
92
+ else
93
+ echo " ✓ .env already exists"
94
+ fi
95
+
96
+ # Copy config-source.json if doesn't exist
97
+ if [ ! -f "config-source.json" ]; then
98
+ if [ -f "config-source.json.example" ]; then
99
+ cp config-source.json.example config-source.json
100
+ echo " ✓ config-source.json created"
101
+ fi
102
+ else
103
+ echo " ✓ config-source.json already exists"
104
+ fi
105
+
106
+ echo ""
107
+ echo "═══════════════════════════════════════════════════════════"
108
+ echo " ✅ Initialization Complete!"
109
+ echo "═══════════════════════════════════════════════════════════"
110
+ echo ""
111
+ echo "Next steps:"
112
+ echo ""
113
+ echo "1. Edit your credentials:"
114
+ echo " nano .env"
115
+ echo " (Add your CLOUDINARY_CLOUD_NAME, API_KEY, API_SECRET)"
116
+ echo ""
117
+ echo "2. Customize your portfolio:"
118
+ echo " - config/app.json (app settings)"
119
+ echo " - config/categories.json (content types)"
120
+ echo " - config/languages.json (supported languages)"
121
+ echo ""
122
+ echo "3. Add your content:"
123
+ echo " - data/painting.json"
124
+ echo " - data/photography.json"
125
+ echo " - etc..."
126
+ echo ""
127
+ echo "4. Customize translations:"
128
+ echo " - lang/en.json"
129
+ echo " - lang/fr.json"
130
+ echo " - etc..."
131
+ echo ""
132
+ echo "5. Start the backend:"
133
+ echo " python3 admin_api.py"
134
+ echo ""
135
+ echo "6. Open in browser:"
136
+ echo " open index.html"
137
+ echo " or"
138
+ echo " python3 -m http.server 8000"
139
+ echo ""
140
+ echo "📚 See README.md for full documentation"
141
+ echo ""