@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,321 @@
1
+ #!/bin/bash
2
+
3
+ # Retro Portfolio Update Script
4
+ # Safely updates the template while preserving user data
5
+
6
+ set -e
7
+
8
+ echo ""
9
+ echo "═══════════════════════════════════════════════════════════"
10
+ echo " 🔄 Retro Portfolio - Update Script"
11
+ echo "═══════════════════════════════════════════════════════════"
12
+ echo ""
13
+
14
+ # Colors for output
15
+ RED='\033[0;31m'
16
+ GREEN='\033[0;32m'
17
+ YELLOW='\033[1;33m'
18
+ BLUE='\033[0;34m'
19
+ NC='\033[0m' # No Color
20
+
21
+ # Detect setup type
22
+ SETUP_TYPE="unknown"
23
+
24
+ if [ -d "template/.git" ]; then
25
+ SETUP_TYPE="submodule"
26
+ elif [ -d ".git" ]; then
27
+ # Check if this is the template repo or a user's fork
28
+ REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
29
+ if [[ "$REMOTE_URL" == *"retro-portfolio"* ]] && [[ "$REMOTE_URL" != *"retro-portfolio-config"* ]]; then
30
+ SETUP_TYPE="direct"
31
+ else
32
+ SETUP_TYPE="fork"
33
+ fi
34
+ fi
35
+
36
+ echo -e "${BLUE}📋 Setup type detected: $SETUP_TYPE${NC}"
37
+ echo ""
38
+
39
+ # Function to create backup
40
+ create_backup() {
41
+ echo -e "${YELLOW}📦 Creating backup...${NC}"
42
+
43
+ BACKUP_DIR=".backup-personal-$(date +%Y%m%d-%H%M%S)"
44
+ mkdir -p "$BACKUP_DIR"
45
+
46
+ # Backup user data
47
+ [ -d "config" ] && cp -r config "$BACKUP_DIR/" && echo " ✓ Backed up config/"
48
+ [ -d "data" ] && cp -r data "$BACKUP_DIR/" && echo " ✓ Backed up data/"
49
+ [ -d "lang" ] && cp -r lang "$BACKUP_DIR/" && echo " ✓ Backed up lang/"
50
+ [ -f ".env" ] && cp .env "$BACKUP_DIR/" && echo " ✓ Backed up .env"
51
+ [ -f "config-source.json" ] && cp config-source.json "$BACKUP_DIR/" && echo " ✓ Backed up config-source.json"
52
+
53
+ echo -e "${GREEN} ✓ Backup created: $BACKUP_DIR${NC}"
54
+ echo ""
55
+ }
56
+
57
+ # Function to verify data integrity
58
+ verify_data() {
59
+ echo -e "${YELLOW}🔍 Verifying data integrity...${NC}"
60
+
61
+ ERRORS=0
62
+
63
+ # Check essential directories exist
64
+ if [ ! -d "config" ] || [ ! -f "config/app.json" ]; then
65
+ echo -e "${RED} ✗ config/ missing or incomplete${NC}"
66
+ ERRORS=$((ERRORS + 1))
67
+ else
68
+ echo -e "${GREEN} ✓ config/ intact${NC}"
69
+ fi
70
+
71
+ if [ ! -d "data" ]; then
72
+ echo -e "${RED} ✗ data/ missing${NC}"
73
+ ERRORS=$((ERRORS + 1))
74
+ else
75
+ echo -e "${GREEN} ✓ data/ intact${NC}"
76
+ fi
77
+
78
+ if [ ! -d "lang" ]; then
79
+ echo -e "${RED} ✗ lang/ missing${NC}"
80
+ ERRORS=$((ERRORS + 1))
81
+ else
82
+ echo -e "${GREEN} ✓ lang/ intact${NC}"
83
+ fi
84
+
85
+ if [ ! -f ".env" ]; then
86
+ echo -e "${YELLOW} ⚠️ .env missing (you may need to recreate it)${NC}"
87
+ else
88
+ echo -e "${GREEN} ✓ .env intact${NC}"
89
+ fi
90
+
91
+ echo ""
92
+
93
+ if [ $ERRORS -gt 0 ]; then
94
+ echo -e "${RED}❌ Data verification failed! Restore from backup.${NC}"
95
+ return 1
96
+ fi
97
+
98
+ echo -e "${GREEN}✅ All data verified successfully!${NC}"
99
+ echo ""
100
+ return 0
101
+ }
102
+
103
+ # Function to update submodule setup
104
+ update_submodule() {
105
+ echo -e "${BLUE}📥 Updating template submodule...${NC}"
106
+ echo ""
107
+
108
+ cd template
109
+
110
+ # Check current version
111
+ CURRENT_COMMIT=$(git rev-parse --short HEAD)
112
+ echo "Current version: $CURRENT_COMMIT"
113
+
114
+ # Pull latest
115
+ git fetch origin
116
+ git pull origin main
117
+
118
+ # Check new version
119
+ NEW_COMMIT=$(git rev-parse --short HEAD)
120
+ echo "New version: $NEW_COMMIT"
121
+
122
+ cd ..
123
+
124
+ if [ "$CURRENT_COMMIT" != "$NEW_COMMIT" ]; then
125
+ echo ""
126
+ echo -e "${GREEN}✅ Template updated successfully!${NC}"
127
+ echo ""
128
+ echo -e "${YELLOW}Don't forget to commit the update:${NC}"
129
+ echo " git add template"
130
+ echo " git commit -m \"Update template to $NEW_COMMIT\""
131
+ else
132
+ echo ""
133
+ echo -e "${GREEN}✅ Already on latest version!${NC}"
134
+ fi
135
+
136
+ echo ""
137
+ }
138
+
139
+ # Function to update direct/fork setup
140
+ update_direct() {
141
+ echo -e "${BLUE}📥 Updating repository...${NC}"
142
+ echo ""
143
+
144
+ # Check current version
145
+ CURRENT_COMMIT=$(git rev-parse --short HEAD)
146
+ echo "Current version: $CURRENT_COMMIT"
147
+
148
+ # Check for uncommitted changes in tracked files
149
+ if ! git diff-index --quiet HEAD -- 2>/dev/null; then
150
+ echo -e "${YELLOW}⚠️ You have uncommitted changes in tracked files.${NC}"
151
+ echo ""
152
+ read -p "Stash changes before updating? (recommended) (Y/n): " -n 1 -r
153
+ echo
154
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
155
+ git stash push -m "Auto-stash before update $(date)"
156
+ STASHED=true
157
+ fi
158
+ fi
159
+
160
+ # Pull latest changes
161
+ if [ "$SETUP_TYPE" == "fork" ]; then
162
+ # Check if upstream exists
163
+ if ! git remote get-url upstream > /dev/null 2>&1; then
164
+ echo -e "${YELLOW}Adding upstream remote...${NC}"
165
+ git remote add upstream https://github.com/yourusername/retro-portfolio.git
166
+ fi
167
+
168
+ git fetch upstream
169
+ git merge upstream/main
170
+ else
171
+ git pull origin main
172
+ fi
173
+
174
+ # Pop stash if we stashed
175
+ if [ "$STASHED" = true ]; then
176
+ echo ""
177
+ echo -e "${YELLOW}Restoring your changes...${NC}"
178
+ git stash pop || echo -e "${YELLOW}⚠️ Could not auto-restore stash. Run 'git stash pop' manually.${NC}"
179
+ fi
180
+
181
+ # Check new version
182
+ NEW_COMMIT=$(git rev-parse --short HEAD)
183
+ echo "New version: $NEW_COMMIT"
184
+
185
+ echo ""
186
+ if [ "$CURRENT_COMMIT" != "$NEW_COMMIT" ]; then
187
+ echo -e "${GREEN}✅ Repository updated successfully!${NC}"
188
+ else
189
+ echo -e "${GREEN}✅ Already on latest version!${NC}"
190
+ fi
191
+
192
+ echo ""
193
+ }
194
+
195
+ # Function to show what changed
196
+ show_changes() {
197
+ echo -e "${BLUE}📝 Recent changes:${NC}"
198
+ echo ""
199
+
200
+ if [ -f "CHANGELOG.md" ]; then
201
+ # Show last 20 lines of changelog
202
+ tail -n 20 CHANGELOG.md
203
+ else
204
+ # Show recent commits
205
+ git log --oneline --decorate -5
206
+ fi
207
+
208
+ echo ""
209
+ }
210
+
211
+ # Function to check for new example files
212
+ check_new_examples() {
213
+ echo -e "${BLUE}🆕 Checking for new features...${NC}"
214
+ echo ""
215
+
216
+ NEW_FEATURES=false
217
+
218
+ # Check for new config examples
219
+ if [ -d "config.example" ]; then
220
+ for example_file in config.example/*.example; do
221
+ if [ -f "$example_file" ]; then
222
+ base_name=$(basename "$example_file" .example)
223
+ user_file="config/$base_name"
224
+
225
+ # If user file exists, check for new fields
226
+ if [ -f "$user_file" ]; then
227
+ # Simple check: compare file sizes (crude but fast)
228
+ EXAMPLE_SIZE=$(wc -c < "$example_file")
229
+ USER_SIZE=$(wc -c < "$user_file")
230
+
231
+ if [ "$EXAMPLE_SIZE" -gt "$USER_SIZE" ]; then
232
+ echo -e "${YELLOW} ℹ️ New options may be available in: $base_name${NC}"
233
+ echo " Compare: diff config/$base_name config.example/$base_name.example"
234
+ NEW_FEATURES=true
235
+ fi
236
+ fi
237
+ fi
238
+ done
239
+ fi
240
+
241
+ if [ "$NEW_FEATURES" = false ]; then
242
+ echo -e "${GREEN} ✓ No new configuration options detected${NC}"
243
+ fi
244
+
245
+ echo ""
246
+ }
247
+
248
+ # Main update flow
249
+ main() {
250
+ # Create backup first
251
+ create_backup
252
+
253
+ # Store current directory
254
+ ORIGINAL_DIR=$(pwd)
255
+
256
+ # Update based on setup type
257
+ case $SETUP_TYPE in
258
+ submodule)
259
+ update_submodule
260
+ ;;
261
+ direct|fork)
262
+ update_direct
263
+ ;;
264
+ *)
265
+ echo -e "${RED}❌ Could not detect setup type${NC}"
266
+ echo "Please update manually. See UPDATE.md for instructions."
267
+ exit 1
268
+ ;;
269
+ esac
270
+
271
+ # Return to original directory
272
+ cd "$ORIGINAL_DIR"
273
+
274
+ # Verify data integrity
275
+ if ! verify_data; then
276
+ echo -e "${RED}❌ Update completed but data verification failed!${NC}"
277
+ echo -e "${YELLOW}Restore from backup:${NC}"
278
+ echo " cp -r $BACKUP_DIR/* ."
279
+ exit 1
280
+ fi
281
+
282
+ # Show what changed
283
+ show_changes
284
+
285
+ # Check for new examples
286
+ check_new_examples
287
+
288
+ # Final instructions
289
+ echo "═══════════════════════════════════════════════════════════"
290
+ echo -e "${GREEN} ✅ Update Complete!${NC}"
291
+ echo "═══════════════════════════════════════════════════════════"
292
+ echo ""
293
+ echo "Next steps:"
294
+ echo ""
295
+ echo "1. Review changes:"
296
+ echo " cat CHANGELOG.md"
297
+ echo ""
298
+ echo "2. Test locally:"
299
+ echo " python3 -m http.server 8000"
300
+ echo " open http://localhost:8000"
301
+ echo ""
302
+ echo "3. Clear browser cache (Cmd+Shift+R or Ctrl+Shift+R)"
303
+ echo ""
304
+ echo "4. Deploy updated site:"
305
+ echo " git push"
306
+ echo ""
307
+
308
+ if [ "$SETUP_TYPE" == "submodule" ]; then
309
+ echo "5. Commit the template update:"
310
+ echo " git add template"
311
+ echo " git commit -m \"Update template\""
312
+ echo " git push"
313
+ echo ""
314
+ fi
315
+
316
+ echo -e "${YELLOW}💡 Backup saved to: $BACKUP_DIR${NC}"
317
+ echo ""
318
+ }
319
+
320
+ # Run main
321
+ main
@@ -0,0 +1,62 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import glob
5
+
6
+ def validate_json_files(directories):
7
+ """
8
+ Recursively finds and validates all .json files in the given directories.
9
+ Returns True if all files are valid, False otherwise.
10
+ """
11
+ has_errors = False
12
+ total_files = 0
13
+ valid_files = 0
14
+
15
+ print("🔍 Starting JSON validation...")
16
+
17
+ for directory in directories:
18
+ if not os.path.isdir(directory):
19
+ print(f"⚠️ Directory not found: {directory}")
20
+ continue
21
+
22
+ # Use glob to find all json files recursively
23
+ files = glob.glob(os.path.join(directory, "**/*.json"), recursive=True)
24
+
25
+ for file_path in files:
26
+ total_files += 1
27
+ try:
28
+ with open(file_path, 'r', encoding='utf-8') as f:
29
+ json.load(f)
30
+ valid_files += 1
31
+ # print(f"✅ Valid: {file_path}") # Optional: Uncomment for verbose output
32
+ except json.JSONDecodeError as e:
33
+ print(f"❌ Syntax Error in {file_path}:")
34
+ print(f" Line {e.lineno}, Column {e.colno}: {e.msg}")
35
+ has_errors = True
36
+ except Exception as e:
37
+ print(f"❌ Error reading {file_path}: {e}")
38
+ has_errors = True
39
+
40
+ print("\n📊 Validation Summary")
41
+ print(f" Total Files: {total_files}")
42
+ print(f" Valid Files: {valid_files}")
43
+ print(f" Invalid Files: {total_files - valid_files}")
44
+
45
+ return not has_errors
46
+
47
+ if __name__ == "__main__":
48
+ # Directories to validate
49
+ dirs_to_check = ["data", "config", "lang"]
50
+
51
+ # Adjust paths if running from root or scripts dir
52
+ if os.path.basename(os.getcwd()) == "scripts":
53
+ os.chdir("..")
54
+
55
+ success = validate_json_files(dirs_to_check)
56
+
57
+ if success:
58
+ print("\n✨ All JSON files are valid!")
59
+ sys.exit(0)
60
+ else:
61
+ print("\n💥 Validation failed. Please fix the errors above.")
62
+ sys.exit(1)
@@ -0,0 +1,37 @@
1
+ /* ═══ Typography — independent of theme/style ═══ */
2
+
3
+ /* Base */
4
+ body {
5
+ font-size: 14px;
6
+ line-height: 1.5;
7
+ }
8
+
9
+ /* Gallery cards */
10
+ .gallery-item h3 {
11
+ font-size: 0.95em;
12
+ margin: 4px 5px 0;
13
+ line-height: 1.3;
14
+ }
15
+
16
+ .gallery-item .gallery-subtitle {
17
+ font-size: 0.85em;
18
+ margin: 1px 5px 0;
19
+ line-height: 1.3;
20
+ }
21
+
22
+ /* Dates — switch from monospace to primary font */
23
+ .item-date {
24
+ font-size: 0.75em;
25
+ font-family: var(--font-primary);
26
+ }
27
+
28
+ /* UI controls — bump from below-legibility sizes */
29
+ .sort-btn { font-size: 11px; }
30
+
31
+ /* Winamp — minimal bumps to preserve skin authenticity */
32
+ .winamp-title { font-size: 10px; }
33
+ .winamp-pl-item { font-size: 11px; }
34
+
35
+ /* Terminal — sharper text */
36
+ .terminal-body { font-size: 12px; }
37
+ .terminal-line { text-shadow: 0 0 2px var(--term-glow, rgba(0,255,0,0.2)); }
@@ -0,0 +1,190 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title data-i18n="header_title">alex a montreal</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="stylesheet" href="fonts.css">
10
+
11
+ <!-- Load configuration first (supports remote/local sources) -->
12
+ <script src="js/config-loader.js"></script>
13
+ <script src="js/i18n.js" defer></script>
14
+ <script src="js/themes.js" defer></script>
15
+ <script src="js/render.js" defer></script>
16
+ <script src="js/router.js" defer></script>
17
+ <script src="js/media.js" defer></script>
18
+ <script src="js/sparkle.js" defer></script>
19
+ <script src="js/effects.js" defer></script>
20
+ <!-- Initialize app after all modules loaded -->
21
+ <script src="js/init.js" defer></script>
22
+ </head>
23
+
24
+ <body>
25
+ <div id="rotate-overlay">
26
+ <div class="rotate-content">
27
+ <div class="rotate-icon">📺↩️</div>
28
+ <h2>ROTATE YOUR DEVICE!</h2>
29
+ <p>This website was designed for<br><b>DESKTOP COMPUTERS</b></p>
30
+ <p class="rotate-sub">Please turn your phone sideways<br>for the full retro experience!</p>
31
+ <div class="rotate-ascii">
32
+ ┌─────────────────┐<br>
33
+ │&nbsp;&nbsp;&nbsp;╔══════════╗&nbsp;&nbsp;&nbsp;│<br>
34
+ │&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;TURN&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;&nbsp;│<br>
35
+ │&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;&nbsp;ME!&nbsp;&nbsp;&nbsp;║&nbsp;&nbsp;&nbsp;│<br>
36
+ │&nbsp;&nbsp;&nbsp;╚══════════╝&nbsp;&nbsp;&nbsp;│<br>
37
+ └─────────────────┘<br>
38
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;⟲
39
+ </div>
40
+ <p class="rotate-blink">*** LANDSCAPE MODE REQUIRED ***</p>
41
+ </div>
42
+ </div>
43
+ <div class="container">
44
+ <header>
45
+ <div class="settings-switcher">
46
+ <button class="settings-btn">⚙</button>
47
+ <div class="settings-dropdown">
48
+ <div class="settings-section-label">Effects</div>
49
+ <div class="settings-option" onclick="togglePartyMode()">
50
+ <span id="party-mode-indicator">✨</span> Party Mode
51
+ </div>
52
+ <div class="settings-divider"></div>
53
+ <div class="settings-section-label">Theme</div>
54
+ <div class="settings-option" onclick="themes.changeTheme('jr16')"><span class="theme-icon">🌿</span>
55
+ JR-16</div>
56
+ <div class="settings-option" onclick="themes.changeTheme('beton')"><span
57
+ class="theme-icon">🌫️</span> Béton</div>
58
+ <div class="settings-option" onclick="themes.changeTheme('ciment')"><span
59
+ class="theme-icon">🪨</span> Ciment</div>
60
+ <div class="settings-option" onclick="themes.changeTheme('bubblegum')"><span
61
+ class="theme-icon">🍬</span> Bubble Gum</div>
62
+ <div class="settings-divider"></div>
63
+ <div class="settings-section-label">Language</div>
64
+ <div class="settings-option" onclick="i18n.changeLang('en')"><span class="lang-flag">🇬🇧🇨🇦</span>
65
+ English</div>
66
+ <div class="settings-option" onclick="i18n.changeLang('fr')"><span class="lang-flag">⚜️🇨🇦</span>
67
+ French</div>
68
+ <div class="settings-option" onclick="i18n.changeLang('mx')"><span class="lang-flag">🇲🇽</span>
69
+ Spanish</div>
70
+ <div class="settings-option" onclick="i18n.changeLang('ht')"><span class="lang-flag">🇭🇹</span>
71
+ Creole</div>
72
+ </div>
73
+ </div>
74
+ <h1 id="page-title" data-i18n="header_title">alex a montreal</h1>
75
+ </header>
76
+
77
+ <div class="marquee-container">
78
+ <marquee scrollamount="5" data-i18n="marquee"></marquee>
79
+ </div>
80
+
81
+ <div class="content">
82
+ <aside class="sidebar">
83
+ <div class="winamp">
84
+ <div class="winamp-titlebar">
85
+ <span class="winamp-grip"></span>
86
+ <span class="winamp-title">Radyo</span>
87
+ <span class="winamp-grip"></span>
88
+ </div>
89
+ <div class="winamp-display">
90
+ <div class="winamp-time-row">
91
+ <span class="winamp-time" id="winamp-time">00:00</span>
92
+ <span class="winamp-time-sep">/</span>
93
+ <span class="winamp-duration" id="winamp-duration">00:00</span>
94
+ </div>
95
+ <div class="winamp-ticker">
96
+ <span class="radio-track-name" data-i18n="sidebar_radio_title">Your Radio</span> - Winamp 2.91
97
+ </div>
98
+ <div class="winamp-viz" id="winamp-viz"></div>
99
+ <div class="winamp-info">
100
+ <div class="winamp-bitrate"><span class="winamp-kbps">192</span>kbps</div>
101
+ <div class="winamp-freq"><span class="winamp-khz">44</span>kHz</div>
102
+ </div>
103
+ </div>
104
+ <div class="winamp-transport">
105
+ <button class="winamp-btn radio-prev" title="Previous"><svg class="icon" viewBox="0 0 24 24">
106
+ <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" transform="scale(-1,1) translate(-24,0)" />
107
+ </svg></button>
108
+ <button class="winamp-btn radio-playpause" title="Play"><svg class="icon" viewBox="0 0 24 24">
109
+ <path d="M8 5v14l11-7z" />
110
+ </svg></button>
111
+ <button class="winamp-btn radio-next" title="Next"><svg class="icon" viewBox="0 0 24 24">
112
+ <path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
113
+ </svg></button>
114
+ </div>
115
+ <div class="winamp-volume-row">
116
+ <svg class="icon winamp-vol-icon" viewBox="0 0 24 24">
117
+ <path d="M3 9v6h4l5 5V4L7 9H3z" />
118
+ </svg>
119
+ <input type="range" class="winamp-volume radio-volume" value="80" min="0" max="100">
120
+ <svg class="icon winamp-vol-icon" viewBox="0 0 24 24">
121
+ <path
122
+ d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
123
+ </svg>
124
+ </div>
125
+ <div class="winamp-seek-row">
126
+ <input type="range" class="winamp-seek" id="winamp-seek" value="0" min="0" max="100">
127
+ </div>
128
+ <div class="winamp-playlist-titlebar">
129
+ <span class="winamp-grip-sm"></span>
130
+ <span class="winamp-pl-title">PLAYLIST</span>
131
+ <span class="winamp-grip-sm"></span>
132
+ </div>
133
+ <div class="winamp-playlist" id="radio-tracklist">
134
+ <div class="winamp-pl-item winamp-pl-empty" data-i18n="sidebar_radio_loading">Loading tracks...
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </aside>
139
+
140
+ <main id="app">
141
+ <div id="filter-nav" class="filter-bar">
142
+ <button class="filter-btn active" data-filter="all" data-i18n="filter_all">All</button>
143
+ <button class="filter-btn" data-filter="painting" data-i18n="nav_painting">Painting</button>
144
+ <button class="filter-btn" data-filter="drawing" data-i18n="nav_drawing">Drawing</button>
145
+ <button class="filter-btn" data-filter="photography" data-i18n="nav_photo">Photography</button>
146
+ <button class="filter-btn" data-filter="sculpting" data-i18n="nav_sculpting">Sculpting</button>
147
+ <button class="filter-btn" data-filter="music" data-i18n="nav_music">Music</button>
148
+ <button class="filter-btn" data-filter="projects" data-i18n="nav_projects">Code</button>
149
+ <span class="sort-controls">
150
+ <button class="sort-btn active" data-sort="desc" title="Newest first">&#9660;</button>
151
+ <button class="sort-btn" data-sort="asc" title="Oldest first">&#9650;</button>
152
+ </span>
153
+ </div>
154
+ <!-- Gallery grid loaded by render.js -->
155
+ </main>
156
+ </div>
157
+
158
+ <footer class="footer-terminal">
159
+ <div class="terminal-window">
160
+ <div class="terminal-titlebar">
161
+ <div class="terminal-titlebar-btns">
162
+ <span class="terminal-tbtn terminal-tbtn-close"></span>
163
+ <span class="terminal-tbtn terminal-tbtn-min"></span>
164
+ <span class="terminal-tbtn terminal-tbtn-max"></span>
165
+ </div>
166
+ <span class="terminal-titlebar-text">user@host:~</span>
167
+ </div>
168
+ <div class="terminal-body">
169
+ <div class="terminal-line"><span class="terminal-prompt">$</span> <span
170
+ data-i18n="footer_copy">&copy; 2026 alex a montreal</span> &mdash; <a
171
+ href="https://github.com/mtldev514" target="_blank" title="GitHub"><svg class="icon"
172
+ viewBox="0 0 24 24">
173
+ <path
174
+ d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1.1-.8.1-.7.1-.7 1.2.1 1.9 1.3 1.9 1.3 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z" />
175
+ </svg> github</a> · <a href="https://www.linkedin.com/in/alexcatus/?locale=fr_FR"
176
+ target="_blank" title="LinkedIn"><svg class="icon" viewBox="0 0 24 24">
177
+ <path
178
+ d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z" />
179
+ </svg> linkedin</a> &mdash; <span class="terminal-dim"><span
180
+ data-i18n="footer_visitors">Visits:</span> <span
181
+ class="visitor-counter">......</span></span> <span class="terminal-cursor">█</span>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </footer>
186
+ </div>
187
+ <script src="js/counter.js"></script>
188
+ </body>
189
+
190
+ </html>