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/.github/workflows/build-and-deploy.yml +105 -0
- package/ADMIN_SETUP.md +306 -0
- package/README.md +300 -0
- package/USAGE.md +451 -0
- package/build.sh +326 -0
- package/index.html +85 -0
- package/js/manifest-loader.js +313 -0
- package/manifest.json +74 -0
- package/package.json +24 -0
- package/setup-admin.sh +134 -0
- package/sync-after-admin.sh +58 -0
|
@@ -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 ""
|