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.
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Manifest Loader - Site-as-a-Package
3
+ * Loads distributed manifest and orchestrates data fetching
4
+ */
5
+
6
+ const ManifestLoader = {
7
+ manifest: null,
8
+ buildInfo: null,
9
+ loaded: false,
10
+
11
+ /**
12
+ * Initialize and load the manifest
13
+ */
14
+ async init(manifestUrl) {
15
+ try {
16
+ console.log('📦 Loading manifest...');
17
+
18
+ // Try to load build-time manifest first (for static builds)
19
+ const buildInfo = await this.loadBuildInfo();
20
+
21
+ if (buildInfo) {
22
+ console.log('✅ Using build-time configuration');
23
+ this.buildInfo = buildInfo;
24
+ return this.loadFromBuildInfo();
25
+ }
26
+
27
+ // Fallback to runtime loading (for development)
28
+ console.log('🔄 Loading manifest at runtime...');
29
+ return this.loadManifestRuntime(manifestUrl);
30
+ } catch (error) {
31
+ console.error('❌ Failed to initialize manifest:', error);
32
+ throw error;
33
+ }
34
+ },
35
+
36
+ /**
37
+ * Load build info (created by build.sh)
38
+ */
39
+ async loadBuildInfo() {
40
+ try {
41
+ const response = await fetch('build-info.json');
42
+ if (!response.ok) return null;
43
+ return await response.json();
44
+ } catch (error) {
45
+ return null;
46
+ }
47
+ },
48
+
49
+ /**
50
+ * Load from pre-built files (build.sh already downloaded everything)
51
+ */
52
+ async loadFromBuildInfo() {
53
+ // When using build.sh, all files are already local
54
+ // Just use local paths
55
+ this.manifest = {
56
+ source: 'build',
57
+ paths: {
58
+ configDir: 'config',
59
+ dataDir: 'data',
60
+ langDir: 'lang'
61
+ }
62
+ };
63
+
64
+ this.loaded = true;
65
+ return this.manifest;
66
+ },
67
+
68
+ /**
69
+ * Load manifest at runtime (development mode)
70
+ */
71
+ async loadManifestRuntime(manifestUrl) {
72
+ const response = await fetch(manifestUrl);
73
+
74
+ if (!response.ok) {
75
+ throw new Error(`HTTP ${response.status}: ${manifestUrl}`);
76
+ }
77
+
78
+ const manifest = await response.json();
79
+
80
+ // Validate manifest
81
+ this.validateManifest(manifest);
82
+
83
+ this.manifest = {
84
+ source: 'runtime',
85
+ data: manifest,
86
+ paths: {
87
+ configDir: manifest.dataRepository.baseUrl + 'config',
88
+ dataDir: manifest.dataRepository.baseUrl + 'data',
89
+ langDir: manifest.dataRepository.baseUrl + 'lang'
90
+ }
91
+ };
92
+
93
+ this.loaded = true;
94
+ return this.manifest;
95
+ },
96
+
97
+ /**
98
+ * Validate manifest structure
99
+ */
100
+ validateManifest(manifest) {
101
+ const required = ['version', 'dataRepository', 'structure'];
102
+
103
+ for (const field of required) {
104
+ if (!manifest[field]) {
105
+ throw new Error(`Invalid manifest: missing "${field}"`);
106
+ }
107
+ }
108
+
109
+ if (!manifest.dataRepository.baseUrl) {
110
+ throw new Error('Invalid manifest: missing dataRepository.baseUrl');
111
+ }
112
+ },
113
+
114
+ /**
115
+ * Get configuration file URL
116
+ */
117
+ getConfigUrl(configName) {
118
+ if (!this.loaded) {
119
+ throw new Error('Manifest not loaded');
120
+ }
121
+
122
+ const { paths } = this.manifest;
123
+
124
+ if (this.manifest.source === 'build') {
125
+ // Local files (after build.sh)
126
+ return `${paths.configDir}/${configName}.json`;
127
+ } else {
128
+ // Remote files (runtime)
129
+ const relativePath = this.manifest.data.structure.config[configName];
130
+ return `${this.manifest.data.dataRepository.baseUrl}${relativePath}`;
131
+ }
132
+ },
133
+
134
+ /**
135
+ * Get data file URL
136
+ */
137
+ getDataUrl(dataName) {
138
+ if (!this.loaded) {
139
+ throw new Error('Manifest not loaded');
140
+ }
141
+
142
+ const { paths } = this.manifest;
143
+
144
+ if (this.manifest.source === 'build') {
145
+ // Local files (after build.sh)
146
+ return `${paths.dataDir}/${dataName}.json`;
147
+ } else {
148
+ // Remote files (runtime)
149
+ const relativePath = this.manifest.data.structure.data[dataName];
150
+ return `${this.manifest.data.dataRepository.baseUrl}${relativePath}`;
151
+ }
152
+ },
153
+
154
+ /**
155
+ * Get language file URL
156
+ */
157
+ getLangUrl(langCode) {
158
+ if (!this.loaded) {
159
+ throw new Error('Manifest not loaded');
160
+ }
161
+
162
+ const { paths } = this.manifest;
163
+
164
+ if (this.manifest.source === 'build') {
165
+ // Local files (after build.sh)
166
+ return `${paths.langDir}/${langCode}.json`;
167
+ } else {
168
+ // Remote files (runtime)
169
+ const relativePath = this.manifest.data.structure.lang[langCode];
170
+ return `${this.manifest.data.dataRepository.baseUrl}${relativePath}`;
171
+ }
172
+ },
173
+
174
+ /**
175
+ * Get all available languages from manifest
176
+ */
177
+ getAvailableLanguages() {
178
+ if (!this.loaded) return [];
179
+
180
+ if (this.manifest.source === 'build') {
181
+ // Parse from build info or config
182
+ return ['en', 'fr', 'mx', 'ht'];
183
+ } else {
184
+ return Object.keys(this.manifest.data.structure.lang);
185
+ }
186
+ },
187
+
188
+ /**
189
+ * Get all available data categories
190
+ */
191
+ getAvailableCategories() {
192
+ if (!this.loaded) return [];
193
+
194
+ if (this.manifest.source === 'build') {
195
+ return ['painting', 'drawing', 'photography', 'sculpting', 'music', 'projects'];
196
+ } else {
197
+ return Object.keys(this.manifest.data.structure.data);
198
+ }
199
+ }
200
+ };
201
+
202
+ /**
203
+ * Enhanced AppConfig that uses ManifestLoader
204
+ */
205
+ const AppConfig = {
206
+ app: null,
207
+ languages: null,
208
+ categories: null,
209
+ mediaTypes: null,
210
+ loaded: false,
211
+ source: 'manifest',
212
+
213
+ /**
214
+ * Load all configuration files using manifest
215
+ */
216
+ async load(manifestUrl = './manifest.json') {
217
+ try {
218
+ // Initialize manifest loader
219
+ await ManifestLoader.init(manifestUrl);
220
+
221
+ // Load all config files
222
+ const [app, languages, categories, mediaTypes] = await Promise.all([
223
+ this.fetchJson(ManifestLoader.getConfigUrl('app')),
224
+ this.fetchJson(ManifestLoader.getConfigUrl('languages')),
225
+ this.fetchJson(ManifestLoader.getConfigUrl('categories')),
226
+ this.fetchJson(ManifestLoader.getConfigUrl('mediaTypes'))
227
+ ]);
228
+
229
+ this.app = app;
230
+ this.languages = languages;
231
+ this.categories = categories;
232
+ this.mediaTypes = mediaTypes;
233
+ this.loaded = true;
234
+
235
+ console.log(`✅ Configuration loaded (source: ${ManifestLoader.manifest.source})`);
236
+ return true;
237
+ } catch (error) {
238
+ console.error('❌ Failed to load configuration:', error);
239
+ return false;
240
+ }
241
+ },
242
+
243
+ /**
244
+ * Fetch JSON with error handling
245
+ */
246
+ async fetchJson(url) {
247
+ const response = await fetch(url);
248
+ if (!response.ok) {
249
+ throw new Error(`HTTP ${response.status}: ${url}`);
250
+ }
251
+ return response.json();
252
+ },
253
+
254
+ /**
255
+ * Get API base URL
256
+ */
257
+ getApiUrl() {
258
+ return this.app?.api?.baseUrl || 'http://127.0.0.1:5001';
259
+ },
260
+
261
+ /**
262
+ * Get supported language codes
263
+ */
264
+ getLanguageCodes() {
265
+ return this.languages?.supportedLanguages.map(l => l.code) || ['en'];
266
+ },
267
+
268
+ /**
269
+ * Get default language
270
+ */
271
+ getDefaultLanguage() {
272
+ return this.languages?.defaultLanguage || 'en';
273
+ },
274
+
275
+ /**
276
+ * Get all content types
277
+ */
278
+ getAllContentTypes() {
279
+ return this.categories?.contentTypes || this.categories?.categories || [];
280
+ },
281
+
282
+ /**
283
+ * Get content type configuration by ID
284
+ */
285
+ getContentType(contentTypeId) {
286
+ return this.getAllContentTypes().find(c => c.id === contentTypeId);
287
+ },
288
+
289
+ /**
290
+ * Get all media types
291
+ */
292
+ getAllMediaTypes() {
293
+ return this.mediaTypes?.mediaTypes || [];
294
+ },
295
+
296
+ /**
297
+ * Get media type configuration by ID
298
+ */
299
+ getMediaType(mediaTypeId) {
300
+ return this.getAllMediaTypes().find(m => m.id === mediaTypeId);
301
+ },
302
+
303
+ /**
304
+ * Get data file path for a category (using manifest)
305
+ */
306
+ getCategoryDataFile(categoryId) {
307
+ return ManifestLoader.getDataUrl(categoryId);
308
+ }
309
+ };
310
+
311
+ // Export for use in other modules
312
+ window.AppConfig = AppConfig;
313
+ window.ManifestLoader = ManifestLoader;
package/manifest.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "_comment": "Manifest distribué - Point d'entrée pour Site-as-a-Package",
3
+ "version": "1.0.0",
4
+ "name": "Retro Portfolio",
5
+ "engine": {
6
+ "minVersion": "1.0.0",
7
+ "repository": "https://github.com/mtldev514/retro-portfolio-engine"
8
+ },
9
+ "dataRepository": {
10
+ "type": "github",
11
+ "owner": "YOUR_USERNAME",
12
+ "repo": "retro-portfolio-data",
13
+ "branch": "main",
14
+ "baseUrl": "https://raw.githubusercontent.com/YOUR_USERNAME/retro-portfolio-data/main/"
15
+ },
16
+ "structure": {
17
+ "config": {
18
+ "app": "config/app.json",
19
+ "languages": "config/languages.json",
20
+ "categories": "config/categories.json",
21
+ "mediaTypes": "config/media-types.json"
22
+ },
23
+ "data": {
24
+ "painting": "data/painting.json",
25
+ "drawing": "data/drawing.json",
26
+ "photography": "data/photography.json",
27
+ "sculpting": "data/sculpting.json",
28
+ "music": "data/music.json",
29
+ "projects": "data/projects.json"
30
+ },
31
+ "lang": {
32
+ "en": "lang/en.json",
33
+ "fr": "lang/fr.json",
34
+ "mx": "lang/mx.json",
35
+ "ht": "lang/ht.json"
36
+ },
37
+ "assets": {
38
+ "styles": ["style.css", "fonts.css"],
39
+ "scripts": [
40
+ "js/config-loader.js",
41
+ "js/i18n.js",
42
+ "js/themes.js",
43
+ "js/render.js",
44
+ "js/router.js",
45
+ "js/media.js",
46
+ "js/sparkle.js",
47
+ "js/effects.js",
48
+ "js/counter.js",
49
+ "js/init.js"
50
+ ]
51
+ }
52
+ },
53
+ "build": {
54
+ "cache": {
55
+ "enabled": true,
56
+ "directory": ".cache",
57
+ "ttl": 3600
58
+ },
59
+ "output": {
60
+ "directory": "dist",
61
+ "clean": true
62
+ }
63
+ },
64
+ "deployment": {
65
+ "target": "github-pages",
66
+ "autoUpdate": true,
67
+ "schedule": "0 0 * * *"
68
+ },
69
+ "metadata": {
70
+ "author": "Alex",
71
+ "buildDate": "2026-02-12",
72
+ "lastUpdate": "2026-02-12T00:00:00Z"
73
+ }
74
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "retro-portfolio-engine",
3
+ "version": "1.0.0",
4
+ "description": "Site-as-a-Package engine for retro portfolio",
5
+ "scripts": {
6
+ "build": "./build.sh",
7
+ "build:force": "./build.sh --force",
8
+ "dev": "cd dist && python3 -m http.server 8000",
9
+ "admin:setup": "./setup-admin.sh",
10
+ "admin:sync": "./sync-after-admin.sh"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/YOUR_USERNAME/retro-portfolio-engine"
15
+ },
16
+ "keywords": [
17
+ "portfolio",
18
+ "site-as-a-package",
19
+ "static-site",
20
+ "retro"
21
+ ],
22
+ "author": "Alex",
23
+ "license": "MIT"
24
+ }
package/setup-admin.sh ADDED
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+
3
+ ##############################################################################
4
+ # Admin Setup Script for Site-as-a-Package
5
+ # Sets up local admin to work with the data repository
6
+ ##############################################################################
7
+
8
+ set -e
9
+
10
+ # Colors
11
+ GREEN='\033[0;32m'
12
+ BLUE='\033[0;34m'
13
+ YELLOW='\033[1;33m'
14
+ NC='\033[0m'
15
+
16
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
17
+ echo -e "${BLUE} 🔧 Admin Setup for Site-as-a-Package${NC}"
18
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
19
+ echo ""
20
+
21
+ # Check if data repo path is provided
22
+ if [ -z "$1" ]; then
23
+ echo -e "${YELLOW}Usage:${NC} ./setup-admin.sh <path-to-retro-portfolio-data>"
24
+ echo ""
25
+ echo "Example:"
26
+ echo " ./setup-admin.sh ../retro-portfolio-data"
27
+ echo ""
28
+ exit 1
29
+ fi
30
+
31
+ DATA_REPO_PATH="$1"
32
+
33
+ # Validate data repo exists
34
+ if [ ! -d "$DATA_REPO_PATH" ]; then
35
+ echo -e "${YELLOW}⚠${NC} Data repository not found at: $DATA_REPO_PATH"
36
+ echo ""
37
+ echo "Please clone it first:"
38
+ echo " git clone https://github.com/YOUR_USERNAME/retro-portfolio-data.git $DATA_REPO_PATH"
39
+ exit 1
40
+ fi
41
+
42
+ # Validate required directories exist
43
+ for dir in "config" "data" "lang"; do
44
+ if [ ! -d "$DATA_REPO_PATH/$dir" ]; then
45
+ echo -e "${YELLOW}⚠${NC} Missing directory: $DATA_REPO_PATH/$dir"
46
+ exit 1
47
+ fi
48
+ done
49
+
50
+ echo -e "${GREEN}✓${NC} Data repository found at: $DATA_REPO_PATH"
51
+ echo ""
52
+
53
+ # Create admin-local directory
54
+ ADMIN_DIR="admin-local"
55
+ if [ -d "$ADMIN_DIR" ]; then
56
+ echo -e "${YELLOW}⚠${NC} $ADMIN_DIR already exists. Remove it first or use a different name."
57
+ exit 1
58
+ fi
59
+
60
+ mkdir -p "$ADMIN_DIR"
61
+ echo -e "${GREEN}✓${NC} Created $ADMIN_DIR directory"
62
+
63
+ # Create symbolic links to data repo directories
64
+ ln -s "$(realpath $DATA_REPO_PATH)/config" "$ADMIN_DIR/config"
65
+ ln -s "$(realpath $DATA_REPO_PATH)/data" "$ADMIN_DIR/data"
66
+ ln -s "$(realpath $DATA_REPO_PATH)/lang" "$ADMIN_DIR/lang"
67
+
68
+ echo -e "${GREEN}✓${NC} Linked config, data, and lang directories"
69
+
70
+ # Create README
71
+ cat > "$ADMIN_DIR/README.md" << 'EOF'
72
+ # Admin Local
73
+
74
+ This directory contains the admin interface for managing your portfolio data.
75
+
76
+ ## Usage
77
+
78
+ 1. Start the admin API:
79
+ ```bash
80
+ python3 admin_api.py
81
+ ```
82
+
83
+ 2. Open admin interface:
84
+ ```
85
+ http://localhost:5001/admin.html
86
+ ```
87
+
88
+ 3. After making changes, commit and push:
89
+ ```bash
90
+ cd ../retro-portfolio-data
91
+ git add data/ lang/
92
+ git commit -m "Update content via admin"
93
+ git push
94
+ ```
95
+
96
+ The site will rebuild automatically on push.
97
+
98
+ ## Directory Structure
99
+
100
+ - `admin.html` - Admin interface
101
+ - `admin_api.py` - Backend API
102
+ - `config/` → symlink to data repo
103
+ - `data/` → symlink to data repo
104
+ - `lang/` → symlink to data repo
105
+ EOF
106
+
107
+ echo -e "${GREEN}✓${NC} Created README"
108
+
109
+ # Create instructions
110
+ echo ""
111
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
112
+ echo -e "${GREEN}✓ Setup Complete!${NC}"
113
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
114
+ echo ""
115
+ echo "Next steps:"
116
+ echo ""
117
+ echo " 1. Copy your existing admin files:"
118
+ echo " ${YELLOW}cp ../retro-portfolio/admin.html $ADMIN_DIR/${NC}"
119
+ echo " ${YELLOW}cp ../retro-portfolio/admin.css $ADMIN_DIR/${NC}"
120
+ echo " ${YELLOW}cp ../retro-portfolio/admin_api.py $ADMIN_DIR/${NC}"
121
+ echo " ${YELLOW}cp -r ../retro-portfolio/scripts $ADMIN_DIR/${NC}"
122
+ echo " ${YELLOW}cp ../retro-portfolio/.env $ADMIN_DIR/${NC}"
123
+ echo ""
124
+ echo " 2. Start the admin:"
125
+ echo " ${YELLOW}cd $ADMIN_DIR${NC}"
126
+ echo " ${YELLOW}python3 admin_api.py${NC}"
127
+ echo ""
128
+ echo " 3. Open in browser:"
129
+ echo " ${YELLOW}http://localhost:5001/admin.html${NC}"
130
+ echo ""
131
+ echo " 4. After changes, commit and push from data repo:"
132
+ echo " ${YELLOW}cd $DATA_REPO_PATH${NC}"
133
+ echo " ${YELLOW}git add . && git commit -m 'Update via admin' && git push${NC}"
134
+ echo ""
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+
3
+ ##############################################################################
4
+ # Quick Sync Script
5
+ # Commits and pushes data changes after using admin
6
+ ##############################################################################
7
+
8
+ set -e
9
+
10
+ GREEN='\033[0;32m'
11
+ BLUE='\033[0;34m'
12
+ NC='\033[0m'
13
+
14
+ # Default data repo path (can be overridden)
15
+ DATA_REPO="${DATA_REPO:-../retro-portfolio-data}"
16
+
17
+ if [ ! -d "$DATA_REPO" ]; then
18
+ echo "❌ Data repository not found at: $DATA_REPO"
19
+ echo "Set DATA_REPO environment variable or pass path as argument"
20
+ exit 1
21
+ fi
22
+
23
+ cd "$DATA_REPO"
24
+
25
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
26
+ echo -e "${BLUE} 📤 Syncing Data Changes${NC}"
27
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
28
+ echo ""
29
+
30
+ # Check if there are changes
31
+ if git diff --quiet && git diff --cached --quiet; then
32
+ echo "✓ No changes to commit"
33
+ exit 0
34
+ fi
35
+
36
+ # Show what changed
37
+ echo "Changes detected:"
38
+ git status --short
39
+
40
+ echo ""
41
+ read -p "Commit message (or press Enter for default): " MESSAGE
42
+
43
+ if [ -z "$MESSAGE" ]; then
44
+ MESSAGE="Update content via admin - $(date '+%Y-%m-%d %H:%M')"
45
+ fi
46
+
47
+ # Commit and push
48
+ git add config/ data/ lang/
49
+ git commit -m "$MESSAGE"
50
+ git push
51
+
52
+ echo ""
53
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
54
+ echo -e "${GREEN}✓ Changes pushed successfully!${NC}"
55
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
56
+ echo ""
57
+ echo "Your site will rebuild automatically within a few moments."
58
+ echo ""