@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.
- package/README.md +408 -0
- package/bin/cli.js +103 -0
- package/engine/admin/admin.css +720 -0
- package/engine/admin/admin.html +801 -0
- package/engine/admin/admin_api.py +230 -0
- package/engine/admin/scripts/backup.sh +116 -0
- package/engine/admin/scripts/config_loader.py +180 -0
- package/engine/admin/scripts/init.sh +141 -0
- package/engine/admin/scripts/manager.py +308 -0
- package/engine/admin/scripts/restore.sh +121 -0
- package/engine/admin/scripts/server.py +41 -0
- package/engine/admin/scripts/update.sh +321 -0
- package/engine/admin/scripts/validate_json.py +62 -0
- package/engine/fonts.css +37 -0
- package/engine/index.html +190 -0
- package/engine/js/config-loader.js +370 -0
- package/engine/js/config.js +173 -0
- package/engine/js/counter.js +17 -0
- package/engine/js/effects.js +97 -0
- package/engine/js/i18n.js +68 -0
- package/engine/js/init.js +107 -0
- package/engine/js/media.js +264 -0
- package/engine/js/render.js +282 -0
- package/engine/js/router.js +133 -0
- package/engine/js/sparkle.js +123 -0
- package/engine/js/themes.js +607 -0
- package/engine/style.css +2037 -0
- package/index.js +35 -0
- package/package.json +48 -0
- package/scripts/admin.js +67 -0
- package/scripts/build.js +142 -0
- package/scripts/init.js +237 -0
- package/scripts/post-install.js +16 -0
- package/scripts/serve.js +54 -0
- package/templates/user-portfolio/.github/workflows/deploy.yml +57 -0
- package/templates/user-portfolio/config/app.json +36 -0
- package/templates/user-portfolio/config/categories.json +241 -0
- package/templates/user-portfolio/config/languages.json +15 -0
- package/templates/user-portfolio/config/media-types.json +59 -0
- package/templates/user-portfolio/data/painting.json +3 -0
- package/templates/user-portfolio/data/projects.json +3 -0
- package/templates/user-portfolio/lang/en.json +114 -0
- 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 ""
|