diff --git a/app.js b/app.js new file mode 100644 index 0000000..b7cdbb5 --- /dev/null +++ b/app.js @@ -0,0 +1,587 @@ +// Project Friction - Main Application +// Museum-quality digital exhibit for Canada's Gulf War role + +class ProjectFriction { + constructor() { + this.currentSection = null; + this.map = null; + this.mapLayers = {}; + this.timeline = null; + this.data = { + sections: [], + mapData: {}, + timelineEvents: [], + articles: {} + }; + + this.init(); + } + + async init() { + try { + // Show loading screen + this.showLoadingScreen(); + + // Load data + await this.loadApplicationData(); + + // Initialize components + this.initializeMap(); + this.initializeNavigation(); + this.initializeTimeline(); + this.initializeInteractions(); + + // Setup event listeners + this.setupEventListeners(); + + // Load initial state + this.loadMainNavigation(); + + // Hide loading screen + this.hideLoadingScreen(); + + console.log('Project Friction initialized successfully'); + } catch (error) { + console.error('Error initializing Project Friction:', error); + this.showErrorMessage('Failed to load exhibition. Please refresh the page.'); + } + } + + async loadApplicationData() { + // Load optional datasets without failing app startup if files are missing. + this.data.sections = await this.loadOptionalJSON(['data/sections.json'], []); + this.data.mapData = await this.loadOptionalJSON(['data/map-data.json', 'map-data.json'], {}); + this.data.timelineEvents = await this.loadOptionalJSON(['data/timeline-events.json'], []); + this.data.articles = await this.loadOptionalJSON(['data/articles.json'], {}); + + // Load block-based section content (used by the block renderer) + this.data.sectionsContent = await this.loadOptionalJSON(['data/sections-content.json'], {}); + + if (!Array.isArray(this.data.sections) || this.data.sections.length === 0) { + this.loadDefaultConfiguration(); + } + } + + async loadJSON(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load ${url}: ${response.status}`); + } + return await response.json(); + } + + async loadOptionalJSON(paths, fallbackValue) { + for (const path of paths) { + try { + return await this.loadJSON(path); + } catch (error) { + console.warn(`Optional data file not available: ${path}`); + } + } + + return fallbackValue; + } + + loadDefaultConfiguration() { + // Fallback configuration if data files fail to load + this.data.sections = [ + { + id: 'context', + title: 'Historical Context', + subtitle: 'Coalition Building & Diplomacy', + icon: '🌍', + color: '#d4a574', + description: 'Understand how Iraq\'s invasion of Kuwait triggered the largest military coalition since WWII and Canada\'s diplomatic role.', + stats: { articles: 7, storyMaps: 3, images: 45 } + }, + { + id: 'rcaf', + title: 'RCAF Desert Cats', + subtitle: '409 Squadron Air Operations', + icon: '✈️', + color: '#3498db', + description: 'Follow the Desert Cats from defensive patrols to strike missions, including the TNC-45 engagement and bombing runs.', + stats: { articles: 8, storyMaps: 4, images: 60 } + }, + { + id: 'rcn', + title: 'RCN Task Group', + subtitle: 'Naval Blockade Operations', + icon: 'βš“', + color: '#2c3e50', + description: 'The transformation of HMCS Terra Nova into a guided-missile destroyer and enforcement of UN sanctions.', + stats: { articles: 7, storyMaps: 2, images: 35 } + }, + { + id: 'army', + title: 'Canadian Army', + subtitle: 'Medical & Security Forces', + icon: 'πŸ₯', + color: '#27ae60', + description: '1 Canadian Field Hospital\'s life-saving work and security operations protecting coalition installations.', + stats: { articles: 7, storyMaps: 3, images: 40 } + }, + { + id: 'legacy', + title: 'After the War', + subtitle: 'Veterans & Long-term Impact', + icon: '🍁', + color: '#c0392b', + description: 'Gulf War Syndrome, environmental cleanup, peacekeeping missions, and the ongoing support for veterans.', + stats: { articles: 7, storyMaps: 2, images: 30 } + } + ]; + + // Default stats for main navigation + this.data.globalStats = { + personnel: '4,500+', + sorties: '2,700', + medals: '4,449', + duration: '7' + }; + } + + initializeMap() { + if (!window.L) { + console.warn('Leaflet is unavailable. Map features are disabled.'); + return; + } + + const mapElement = document.getElementById('map'); + if (!mapElement) { + console.warn('Map container not found.'); + return; + } + + // Initialize Leaflet map + this.map = L.map('map', { + center: [29.3759, 47.9774], // Kuwait City + zoom: 6, + zoomControl: false + }); + + // Add tile layer + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: 'Β© OpenStreetMap contributors', + maxZoom: 18 + }).addTo(this.map); + + // Initialize layer groups + this.mapLayers = { + coalition: L.layerGroup(), + iraqi: L.layerGroup(), + canadian: L.layerGroup(), + events: L.layerGroup(), + routes: L.layerGroup() + }; + + // Add initial overview markers + this.loadOverviewMarkers(); + } + + loadOverviewMarkers() { + if (!this.map || !window.L) return; + + // Add basic overview markers + const overviewMarkers = [ + { lat: 29.3759, lng: 47.9774, title: 'Kuwait City', type: 'liberation' }, + { lat: 25.2731, lng: 51.6080, title: 'Doha Air Base', type: 'canadian' }, + { lat: 26.2285, lng: 50.5860, title: 'Naval Operations', type: 'canadian' } + ]; + + overviewMarkers.forEach(marker => { + const mapMarker = L.marker([marker.lat, marker.lng]) + .bindPopup(`${marker.title}`) + .addTo(this.map); + }); + } + + initializeNavigation() { + if (typeof NavigationController === 'undefined') { + console.warn('NavigationController is unavailable. Using built-in navigation only.'); + return; + } + + this.navigation = new NavigationController(this); + this.navigation.init(); + } + + initializeTimeline() { + if (typeof TimelineController === 'undefined') { + console.warn('TimelineController is unavailable.'); + return; + } + + this.timeline = new TimelineController(this); + this.timeline.init(); + } + + initializeInteractions() { + if (typeof InteractionController === 'undefined') { + console.warn('InteractionController is unavailable.'); + return; + } + + this.interactions = new InteractionController(this); + this.interactions.init(); + } + + setupEventListeners() { + // Sidebar toggle + const sidebarToggle = document.getElementById('sidebarToggle'); + if (sidebarToggle) { + sidebarToggle.addEventListener('click', () => this.toggleSidebar()); + } + + // Back button + const backButton = document.getElementById('backButton'); + if (backButton) { + backButton.addEventListener('click', () => this.showMainNavigation()); + } + + // Window resize handler + window.addEventListener('resize', () => this.handleResize()); + + // Keyboard navigation + document.addEventListener('keydown', (e) => this.handleKeyboard(e)); + } + + loadMainNavigation() { + // Show main navigation + const mainNav = document.getElementById('mainNavigation'); + const sectionContainer = document.getElementById('sectionContainer'); + const backButton = document.getElementById('backButton'); + + if (mainNav) mainNav.classList.remove('hidden'); + if (sectionContainer) sectionContainer.classList.add('hidden'); + if (backButton) backButton.classList.remove('show'); + + // Load navigation cards and stats + this.renderQuickStats(); + this.renderNavigationCards(); + + // Reset map to overview + this.resetMapView(); + this.currentSection = null; + } + + renderQuickStats() { + const statsContainer = document.getElementById('quickStats'); + if (!statsContainer) return; + + const stats = this.data.globalStats || { + personnel: '4,500+', + sorties: '2,700', + medals: '4,449', + duration: '7' + }; + + statsContainer.innerHTML = ` +
+
${stats.personnel}
+
Personnel
+
+
+
${stats.sorties}
+
Sorties
+
+
+
${stats.medals}
+
Medals
+
+
+
${stats.duration}
+
Months
+
+ `; + } + + renderNavigationCards() { + const cardsContainer = document.getElementById('navigationCards'); + if (!cardsContainer) return; + + const cardsHTML = this.data.sections.map(section => ` + + `).join(''); + + cardsContainer.innerHTML = cardsHTML; + + // Add click handlers + cardsContainer.querySelectorAll('.nav-card').forEach(card => { + card.addEventListener('click', () => { + const sectionId = card.dataset.section; + this.showSection(sectionId); + }); + }); + } + + async showSection(sectionId) { + try { + const section = this.data.sections.find(s => s.id === sectionId); + if (!section) { + throw new Error(`Section ${sectionId} not found`); + } + + // Hide main navigation + const mainNav = document.getElementById('mainNavigation'); + if (mainNav) mainNav.classList.add('hidden'); + + // Show section container and back button + const sectionContainer = document.getElementById('sectionContainer'); + const backButton = document.getElementById('backButton'); + + if (sectionContainer) sectionContainer.classList.remove('hidden'); + if (backButton) backButton.classList.add('show'); + + // Load section content + await this.loadSectionContent(section); + + // Update map for section + this.updateMapForSection(section); + + this.currentSection = sectionId; + + } catch (error) { + console.error(`Error loading section ${sectionId}:`, error); + this.showErrorMessage(`Failed to load ${sectionId} section. Please try again.`); + } + } + + async loadSectionContent(section) { + const sectionContainer = document.getElementById('sectionContainer'); + if (!sectionContainer) return; + + // 1. Check localStorage for builder-saved blocks + const saved = localStorage.getItem('pf-section-' + section.id); + if (saved) { + try { + const blocks = JSON.parse(saved); + if (typeof BlockRenderer !== 'undefined') { + sectionContainer.innerHTML = this.wrapSectionContent(section, BlockRenderer.render(blocks)); + this.initializeSectionInteractions(section.id); + return; + } + } catch (e) { /* fall through */ } + } + + // 2. Use sections-content.json block data + const contentData = this.data.sectionsContent && this.data.sectionsContent[section.id]; + if (contentData && contentData.blocks && typeof BlockRenderer !== 'undefined') { + sectionContainer.innerHTML = this.wrapSectionContent(section, BlockRenderer.render(contentData.blocks)); + this.initializeSectionInteractions(section.id); + return; + } + + // 3. Try legacy HTML file + try { + const response = await fetch('sections/' + section.id + '.html'); + if (!response.ok) throw new Error('No legacy HTML'); + sectionContainer.innerHTML = await response.text(); + this.initializeSectionInteractions(section.id); + } catch (error) { + // 4. Fallback placeholder + this.loadFallbackSectionContent(section); + } + } + + wrapSectionContent(section, blocksHtml) { + return '
' + + '
' + + '
' + + '' + section.icon + '' + + '

' + section.title + '

' + + '

' + section.subtitle + '

' + + '
' + + blocksHtml + + '
'; + } + + loadFallbackSectionContent(section) { + const sectionContainer = document.getElementById('sectionContainer'); + if (!sectionContainer) return; + + sectionContainer.innerHTML = ` +
+
+ ${section.icon} + ${section.title} +
+
${section.subtitle}
+
+
+
+

Content Loading

+

This section is being developed. Please check back later for complete content.

+
+
+ `; + } + + initializeSectionInteractions(sectionId) { + // Wire map-button blocks to the Leaflet map + const self = this; + document.querySelectorAll('.block-map-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + const lat = parseFloat(btn.dataset.lat); + const lng = parseFloat(btn.dataset.lng); + const zoom = parseInt(btn.dataset.zoom) || 6; + if (self.map) { + self.map.setView([lat, lng], zoom); + btn.classList.add('active'); + setTimeout(function() { btn.classList.remove('active'); }, 1500); + } + }); + }); + } + + updateMapForSection(section) { + if (!this.map) return; + + // Clear existing layers + Object.values(this.mapLayers).forEach(layer => { + this.map.removeLayer(layer); + layer.clearLayers(); + }); + + // Add section-specific map data + const sectionMapData = this.data.mapData[section.id]; + if (sectionMapData) { + this.loadMapData(sectionMapData); + } + + // Update map view for section + this.setMapViewForSection(section.id); + } + + setMapViewForSection(sectionId) { + if (!this.map) return; + + const mapViews = { + context: { center: [29.3759, 47.9774], zoom: 6 }, + rcaf: { center: [25.2731, 51.6080], zoom: 8 }, + rcn: { center: [27.0, 50.0], zoom: 7 }, + army: { center: [32.0, 45.0], zoom: 6 }, + legacy: { center: [35.0, 45.0], zoom: 5 } + }; + + const view = mapViews[sectionId] || mapViews.context; + this.map.setView(view.center, view.zoom); + } + + loadMapData(mapData) { + if (!this.map || !window.L) return; + + // Load markers, routes, and other geographic data + if (mapData.markers) { + mapData.markers.forEach(marker => { + const mapMarker = L.marker([marker.lat, marker.lng]) + .bindPopup(`${marker.title}
${marker.description || ''}`); + + const layerGroup = this.mapLayers[marker.type] || this.mapLayers.events; + layerGroup.addLayer(mapMarker); + }); + } + + // Add all layers to map + Object.values(this.mapLayers).forEach(layer => { + this.map.addLayer(layer); + }); + } + + resetMapView() { + if (!this.map) return; + + // Reset to overview + this.map.setView([29.3759, 47.9774], 6); + + // Clear section-specific layers + Object.values(this.mapLayers).forEach(layer => { + this.map.removeLayer(layer); + layer.clearLayers(); + }); + + // Reload overview markers + this.loadOverviewMarkers(); + } + + toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + const toggleIcon = document.getElementById('toggle-icon'); + + if (sidebar && toggleIcon) { + sidebar.classList.toggle('collapsed'); + toggleIcon.textContent = sidebar.classList.contains('collapsed') ? 'β–Ά' : 'β—€'; + } + } + + showMainNavigation() { + this.loadMainNavigation(); + } + + handleResize() { + // Handle responsive behavior + if (this.map) { + this.map.invalidateSize(); + } + } + + handleKeyboard(event) { + // Handle keyboard navigation + if (event.key === 'Escape') { + if (this.currentSection) { + this.showMainNavigation(); + } + } + } + + showLoadingScreen() { + const loadingScreen = document.getElementById('loadingScreen'); + if (loadingScreen) { + loadingScreen.style.display = 'flex'; + } + } + + hideLoadingScreen() { + const loadingScreen = document.getElementById('loadingScreen'); + if (loadingScreen) { + setTimeout(() => { + loadingScreen.classList.add('fade-out'); + setTimeout(() => { + loadingScreen.style.display = 'none'; + }, 500); + }, 1000); + } + } + + showErrorMessage(message) { + console.error(message); + // In production, this would show a user-friendly error dialog + alert(message); + } +} + +// Initialize the application when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + window.projectFriction = new ProjectFriction(); +}); + +// Export for module usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = ProjectFriction; +} \ No newline at end of file diff --git a/blocks.css b/blocks.css new file mode 100644 index 0000000..e59e71c --- /dev/null +++ b/blocks.css @@ -0,0 +1,194 @@ +/* Project Friction - Block Styles */ +/* Styles for all 8 content block types rendered by BlockRenderer */ + +/* ── Shared block spacing ────────────────────────────────────────────────── */ +.block { + margin-bottom: 1.5rem; +} + +/* ── Heading Block ───────────────────────────────────────────────────────── */ +.block-heading-1 { + font-size: 26px; + font-weight: normal; + color: var(--color-primary); + font-family: var(--font-primary); + border-bottom: 3px solid var(--theme-color, var(--color-accent)); + padding-bottom: 0.5rem; + margin-top: 0.5rem; + margin-bottom: 1.25rem; +} + +.block-heading-2 { + font-size: 20px; + font-weight: normal; + color: var(--color-primary); + font-family: var(--font-primary); + border-bottom: 2px solid var(--color-gray); + padding-bottom: 0.4rem; + margin-top: 1.5rem; + margin-bottom: 1rem; +} + +.block-heading-3 { + font-size: 16px; + font-weight: 600; + color: var(--color-secondary); + font-family: var(--font-secondary); + margin-top: 1rem; + margin-bottom: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ── Paragraph Block ─────────────────────────────────────────────────────── */ +.block-paragraph { + font-size: 15px; + line-height: 1.75; + color: #2d3748; + font-family: var(--font-secondary); + margin-bottom: 1rem; +} + +/* ── Image Block ─────────────────────────────────────────────────────────── */ +.block-image { + margin: 1.25rem 0; + border-radius: var(--radius-md); + overflow: hidden; + box-shadow: var(--shadow-md); +} + +.block-image img { + width: 100%; + display: block; + object-fit: cover; + max-height: 320px; +} + +.block-image figcaption { + background: #f0f3f6; + color: var(--color-dark-gray); + font-size: 12px; + font-family: var(--font-secondary); + padding: 0.5rem 0.75rem; + font-style: italic; + border-top: 1px solid var(--color-gray); +} + +/* ── Quote Block ─────────────────────────────────────────────────────────── */ +/* Uses existing .quote-block, .quote-text, .quote-attribution from components.css */ + +/* ── Gallery Block ───────────────────────────────────────────────────────── */ +.block-gallery { + margin: 1.25rem 0; +} + +.gallery-scroll { + display: flex; + gap: 0.75rem; + overflow-x: auto; + padding-bottom: 0.5rem; + scrollbar-width: thin; + scrollbar-color: var(--color-gray) transparent; +} + +.gallery-scroll::-webkit-scrollbar { + height: 4px; +} + +.gallery-scroll::-webkit-scrollbar-thumb { + background: var(--color-gray); + border-radius: 4px; +} + +.gallery-item { + flex: 0 0 200px; + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: var(--shadow-sm); + position: relative; +} + +.gallery-item img { + width: 100%; + height: 140px; + object-fit: cover; + display: block; +} + +.gallery-item-caption { + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 11px; + font-family: var(--font-secondary); + padding: 0.3rem 0.5rem; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +/* ── 4-Image Grid Block ──────────────────────────────────────────────────── */ +.block-grid4 { + margin: 1.25rem 0; +} + +.grid4-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; +} + +.grid4-item { + position: relative; + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +.grid4-item img { + width: 100%; + height: 120px; + object-fit: cover; + display: block; + transition: transform 0.2s ease; +} + +.grid4-item:hover img { + transform: scale(1.04); +} + +.grid4-caption { + background: rgba(0, 0, 0, 0.65); + color: #fff; + font-size: 10px; + font-family: var(--font-secondary); + padding: 0.25rem 0.4rem; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +/* ── Statistics Block ────────────────────────────────────────────────────── */ +.block-statistics { + margin: 1.25rem 0; +} + +.block-stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; +} + +/* ── Map Button Block ────────────────────────────────────────────────────── */ +.block-map-button-wrap { + margin: 0.75rem 0; +} + +/* ── Section Content Container ───────────────────────────────────────────── */ +.section-content-container { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + diff --git a/builder.html b/builder.html new file mode 100644 index 0000000..14fbd76 --- /dev/null +++ b/builder.html @@ -0,0 +1,653 @@ + + + + + + Section Builder β€” Project Friction + + + + + + + + +
+
+ + Section Builder +
+ +
+ +
+ + + + + +
+
+

Loading…

+ +
+
+
+ +
+ + + + + + + + + diff --git a/components.css b/components.css new file mode 100644 index 0000000..75dd48f --- /dev/null +++ b/components.css @@ -0,0 +1,536 @@ +/* Project Friction - Components CSS */ +/* Reusable UI components for the museum exhibit */ + +/* Navigation Cards */ +.nav-card { + background: var(--color-white); + border: 2px solid #e9ecef; + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + margin-bottom: var(--spacing-lg); + cursor: pointer; + transition: all var(--transition-normal); + position: relative; + overflow: hidden; +} + +.nav-card::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 5px; + background: var(--theme-color); + transition: width var(--transition-normal); +} + +.nav-card:hover { + transform: translateX(5px); + box-shadow: var(--shadow-lg); + border-color: var(--theme-color); +} + +.nav-card:hover::before { + width: 8px; +} + +.nav-card[data-section="context"] { --theme-color: var(--color-context); } +.nav-card[data-section="rcaf"] { --theme-color: var(--color-rcaf); } +.nav-card[data-section="rcn"] { --theme-color: var(--color-rcn); } +.nav-card[data-section="army"] { --theme-color: var(--color-army); } +.nav-card[data-section="legacy"] { --theme-color: var(--color-legacy); } + +.nav-card-header { + display: flex; + align-items: center; + gap: var(--spacing-lg); + margin-bottom: var(--spacing-md); +} + +.nav-card-icon { + font-size: 28px; +} + +.nav-card-title { + flex: 1; +} + +.nav-card-title h3 { + font-size: 16px; + margin-bottom: var(--spacing-xs); + color: var(--color-black); + font-family: var(--font-primary); +} + +.nav-card-title span { + font-size: 12px; + color: var(--color-dark-gray); + font-family: var(--font-secondary); +} + +.nav-card-preview { + font-size: 13px; + line-height: 1.6; + color: var(--color-dark-gray); + margin-bottom: var(--spacing-md); + font-family: var(--font-secondary); +} + +.nav-card-links { + display: flex; + gap: var(--spacing-lg); + font-size: 12px; + color: var(--color-rcaf); + font-family: var(--font-secondary); +} + +.nav-card-link { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +/* Interactive Map Buttons */ +.map-button { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + background: linear-gradient(135deg, var(--color-rcaf) 0%, #2980b9 100%); + color: var(--color-white); + border: none; + border-radius: 25px; + font-size: 14px; + cursor: pointer; + transition: all var(--transition-normal); + margin: var(--spacing-md) var(--spacing-md) var(--spacing-md) 0; + box-shadow: 0 2px 5px rgba(52, 152, 219, 0.3); + font-family: var(--font-secondary); +} + +.map-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(52, 152, 219, 0.4); +} + +.map-button.active { + background: linear-gradient(135deg, var(--color-accent) 0%, #c0392b 100%); + box-shadow: 0 2px 5px rgba(192, 57, 43, 0.3); +} + +.map-button-icon { + font-size: 16px; +} + +/* Fact Boxes */ +.fact-box { + background: linear-gradient(to right, #fef5e7, #fdf2e9); + border-left: 5px solid #f39c12; + padding: var(--spacing-xl); + margin: var(--spacing-xl) 0; + border-radius: var(--radius-sm); + box-shadow: var(--shadow-sm); +} + +.fact-box h3 { + font-size: 16px; + color: #d68910; + margin-bottom: var(--spacing-md); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + font-family: var(--font-secondary); +} + +.fact-box ul { + list-style: none; +} + +.fact-box li { + padding: 6px 0; + color: #7f6000; + font-size: 14px; + display: flex; + align-items: flex-start; + font-family: var(--font-secondary); +} + +.fact-box li::before { + content: "β–Έ"; + margin-right: var(--spacing-md); + color: #f39c12; + font-weight: bold; +} + +/* Quote Blocks */ +.quote-block { + background: var(--color-light-gray); + border-left: 4px solid var(--color-secondary); + padding: var(--spacing-xl); + margin: var(--spacing-xxl) 0; + font-style: italic; + position: relative; + border-radius: var(--radius-sm); +} + +.quote-block::before { + content: """; + font-size: 60px; + color: #cbd5e0; + position: absolute; + top: -10px; + left: 10px; + font-family: var(--font-primary); +} + +.quote-text { + font-size: 17px; + line-height: 1.6; + color: var(--color-secondary); + margin-bottom: var(--spacing-md); + padding-left: 30px; + font-family: var(--font-primary); +} + +.quote-attribution { + text-align: right; + font-size: 14px; + color: var(--color-dark-gray); + font-style: normal; + font-family: var(--font-secondary); +} + +/* Timeline Cards */ +.timeline-card { + background: var(--color-white); + border: 1px solid var(--color-gray); + border-radius: var(--radius-md); + padding: var(--spacing-xl); + margin: var(--spacing-xl) 0; + position: relative; + transition: all var(--transition-normal); +} + +.timeline-card:hover { + box-shadow: var(--shadow-lg); + transform: translateX(5px); +} + +.timeline-date { + position: absolute; + left: -15px; + top: var(--spacing-xl); + background: var(--color-accent); + color: var(--color-white); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: bold; + font-family: var(--font-secondary); +} + +.timeline-content { + margin-left: var(--spacing-xl); +} + +.timeline-content h4 { + font-size: 18px; + color: var(--color-primary); + margin-bottom: var(--spacing-md); + font-family: var(--font-primary); +} + +.timeline-content p { + font-size: 14px; + line-height: 1.6; + color: var(--color-dark-gray); + font-family: var(--font-secondary); +} + +/* Media Gallery */ +.media-section { + margin: var(--spacing-xxl) 0; + padding: var(--spacing-xl); + background: var(--color-light-gray); + border-radius: var(--radius-md); +} + +.media-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.media-header h3 { + font-size: 18px; + color: var(--color-primary); + font-family: var(--font-primary); +} + +.media-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-lg); +} + +.media-item { + position: relative; + aspect-ratio: 4/3; + background: #ddd; + border-radius: var(--radius-sm); + overflow: hidden; + cursor: pointer; + transition: transform var(--transition-normal); +} + +.media-item:hover { + transform: scale(1.05); + box-shadow: var(--shadow-lg); +} + +.media-caption { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); + color: var(--color-white); + padding: var(--spacing-md); + font-size: 12px; + font-family: var(--font-secondary); +} + +/* Document Section */ +.document-section { + background: var(--color-white); + border: 2px solid #ecf0f1; + border-radius: var(--radius-md); + padding: var(--spacing-xl); + margin: var(--spacing-xl) 0; +} + +.document-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 2px solid #ecf0f1; +} + +.document-header h3 { + font-size: 16px; + color: var(--color-primary); + font-family: var(--font-primary); +} + +.document-list { + list-style: none; +} + +.document-list li { + padding: var(--spacing-md) 0; + border-bottom: 1px solid #f5f5f5; +} + +.document-link { + display: flex; + align-items: center; + gap: var(--spacing-md); + color: var(--color-rcaf); + text-decoration: none; + transition: all var(--transition-fast); + font-family: var(--font-secondary); + font-size: 14px; +} + +.document-link:hover { + color: #2980b9; + padding-left: var(--spacing-md); +} + +/* Article Index */ +.article-index { + padding: var(--spacing-xl); + background: var(--color-white); + border-radius: var(--radius-md); + border: 1px solid var(--color-gray); + margin: var(--spacing-xl) 0; +} + +.article-index h3 { + font-size: 16px; + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 2px solid var(--color-rcaf); + color: var(--color-primary); + font-family: var(--font-primary); +} + +.article-list { + list-style: none; +} + +.article-list li { + padding: var(--spacing-sm) 0; + border-bottom: 1px solid #f1f3f5; +} + +.article-list a { + color: var(--color-rcaf); + text-decoration: none; + font-size: 14px; + display: flex; + align-items: center; + gap: var(--spacing-sm); + transition: all var(--transition-fast); + font-family: var(--font-secondary); +} + +.article-list a:hover { + padding-left: var(--spacing-md); + color: #0056b3; +} + +/* Info Box */ +.info-box { + background: linear-gradient(to right, var(--color-light-gray), var(--color-white)); + border-left: 4px solid var(--color-rcaf); + padding: var(--spacing-lg); + margin: var(--spacing-xl) 0; + border-radius: var(--radius-sm); +} + +.info-box h4 { + font-size: 14px; + margin-bottom: var(--spacing-md); + color: var(--color-black); + font-family: var(--font-secondary); + font-weight: 600; +} + +.info-box ul { + list-style: none; +} + +.info-box li { + font-size: 13px; + padding: 4px 0; + color: #495057; + font-family: var(--font-secondary); +} + +/* Story Map Banner */ +.story-map-banner { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: var(--color-white); + padding: var(--spacing-xl); + border-radius: var(--radius-lg); + margin: var(--spacing-xl) 0; + cursor: pointer; + transition: transform var(--transition-normal); +} + +.story-map-banner:hover { + transform: scale(1.02); + box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); +} + +.story-map-banner h4 { + font-size: 18px; + margin-bottom: var(--spacing-sm); + display: flex; + align-items: center; + gap: var(--spacing-md); + font-family: var(--font-primary); +} + +.story-map-banner p { + font-size: 14px; + opacity: 0.9; + line-height: 1.6; + font-family: var(--font-secondary); +} + +/* Section Headers */ +.chapter-header { + margin: var(--spacing-xxl) 0 var(--spacing-xl) 0; + padding-bottom: var(--spacing-md); + border-bottom: 2px solid var(--color-context); +} + +.chapter-header h2 { + font-size: 24px; + color: var(--color-primary); + font-family: var(--font-primary); + font-weight: normal; +} + +.chapter-date { + font-size: 14px; + color: var(--color-dark-gray); + margin-top: var(--spacing-sm); + font-style: italic; + font-family: var(--font-secondary); +} + +/* Buttons */ +.btn { + padding: var(--spacing-md) var(--spacing-lg); + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 14px; + font-family: var(--font-secondary); + transition: all var(--transition-fast); + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); +} + +.btn-primary { + background: var(--color-rcaf); + color: var(--color-white); +} + +.btn-primary:hover { + background: #2980b9; + transform: translateY(-2px); +} + +.btn-secondary { + background: var(--color-gray); + color: var(--color-black); +} + +.btn-secondary:hover { + background: #adb5bd; +} + +/* Close button */ +.close-btn, +.close-info { + position: absolute; + top: var(--spacing-md); + right: var(--spacing-md); + width: 30px; + height: 30px; + border: none; + background: rgba(0,0,0,0.1); + border-radius: var(--radius-full); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: var(--color-dark-gray); + transition: all var(--transition-fast); +} + +.close-btn:hover, +.close-info:hover { + background: rgba(0,0,0,0.2); + color: var(--color-black); +} \ No newline at end of file diff --git a/copilot_instructions.md b/copilot_instructions.md new file mode 100644 index 0000000..32fa688 --- /dev/null +++ b/copilot_instructions.md @@ -0,0 +1,200 @@ +# Project Friction - Digital Gulf War Exhibit +## Copilot Development Instructions + +### PROJECT SCOPE & CONTEXT + +**Project Name**: Project Friction - Canada's Role in the Gulf War Digital Exhibit +**Client**: Canadian Research and Mapping Association (CRMA) / Veterans Affairs Canada +**Purpose**: Interactive digital museum exhibit for online and touchscreen kiosk deployment +**Timeline**: Phase 2 development (Feb-Jun 2026) - Content Creation & Frontend Development + +**Core Mission**: +Create a comprehensive, educational digital exhibit showcasing Canada's military contribution to the 1991 Gulf War through interactive maps, historical narratives, and multimedia content. The exhibit must serve both online users and museum visitors using touchscreen kiosks. + +### TECHNICAL REQUIREMENTS + +**Primary Technologies**: +- HTML5 for structure +- CSS3 for styling (museum-quality visual design) +- Vanilla JavaScript for interactivity +- Leaflet.js for interactive mapping +- No frameworks required - keep dependencies minimal for kiosk compatibility + +**Target Platforms**: +- Desktop browsers (museum kiosks) +- Modern tablets and mobile devices +- Must work offline after initial load + +**Performance Standards**: +- Load time under 3 seconds +- Touch-friendly interface (minimum 44px touch targets) +- WCAG 2.1 AA accessibility compliance +- Responsive design for multiple screen sizes + +### FILE STRUCTURE TO CREATE + +``` +project-friction/ +β”œβ”€β”€ index.html # Main navigation hub +β”œβ”€β”€ css/ +β”‚ β”œβ”€β”€ main.css # Global styles and layout +β”‚ β”œβ”€β”€ components.css # Reusable UI components +β”‚ β”œβ”€β”€ sections.css # Section-specific styles +β”‚ └── responsive.css # Media queries and responsive design +β”œβ”€β”€ js/ +β”‚ β”œβ”€β”€ app.js # Main application logic +β”‚ β”œβ”€β”€ navigation.js # Section switching and breadcrumbs +β”‚ β”œβ”€β”€ map-controller.js # Map initialization and controls +β”‚ β”œβ”€β”€ timeline.js # Timeline animation and controls +β”‚ β”œβ”€β”€ data-layers.js # Map data and layer management +β”‚ └── interactions.js # UI interactions and animations +β”œβ”€β”€ data/ +β”‚ β”œβ”€β”€ map-data.json # Geographic data for all sections +β”‚ β”œβ”€β”€ timeline-events.json # Timeline event data +β”‚ β”œβ”€β”€ articles.json # Article content and metadata +β”‚ └── media-gallery.json # Image and media references +β”œβ”€β”€ sections/ +β”‚ β”œβ”€β”€ context.html # Historical Context section +β”‚ β”œβ”€β”€ rcaf.html # RCAF Desert Cats section +β”‚ β”œβ”€β”€ rcn.html # RCN Task Group section +β”‚ β”œβ”€β”€ army.html # Canadian Army section +β”‚ └── legacy.html # Post-War Legacy section +└── assets/ + β”œβ”€β”€ images/ # Historical photos and media + β”œβ”€β”€ icons/ # UI icons and map markers + └── fonts/ # Custom typography files +``` + +### DEVELOPMENT APPROACH + +**Phase 1: Core Structure** +1. Create main HTML layout with sidebar navigation +2. Implement CSS grid/flexbox layout system +3. Build basic JavaScript navigation between sections +4. Initialize Leaflet map with basic controls + +**Phase 2: Interactive Features** +1. Timeline control implementation +2. Map layer switching system +3. Interactive buttons for map filters +4. Content loading and display system + +**Phase 3: Content Integration** +1. Load historical narrative content +2. Implement map data layers +3. Add media galleries and fact boxes +4. Polish animations and transitions + +### SPECIFIC IMPLEMENTATION GUIDELINES + +**CSS Architecture**: +- Use CSS Custom Properties for theming +- BEM methodology for class naming +- Mobile-first responsive design +- Print stylesheet for offline reading + +**JavaScript Patterns**: +- Module pattern for code organization +- Event delegation for touch interactions +- Async/await for data loading +- Error handling for offline scenarios + +**Map Integration**: +- Leaflet.js with custom marker styles +- GeoJSON data format for geographic features +- Layer groups for different data types +- Custom popup templates + +**Accessibility Requirements**: +- Semantic HTML structure +- ARIA labels for interactive elements +- Keyboard navigation support +- Screen reader compatible content +- High contrast mode support + +### CONTENT SPECIFICATIONS + +**Historical Context Section** (already developed): +- 2,500+ word narrative in 6 chapters +- Interactive map buttons for key events +- Timeline integration +- Fact boxes and quote blocks +- Document reference sections + +**Remaining Sections** (to be developed): +- RCAF: CF-18 operations, pilot stories, mission details +- RCN: Naval operations, ship modifications, MIF operations +- Army: Field hospital operations, security details, humanitarian missions +- Legacy: Veteran health, commemorations, lessons learned + +**Map Data Requirements**: +- Force positions and movements +- Event locations with timestamps +- Base locations and infrastructure +- Supply routes and operational areas +- Political boundaries and geographic context + +### QUALITY STANDARDS + +**Content Quality**: +- Museum-grade historical accuracy +- Canadian perspective throughout +- Primary source citations +- Appropriate reading level (Grade 8-10) + +**Visual Design**: +- Professional museum aesthetic +- Consistent color coding by section +- Clear typography hierarchy +- Intuitive navigation patterns + +**User Experience**: +- Maximum 3-click depth to any content +- Clear breadcrumb navigation +- Immediate visual feedback on interactions +- Graceful degradation for older devices + +### TESTING REQUIREMENTS + +**Device Testing**: +- Desktop browsers (Chrome, Firefox, Safari, Edge) +- Tablet devices (iPad, Android tablets) +- Touch screen kiosks (Windows-based museum hardware) + +**Content Validation**: +- Historical accuracy review by subject matter experts +- Canadian military veteran feedback +- Museum educator usability testing + +### DEPLOYMENT CONSIDERATIONS + +**Online Deployment**: +- Static site hosting (Netlify/Vercel compatible) +- CDN integration for media assets +- Progressive web app features + +**Kiosk Deployment**: +- Offline functionality +- Reset mechanism for public use +- Screensaver integration +- Maintenance mode capabilities + +--- + +## IMPORTANT CONSTRAINTS + +1. **Stick to defined scope**: 5 thematic sections covering Canada's Gulf War role +2. **No feature creep**: Request approval for any additions beyond specified content +3. **Museum standards**: All content must meet professional exhibition quality +4. **Accessibility first**: Design for all users, including those with disabilities +5. **Historical accuracy**: No fictional elements or speculative content +6. **Canadian focus**: Maintain Canadian perspective while showing international context + +## APPROVAL REQUIRED FOR: +- Additional sections beyond the 5 defined areas +- New interactive features not specified in requirements +- Integration of external APIs or services +- Significant changes to visual design approach +- Addition of audio/video content beyond static images + +This project represents a significant digital heritage initiative. Maintain focus on educational value, historical accuracy, and professional presentation standards throughout development. \ No newline at end of file diff --git a/data/sections-content.json b/data/sections-content.json new file mode 100644 index 0000000..6b366b7 --- /dev/null +++ b/data/sections-content.json @@ -0,0 +1,113 @@ +{ + "context": { + "blocks": [ + { "id": "c1", "type": "heading", "level": 1, "text": "Historical Context: The Road to War" }, + { "id": "c2", "type": "paragraph", "text": "On August 2, 1990, Iraqi forces under President Saddam Hussein crossed the border into neighbouring Kuwait, overwhelming its small military in a matter of hours. The invasion shocked the international community and triggered an unprecedented diplomatic and military response. Within days, the United Nations Security Council passed Resolution 660, condemning the invasion and demanding an immediate, unconditional withdrawal of Iraqi forces." }, + { "id": "c3", "type": "statistics", "stats": [ + { "number": "34", "label": "Coalition Nations" }, + { "number": "12", "label": "UN Resolutions" }, + { "number": "43", "label": "Days Air Campaign" }, + { "number": "100", "label": "Hours Ground War" } + ]}, + { "id": "c4", "type": "map-button", "label": "Show Iraqi Invasion Routes", "lat": 29.8, "lng": 47.9, "zoom": 8 }, + { "id": "c5", "type": "heading", "level": 2, "text": "Canada's Diplomatic Response" }, + { "id": "c6", "type": "paragraph", "text": "Canada was among the first nations to condemn Iraq's aggression and commit forces to the coalition. Prime Minister Brian Mulroney announced on August 10, 1990 that Canada would deploy naval vessels to the Persian Gulf as part of the multinational force. This decision reflected Canada's longstanding commitment to collective security and the rule of international law." }, + { "id": "c7", "type": "quote", "text": "Canada will not stand idly by while a small nation is crushed by its neighbour in clear violation of international law.", "author": "Prime Minister Brian Mulroney, August 1990" }, + { "id": "c8", "type": "map-button", "label": "Show Coalition Force Positions", "lat": 27.0, "lng": 50.0, "zoom": 6 }, + { "id": "c9", "type": "heading", "level": 2, "text": "Operation Desert Shield" }, + { "id": "c10", "type": "paragraph", "text": "Operation Desert Shield β€” the build-up phase from August 1990 to January 1991 β€” saw the massing of over 700,000 coalition troops in Saudi Arabia and the surrounding region. Canada contributed naval, air, and later ground forces, making it one of the most significant Canadian military deployments since the Korean War." }, + { "id": "c11", "type": "image", "src": "https://placehold.co/800x450/2c3e50/ffffff?text=Coalition+Forces+Assembling", "alt": "Coalition forces assembling in Saudi Arabia", "caption": "Coalition forces assembling in Saudi Arabia, late 1990. Canada contributed 4,500 personnel across naval, air, and ground components." } + ] + }, + "rcaf": { + "blocks": [ + { "id": "r1", "type": "heading", "level": 1, "text": "RCAF Desert Cats: 409 Squadron" }, + { "id": "r2", "type": "paragraph", "text": "Canada's air contribution to the Gulf War was led by 409 Tactical Fighter Squadron. Known as the 'Nighthawks,' the squadron's CF-18 Hornets were deployed to Doha, Qatar, where they formed the core of Canada's Gulf War air contingent β€” the Desert Cats. From their base at Canada Dry 1, pilots flew Combat Air Patrol and, later, offensive strike missions deep into Iraq." }, + { "id": "r3", "type": "statistics", "stats": [ + { "number": "26", "label": "CF-18 Hornets" }, + { "number": "2,700+", "label": "Sorties Flown" }, + { "number": "5", "label": "Months Deployed" }, + { "number": "0", "label": "Aircraft Lost" } + ]}, + { "id": "r4", "type": "map-button", "label": "Show Doha Air Base (Canada Dry 1)", "lat": 25.27, "lng": 51.61, "zoom": 10 }, + { "id": "r5", "type": "heading", "level": 2, "text": "From Defence to Offence" }, + { "id": "r6", "type": "paragraph", "text": "Initially tasked with Combat Air Patrol (CAP) missions protecting the naval fleet, the Desert Cats transitioned to offensive strike operations after Operation Desert Storm began on January 17, 1991. CF-18 pilots flew interdiction missions targeting Iraqi airfields, radar sites, and command infrastructure β€” a significant shift that required rapid retraining and aircraft modification." }, + { "id": "r7", "type": "quote", "text": "We went from protecting the fleet to striking deep into Iraq. The transition was fast, but the training paid off. The Desert Cats were ready.", "author": "Pilot, 409 Squadron" }, + { "id": "r8", "type": "heading", "level": 2, "text": "The TNC-45 Engagement" }, + { "id": "r9", "type": "paragraph", "text": "On January 30, 1991, two CF-18s from 409 Squadron engaged and sank an Iraqi Zhuk-class patrol vessel, the TNC-45, in the northern Persian Gulf. It was the first time Canadian aircraft had destroyed an enemy vessel since the Second World War β€” a historic moment for the RCAF and a demonstration of the Desert Cats' combat effectiveness." }, + { "id": "r10", "type": "map-button", "label": "Show TNC-45 Engagement Site", "lat": 29.14, "lng": 48.13, "zoom": 9 }, + { "id": "r11", "type": "grid4", "images": [ + { "src": "https://placehold.co/400x300/3498db/ffffff?text=CF-18+Hornet", "alt": "CF-18 Hornet", "caption": "CF-18 Hornet in Desert Cats livery" }, + { "src": "https://placehold.co/400x300/2980b9/ffffff?text=Doha+Air+Base", "alt": "Doha Air Base", "caption": "Canada Dry 1, Doha, Qatar" }, + { "src": "https://placehold.co/400x300/1a5276/ffffff?text=Desert+Cats+Patch", "alt": "Desert Cats patch", "caption": "409 Squadron Desert Cats patch" }, + { "src": "https://placehold.co/400x300/154360/ffffff?text=Mission+Briefing", "alt": "Mission briefing", "caption": "Pre-mission briefing, 1991" } + ]} + ] + }, + "rcn": { + "blocks": [ + { "id": "n1", "type": "heading", "level": 1, "text": "RCN Task Group: Naval Blockade Operations" }, + { "id": "n2", "type": "paragraph", "text": "Canada was among the first nations to respond to the Gulf Crisis with a naval contribution. Three Royal Canadian Navy vessels β€” HMCS Athabaskan, HMCS Terra Nova, and HMCS Protecteur β€” were dispatched to the Persian Gulf as part of Operation Friction. Together they formed Canadian Task Group 302.3, operating under the Maritime Interception Force." }, + { "id": "n3", "type": "statistics", "stats": [ + { "number": "3", "label": "RCN Vessels" }, + { "number": "1,000+", "label": "Sailors Deployed" }, + { "number": "160+", "label": "Ships Challenged" }, + { "number": "8", "label": "Months at Sea" } + ]}, + { "id": "n4", "type": "map-button", "label": "Show Naval Operations Area", "lat": 27.0, "lng": 50.5, "zoom": 7 }, + { "id": "n5", "type": "heading", "level": 2, "text": "HMCS Terra Nova: A Remarkable Transformation" }, + { "id": "n6", "type": "paragraph", "text": "One of the most significant operational achievements was the rapid conversion of HMCS Terra Nova into a guided-missile destroyer. Canadian naval technicians worked around the clock to retrofit the vessel with modern anti-aircraft missile systems, significantly upgrading her capability for the high-threat Persian Gulf environment. The conversion was completed in record time." }, + { "id": "n7", "type": "image", "src": "https://placehold.co/800x450/2c3e50/ffffff?text=HMCS+Terra+Nova", "alt": "HMCS Terra Nova in the Persian Gulf", "caption": "HMCS Terra Nova operating in the Persian Gulf, 1990–91. The vessel underwent rapid modifications to enhance her missile defence capability." }, + { "id": "n8", "type": "quote", "text": "The men and women of the task group performed extraordinary work under pressure. What they achieved in those weeks of preparation was remarkable.", "author": "Commodore Ken Summers, CTG 302.3 Commander" }, + { "id": "n9", "type": "heading", "level": 2, "text": "Maritime Interception Force" }, + { "id": "n10", "type": "paragraph", "text": "Canada's naval task group formed a key part of the Maritime Interception Force (MIF), enforcing the UN-mandated blockade of Iraq. Canadian vessels challenged hundreds of merchant ships, and the task group was recognised for its professionalism and effectiveness in this complex multinational operation at sea." }, + { "id": "n11", "type": "map-button", "label": "Show HMCS Task Group Position", "lat": 26.23, "lng": 50.59, "zoom": 9 } + ] + }, + "army": { + "blocks": [ + { "id": "a1", "type": "heading", "level": 1, "text": "Canadian Army: Medical & Security Forces" }, + { "id": "a2", "type": "paragraph", "text": "Canada's army contribution to the Gulf War centred on two primary missions: the deployment of 1 Canadian Field Hospital to Saudi Arabia, and the provision of security forces to protect coalition installations. Together, these elements brought Canada's ground commitment to over 1,200 personnel operating in one of the most hostile environments in the world." }, + { "id": "a3", "type": "statistics", "stats": [ + { "number": "530+", "label": "Hospital Personnel" }, + { "number": "600", "label": "Bed Capacity" }, + { "number": "500+", "label": "Security Forces" }, + { "number": "0", "label": "Canadian Fatalities" } + ]}, + { "id": "a4", "type": "map-button", "label": "Show 1 Canadian Field Hospital", "lat": 28.43, "lng": 46.09, "zoom": 9 }, + { "id": "a5", "type": "heading", "level": 2, "text": "1 Canadian Field Hospital" }, + { "id": "a6", "type": "paragraph", "text": "Established at Al-Qaysumah, Saudi Arabia, 1 Canadian Field Hospital (1 CFH) was one of the most capable coalition medical facilities in the theatre of operations. With a planned capacity of 600 beds, it included surgical, intensive care, and recovery wards staffed by some of Canada's finest medical personnel. The swift coalition victory meant the hospital treated fewer battlefield casualties than anticipated, but the hospital remained on standby throughout the conflict." }, + { "id": "a7", "type": "quote", "text": "We prepared for the worst. The training was intense, the equipment was first-class, and the people were exceptional. We were ready for whatever came our way.", "author": "Colonel John Abt, Commanding Officer, 1 Canadian Field Hospital" }, + { "id": "a8", "type": "image", "src": "https://placehold.co/800x450/27ae60/ffffff?text=1+Canadian+Field+Hospital", "alt": "1 Canadian Field Hospital, Al-Qaysumah", "caption": "1 Canadian Field Hospital at Al-Qaysumah, Saudi Arabia. The 530-person unit was fully operational and prepared for mass casualty scenarios." }, + { "id": "a9", "type": "heading", "level": 2, "text": "Security Operations in Bahrain" }, + { "id": "a10", "type": "paragraph", "text": "Members of the Royal Canadian Regiment and other army units provided security for coalition installations in Bahrain and Saudi Arabia. These operations, while less visible than combat missions, were essential to the smooth functioning of the broader coalition effort. Canadian soldiers maintained a professional presence and provided force protection in a tense and unpredictable environment." }, + { "id": "a11", "type": "map-button", "label": "Show Bahrain Security Operations", "lat": 26.07, "lng": 50.56, "zoom": 10 } + ] + }, + "legacy": { + "blocks": [ + { "id": "l1", "type": "heading", "level": 1, "text": "After the War: Veterans, Legacy & Long-Term Impact" }, + { "id": "l2", "type": "paragraph", "text": "Operation Desert Storm ended on February 28, 1991 β€” just 100 hours after the ground offensive began. The swift coalition victory liberated Kuwait and expelled Iraqi forces, but for thousands of Canadian veterans, the war's end was only the beginning of a longer struggle. Gulf War Syndrome, commemorations, peacekeeping deployments, and ongoing veteran support shaped the post-war legacy of Canada's participation." }, + { "id": "l3", "type": "statistics", "stats": [ + { "number": "4,449", "label": "Medals Awarded" }, + { "number": "4,500+", "label": "Veterans" }, + { "number": "700+", "label": "Gulf War Illness Claims" }, + { "number": "1991–2003", "label": "UNIKOM Service" } + ]}, + { "id": "l4", "type": "heading", "level": 2, "text": "Gulf War Syndrome" }, + { "id": "l5", "type": "paragraph", "text": "In the years following the war, thousands of veterans across the coalition reported a range of unexplained illnesses β€” chronic fatigue, muscle pain, cognitive difficulties, and other debilitating symptoms. This cluster of conditions became known as Gulf War Syndrome. Canadian veterans fought for official recognition of their illnesses and improved healthcare support, a battle that continued for many years after they returned home." }, + { "id": "l6", "type": "quote", "text": "We served with honour and came home changed. The government needed to understand that the war didn't end when we crossed the ocean and came home.", "author": "Gulf War Veteran, Royal Canadian Regiment" }, + { "id": "l7", "type": "map-button", "label": "Show Veterans Affairs Canada", "lat": 45.42, "lng": -75.70, "zoom": 10 }, + { "id": "l8", "type": "heading", "level": 2, "text": "Peacekeeping & UNIKOM" }, + { "id": "l9", "type": "paragraph", "text": "Following the ceasefire, Canada contributed personnel to the UN Iraq-Kuwait Observation Mission (UNIKOM), established to monitor the demilitarised zone along the Iraq-Kuwait border. Canadian peacekeepers served with UNIKOM from 1991 until the mission's end in 2003, reflecting Canada's ongoing commitment to regional stability and the international rule of law." }, + { "id": "l10", "type": "map-button", "label": "Show UNIKOM Peacekeeping Zone", "lat": 30.05, "lng": 47.80, "zoom": 9 }, + { "id": "l11", "type": "gallery", "images": [ + { "src": "https://placehold.co/600x400/c0392b/ffffff?text=Medal+Parade", "alt": "Medal parade", "caption": "Veterans receiving Gulf War Service Medals" }, + { "src": "https://placehold.co/600x400/922b21/ffffff?text=Homecoming", "alt": "Homecoming ceremony", "caption": "Canadian troops returning home, 1991" }, + { "src": "https://placehold.co/600x400/76261d/ffffff?text=Commemorations", "alt": "Commemoration ceremony", "caption": "Annual Gulf War commemoration" }, + { "src": "https://placehold.co/600x400/4a1515/ffffff?text=UNIKOM+Service", "alt": "UNIKOM peacekeeping", "caption": "Canadian peacekeepers with UNIKOM, 1992" } + ]} + ] + } +} + diff --git a/index.html b/index.html new file mode 100644 index 0000000..56b0c69 --- /dev/null +++ b/index.html @@ -0,0 +1,112 @@ + + + + + + Project Friction - Canada's Gulf War Digital Exhibit + + + + + + +
+ + + + +
+
+ + +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+
+ + +
+
+
+

Loading Project Friction

+

Preparing the interactive exhibit...

+
+
+ + + + + + + \ No newline at end of file diff --git a/js/block-renderer.js b/js/block-renderer.js new file mode 100644 index 0000000..ecd7892 --- /dev/null +++ b/js/block-renderer.js @@ -0,0 +1,95 @@ +/** + * Block Renderer - Project Friction + * Converts a block array into HTML strings. + * Used by both the main exhibit (app.js) and the builder preview. + */ +class BlockRenderer { + + static render(blocks) { + if (!Array.isArray(blocks)) return ''; + return blocks.map(function(block) { return BlockRenderer.renderBlock(block); }).join('\n'); + } + + static renderBlock(block) { + if (!block || !block.type) return ''; + switch (block.type) { + case 'heading': return BlockRenderer.renderHeading(block); + case 'paragraph': return BlockRenderer.renderParagraph(block); + case 'image': return BlockRenderer.renderImage(block); + case 'quote': return BlockRenderer.renderQuote(block); + case 'gallery': return BlockRenderer.renderGallery(block); + case 'grid4': return BlockRenderer.renderGrid4(block); + case 'statistics': return BlockRenderer.renderStatistics(block); + case 'map-button': return BlockRenderer.renderMapButton(block); + default: return ''; + } + } + + static renderHeading(block) { + var level = Math.min(Math.max(parseInt(block.level) || 2, 1), 6); + return '' + (block.text || '') + ''; + } + + static renderParagraph(block) { + return '

' + (block.text || '') + '

'; + } + + static renderImage(block) { + return '
' + + '' + (block.alt || '') + '' + + (block.caption ? '
' + block.caption + '
' : '') + + '
'; + } + + static renderQuote(block) { + return '
' + + '

' + (block.text || '') + '

' + + (block.author ? '— ' + block.author + '' : '') + + '
'; + } + + static renderGallery(block) { + var images = block.images || []; + var items = images.map(function(img) { + return ''; + }).join(''); + return ''; + } + + static renderGrid4(block) { + var images = (block.images || []).slice(0, 4); + var items = images.map(function(img) { + return '
' + + '' + (img.alt || '') + '' + + (img.caption ? '
' + img.caption + '
' : '') + + '
'; + }).join(''); + return '
' + items + '
'; + } + + static renderStatistics(block) { + var stats = block.stats || []; + var items = stats.map(function(s) { + return '
' + + '
' + (s.number || '') + '
' + + '
' + (s.label || '') + '
' + + '
'; + }).join(''); + return '
' + items + '
'; + } + + static renderMapButton(block) { + return '
' + + '
'; + } +} + diff --git a/js/builder.js b/js/builder.js new file mode 100644 index 0000000..de6a0c0 --- /dev/null +++ b/js/builder.js @@ -0,0 +1,528 @@ +/** + * Section Builder - Project Friction + * Block-based page builder for the 5 exhibit sections. + * Saves to localStorage; export/import as JSON. + */ +class SectionBuilder { + + constructor() { + this.sections = { + context: { title: 'Historical Context', icon: '🌍', color: '#d4a574', blocks: [] }, + rcaf: { title: 'RCAF Desert Cats', icon: '✈️', color: '#3498db', blocks: [] }, + rcn: { title: 'RCN Task Group', icon: 'βš“', color: '#2c3e50', blocks: [] }, + army: { title: 'Canadian Army', icon: 'πŸ₯', color: '#27ae60', blocks: [] }, + legacy: { title: 'After the War', icon: '🍁', color: '#c0392b', blocks: [] } + }; + this.currentSection = 'context'; + this.editingIndex = null; + this.defaultContent = null; + this.init(); + } + + async init() { + try { + var res = await fetch('data/sections-content.json'); + if (res.ok) this.defaultContent = await res.json(); + } catch (e) { console.warn('Could not load default content'); } + + this.loadAllFromStorage(); + this.renderSectionTabs(); + this.bindGlobalEvents(); + this.loadSection('context'); + } + + // ── Storage ──────────────────────────────────────────────────────────── + + loadAllFromStorage() { + var self = this; + Object.keys(this.sections).forEach(function(id) { + var saved = localStorage.getItem('pf-section-' + id); + if (saved) { + try { self.sections[id].blocks = JSON.parse(saved); } catch(e) {} + } else if (self.defaultContent && self.defaultContent[id]) { + self.sections[id].blocks = JSON.parse(JSON.stringify(self.defaultContent[id].blocks)); + } + }); + } + + saveSection(id) { + localStorage.setItem('pf-section-' + id, JSON.stringify(this.sections[id].blocks)); + this.showToast(this.sections[id].title + ' saved!'); + } + + // ── Section Loading ──────────────────────────────────────────────────── + + loadSection(id) { + this.currentSection = id; + var sec = this.sections[id]; + + document.querySelectorAll('.section-tab').forEach(function(t) { + t.classList.toggle('active', t.dataset.section === id); + }); + + document.getElementById('canvasSectionTitle').textContent = sec.icon + ' ' + sec.title; + var badge = document.getElementById('canvasBadge'); + badge.textContent = sec.blocks.length + ' block' + (sec.blocks.length !== 1 ? 's' : ''); + badge.style.background = sec.color; + + this.renderCanvas(); + } + + // ── Canvas Rendering ─────────────────────────────────────────────────── + + renderCanvas() { + var canvas = document.getElementById('builderCanvas'); + var blocks = this.sections[this.currentSection].blocks; + var self = this; + + if (blocks.length === 0) { + canvas.innerHTML = '
' + + '

No blocks yet.

' + + '

Click a block type in the sidebar to add one.

' + + '
'; + return; + } + + var html = blocks.map(function(block, index) { + return self.renderCanvasBlock(block, index, blocks.length); + }).join(''); + + html += '
' + + '' + + '
'; + + canvas.innerHTML = html; + this.bindCanvasEvents(); + } + + renderCanvasBlock(block, index, total) { + var preview = BlockRenderer.renderBlock(block); + var isFirst = index === 0; + var isLast = index === total - 1; + var typeLabel = this.blockTypeLabel(block.type); + + return '
' + + '
' + + '' + typeLabel + '' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + preview + '
' + + '
' + + '' + + '
' + + '
'; + } + + bindCanvasEvents() { + var self = this; + + document.querySelectorAll('[data-action]').forEach(function(btn) { + btn.addEventListener('click', function() { + var action = btn.dataset.action; + var idx = parseInt(btn.dataset.index); + if (action === 'edit') self.openEditModal(idx); + if (action === 'delete') self.deleteBlock(idx); + if (action === 'up') self.moveBlock(idx, -1); + if (action === 'down') self.moveBlock(idx, 1); + }); + }); + + document.querySelectorAll('.insert-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + self.pendingInsertIndex = parseInt(btn.dataset.index); + self.highlightPalette(); + }); + }); + } + + highlightPalette() { + var palette = document.querySelector('.palette-blocks'); + palette.classList.add('palette-highlight'); + setTimeout(function() { palette.classList.remove('palette-highlight'); }, 1200); + palette.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + + // ── Block CRUD ───────────────────────────────────────────────────────── + + addBlock(type) { + var block = this.createDefaultBlock(type); + var blocks = this.sections[this.currentSection].blocks; + var atIdx = (this.pendingInsertIndex !== undefined) ? this.pendingInsertIndex : blocks.length; + this.pendingInsertIndex = undefined; + + if (atIdx >= 0 && atIdx <= blocks.length) { + blocks.splice(atIdx, 0, block); + } else { + blocks.push(block); + } + + this.renderCanvas(); + var newIdx = blocks.indexOf(block); + this.openEditModal(newIdx); + } + + createDefaultBlock(type) { + var id = 'b' + Date.now(); + switch (type) { + case 'heading': return { id: id, type: type, level: 2, text: 'New Heading' }; + case 'paragraph': return { id: id, type: type, text: 'Enter paragraph text here.' }; + case 'image': return { id: id, type: type, src: '', alt: '', caption: '' }; + case 'quote': return { id: id, type: type, text: 'Quote text here.', author: 'Source' }; + case 'gallery': return { id: id, type: type, images: [{ src: '', alt: '', caption: '' }] }; + case 'grid4': return { id: id, type: type, images: [ + { src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' }, + { src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' } + ]}; + case 'statistics': return { id: id, type: type, stats: [{ number: '0', label: 'Label' }, { number: '0', label: 'Label' }] }; + case 'map-button': return { id: id, type: type, label: 'View on Map', lat: 29.3759, lng: 47.9774, zoom: 6 }; + default: return { id: id, type: type }; + } + } + + deleteBlock(index) { + if (!confirm('Delete this block?')) return; + this.sections[this.currentSection].blocks.splice(index, 1); + this.renderCanvas(); + } + + moveBlock(index, direction) { + var blocks = this.sections[this.currentSection].blocks; + var newIndex = index + direction; + if (newIndex < 0 || newIndex >= blocks.length) return; + var tmp = blocks[index]; blocks[index] = blocks[newIndex]; blocks[newIndex] = tmp; + this.renderCanvas(); + // Scroll moved block into view + var el = document.querySelector('[data-index="' + newIndex + '"]'); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + + // ── Modal Editor ─────────────────────────────────────────────────────── + + openEditModal(index) { + this.editingIndex = index; + var block = this.sections[this.currentSection].blocks[index]; + document.getElementById('modalTitle').textContent = 'Edit: ' + this.blockTypeLabel(block.type); + document.getElementById('modalBody').innerHTML = this.buildModalForm(block); + document.getElementById('builderModal').classList.remove('hidden'); + this.bindDynamicFormEvents(document.getElementById('modalBody')); + var first = document.getElementById('modalBody').querySelector('input, textarea, select'); + if (first) first.focus(); + } + + buildModalForm(block) { + switch (block.type) { + case 'heading': + return this.formGroup('Level', + '') + + this.formGroup('Text', ''); + + case 'paragraph': + return this.formGroup('Text', ''); + + case 'image': + return this.formGroup('Image URL', '') + + this.formGroup('Alt Text (accessibility)', '') + + this.formGroup('Caption (optional)', ''); + + case 'quote': + return this.formGroup('Quote Text', '') + + this.formGroup('Attribution (optional)', ''); + + case 'gallery': + return this.formGroup('Gallery Images', + '
' + + (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('') + + '
' + + ''); + + case 'grid4': + return this.formGroup('4-Image Grid', + '
' + + [0,1,2,3].map(function(i) { return SectionBuilder.imageRow(i, (block.images || [])[i] || {}, false); }).join('') + + '
'); + + case 'statistics': + return this.formGroup('Statistics', + '
' + + (block.stats || []).map(function(s, i) { return SectionBuilder.statRow(i, s); }).join('') + + '
' + + ''); + + case 'map-button': + return this.formGroup('Button Label', '') + + '
' + + this.formGroup('Latitude', '') + + this.formGroup('Longitude', '') + + this.formGroup('Zoom (1–18)', '') + + '
'; + + default: + return '

Unknown block type.

'; + } + } + + static imageRow(index, img, removable) { + return '
' + + '
' + + '' + + '' + + '' + + '
' + + (removable ? '' : '') + + '
'; + } + + static statRow(index, stat) { + return '
' + + '' + + '' + + '' + + '
'; + } + + bindDynamicFormEvents(container) { + var self = this; + + var addImgBtn = container.querySelector('.btn-add-img'); + if (addImgBtn) { + addImgBtn.addEventListener('click', function() { + var items = container.querySelector('#galleryItems'); + var newIdx = items.querySelectorAll('.item-row').length; + items.insertAdjacentHTML('beforeend', SectionBuilder.imageRow(newIdx, {}, true)); + self.bindRemoveBtns(container); + }); + } + + var addStatBtn = container.querySelector('.btn-add-stat'); + if (addStatBtn) { + addStatBtn.addEventListener('click', function() { + var items = container.querySelector('#statsItems'); + var newIdx = items.querySelectorAll('.item-row').length; + items.insertAdjacentHTML('beforeend', SectionBuilder.statRow(newIdx, {})); + self.bindRemoveBtns(container); + }); + } + + this.bindRemoveBtns(container); + } + + bindRemoveBtns(container) { + container.querySelectorAll('.btn-remove-item').forEach(function(btn) { + btn.onclick = function() { + btn.closest('.item-row').remove(); + container.querySelectorAll('.item-row').forEach(function(row, i) { + row.dataset.index = i; + row.querySelectorAll('[data-index]').forEach(function(el) { el.dataset.index = i; }); + }); + }; + }); + } + + saveModalBlock() { + var block = this.sections[this.currentSection].blocks[this.editingIndex]; + var body = document.getElementById('modalBody'); + + switch (block.type) { + case 'heading': + block.level = parseInt(body.querySelector('[name="level"]').value); + block.text = body.querySelector('[name="text"]').value; + break; + case 'paragraph': + block.text = body.querySelector('[name="text"]').value; + break; + case 'image': + block.src = body.querySelector('[name="src"]').value; + block.alt = body.querySelector('[name="alt"]').value; + block.caption = body.querySelector('[name="caption"]').value; + break; + case 'quote': + block.text = body.querySelector('[name="text"]').value; + block.author = body.querySelector('[name="author"]').value; + break; + case 'gallery': + block.images = this.readItemRows(body.querySelector('#galleryItems'), ['src','alt','caption']); + break; + case 'grid4': + block.images = this.readItemRows(body.querySelector('#grid4Items'), ['src','alt','caption']); + break; + case 'statistics': + block.stats = this.readItemRows(body.querySelector('#statsItems'), ['number','label']); + break; + case 'map-button': + block.label = body.querySelector('[name="label"]').value; + block.lat = parseFloat(body.querySelector('[name="lat"]').value); + block.lng = parseFloat(body.querySelector('[name="lng"]').value); + block.zoom = parseInt(body.querySelector('[name="zoom"]').value); + break; + } + + this.closeModal(); + this.renderCanvas(); + } + + readItemRows(container, fields) { + if (!container) return []; + return Array.from(container.querySelectorAll('.item-row')).map(function(row) { + var obj = {}; + fields.forEach(function(field) { + var el = row.querySelector('[data-field="' + field + '"]'); + obj[field] = el ? el.value : ''; + }); + return obj; + }); + } + + closeModal() { + document.getElementById('builderModal').classList.add('hidden'); + this.editingIndex = null; + } + + // ── UI Helpers ───────────────────────────────────────────────────────── + + renderSectionTabs() { + var self = this; + var container = document.getElementById('sectionTabs'); + container.innerHTML = Object.entries(this.sections).map(function(entry) { + var id = entry[0], sec = entry[1]; + return ''; + }).join(''); + + container.querySelectorAll('.section-tab').forEach(function(btn) { + btn.addEventListener('click', function() { self.loadSection(btn.dataset.section); }); + }); + } + + bindGlobalEvents() { + var self = this; + + document.getElementById('saveBtn').addEventListener('click', function() { + if (self.currentSection) self.saveSection(self.currentSection); + }); + + document.getElementById('resetBtn').addEventListener('click', function() { + if (!self.currentSection) return; + if (!confirm('Reset to default content? Saved changes will be lost.')) return; + localStorage.removeItem('pf-section-' + self.currentSection); + if (self.defaultContent && self.defaultContent[self.currentSection]) { + self.sections[self.currentSection].blocks = JSON.parse(JSON.stringify(self.defaultContent[self.currentSection].blocks)); + } else { + self.sections[self.currentSection].blocks = []; + } + self.renderCanvas(); + self.showToast('Section reset to default.'); + }); + + document.getElementById('exportBtn').addEventListener('click', function() { self.exportJSON(); }); + + document.getElementById('importBtn').addEventListener('click', function() { + document.getElementById('importFile').click(); + }); + + document.getElementById('importFile').addEventListener('change', function(e) { + var file = e.target.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = function(evt) { + try { + var data = JSON.parse(evt.target.result); + Object.keys(self.sections).forEach(function(id) { + if (data[id] && data[id].blocks) { + self.sections[id].blocks = data[id].blocks; + localStorage.setItem('pf-section-' + id, JSON.stringify(data[id].blocks)); + } + }); + self.loadSection(self.currentSection); + self.showToast('Content imported!'); + } catch (err) { + alert('Invalid JSON file: ' + err.message); + } + }; + reader.readAsText(file); + e.target.value = ''; + }); + + document.querySelectorAll('.palette-block').forEach(function(btn) { + btn.addEventListener('click', function() { + if (!self.currentSection) return; + self.addBlock(btn.dataset.type); + }); + }); + + document.getElementById('modalSave').addEventListener('click', function() { self.saveModalBlock(); }); + document.getElementById('modalCancel').addEventListener('click', function() { self.closeModal(); }); + document.getElementById('modalClose').addEventListener('click', function() { self.closeModal(); }); + document.querySelector('.modal-overlay').addEventListener('click', function() { self.closeModal(); }); + + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') self.closeModal(); + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + if (self.currentSection) self.saveSection(self.currentSection); + } + }); + } + + exportJSON() { + var output = {}; + Object.keys(this.sections).forEach(function(id) { + output[id] = { blocks: this.sections[id].blocks }; + }, this); + var blob = new Blob([JSON.stringify(output, null, 2)], { type: 'application/json' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'sections-content.json'; + a.click(); + URL.revokeObjectURL(url); + } + + showToast(message) { + var toast = document.createElement('div'); + toast.className = 'builder-toast'; + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(function() { toast.classList.add('show'); }, 10); + setTimeout(function() { + toast.classList.remove('show'); + setTimeout(function() { toast.remove(); }, 300); + }, 2500); + } + + blockTypeLabel(type) { + var labels = { + heading: 'H Heading', paragraph: 'ΒΆ Paragraph', image: 'πŸ–Ό Image', + quote: '" Quote', gallery: '⊞ Gallery', grid4: 'β–¦ Grid 4', + statistics: '# Statistics', 'map-button': 'πŸ“ Map Button' + }; + return labels[type] || type; + } + + formGroup(label, inputHtml) { + return '
' + inputHtml + '
'; + } + + esc(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + } +} + +document.addEventListener('DOMContentLoaded', function() { + window.builder = new SectionBuilder(); +}); + diff --git a/main.css b/main.css new file mode 100644 index 0000000..07b8439 --- /dev/null +++ b/main.css @@ -0,0 +1,404 @@ +/* Project Friction - Main CSS */ +/* Museum-quality digital exhibit styling */ + +:root { + /* Color Palette */ + --color-primary: #2c3e50; + --color-secondary: #34495e; + --color-accent: #c0392b; + --color-context: #d4a574; + --color-rcaf: #3498db; + --color-rcn: #2c3e50; + --color-army: #27ae60; + --color-legacy: #c0392b; + + /* Neutral Colors */ + --color-white: #ffffff; + --color-light-gray: #f8f9fa; + --color-gray: #dee2e6; + --color-dark-gray: #6c757d; + --color-black: #212529; + + /* Typography */ + --font-primary: 'Georgia', 'Times New Roman', serif; + --font-secondary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-xxl: 3rem; + + /* Transitions */ + --transition-fast: 0.15s ease; + --transition-normal: 0.3s ease; + --transition-slow: 0.5s ease; + + /* Shadows */ + --shadow-sm: 0 2px 5px rgba(0,0,0,0.1); + --shadow-md: 0 4px 10px rgba(0,0,0,0.1); + --shadow-lg: 0 8px 20px rgba(0,0,0,0.15); + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 50%; +} + +/* Reset and Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-primary); + background: var(--color-light-gray); + color: var(--color-black); + overflow: hidden; + height: 100vh; + line-height: 1.6; +} + +/* Layout Structure */ +.app-container { + display: flex; + height: 100vh; + position: relative; +} + +/* Sidebar Styling */ +.sidebar { + width: 420px; + background: var(--color-white); + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + z-index: 100; + transition: transform var(--transition-normal); + position: relative; +} + +.sidebar.collapsed { + transform: translateX(-370px); +} + +.sidebar-toggle { + position: absolute; + right: -30px; + top: 20px; + width: 30px; + height: 60px; + background: var(--color-white); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 101; + transition: background-color var(--transition-fast); +} + +.sidebar-toggle:hover { + background: var(--color-light-gray); +} + +/* Header Styling */ +.sidebar-header { + background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%); + color: var(--color-white); + padding: var(--spacing-xl); + border-bottom: 3px solid var(--color-accent); +} + +.logo-section { + display: flex; + align-items: center; + gap: var(--spacing-lg); +} + +.logo { + width: 45px; + height: 45px; + background: var(--color-accent); + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-white); + font-weight: 600; + font-size: 18px; + font-family: var(--font-secondary); +} + +.title h1 { + font-size: 22px; + font-weight: 600; + margin-bottom: var(--spacing-xs); +} + +.title p { + font-size: 13px; + opacity: 0.8; + font-style: italic; +} + +/* Navigation States */ +.back-button { + padding: var(--spacing-md) var(--spacing-xl); + background: var(--color-light-gray); + border-bottom: 1px solid var(--color-gray); + display: flex; + align-items: center; + gap: var(--spacing-md); + cursor: pointer; + transition: background-color var(--transition-fast); + font-family: var(--font-secondary); +} + +.back-button:hover { + background: #e9ecef; +} + +.back-button.hidden { + display: none; +} + +.back-button.show { + display: flex; +} + +/* Main Navigation */ +.main-navigation { + display: block; + flex: 1; + overflow-y: auto; +} + +.main-navigation.hidden { + display: none; +} + +.nav-welcome { + padding: var(--spacing-xl); + background: var(--color-light-gray); + border-bottom: 1px solid var(--color-gray); +} + +.nav-welcome h2 { + font-size: 18px; + color: var(--color-black); + margin-bottom: var(--spacing-md); + font-family: var(--font-primary); +} + +.nav-welcome p { + font-size: 14px; + line-height: 1.6; + color: var(--color-dark-gray); + font-family: var(--font-secondary); +} + +/* Quick Stats */ +.quick-stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-md); + padding: var(--spacing-xl); + background: var(--color-white); +} + +.stat-card { + text-align: center; + padding: var(--spacing-lg); + background: var(--color-light-gray); + border-radius: var(--radius-md); + border: 1px solid #e9ecef; + transition: transform var(--transition-fast); +} + +.stat-card:hover { + transform: translateY(-2px); +} + +.stat-number { + font-size: 22px; + font-weight: 600; + color: var(--color-rcaf); + margin-bottom: var(--spacing-xs); + font-family: var(--font-secondary); +} + +.stat-label { + font-size: 11px; + color: var(--color-dark-gray); + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: var(--font-secondary); +} + +/* Navigation Areas */ +.nav-areas { + padding: var(--spacing-xl); +} + +.nav-areas-title { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-dark-gray); + margin-bottom: var(--spacing-lg); + font-weight: 600; + font-family: var(--font-secondary); +} + +/* Map Container */ +.map-container { + flex: 1; + position: relative; + background: var(--color-secondary); +} + +#map { + width: 100%; + height: 100%; +} + +/* Footer */ +.sidebar-footer { + padding: var(--spacing-lg) var(--spacing-xl); + background: var(--color-light-gray); + border-top: 1px solid var(--color-gray); + font-size: 12px; + color: var(--color-dark-gray); + text-align: center; + font-family: var(--font-secondary); +} + +.sidebar-footer a { + color: var(--color-rcaf); + text-decoration: none; + transition: color var(--transition-fast); +} + +.sidebar-footer a:hover { + color: var(--color-primary); +} + +/* Utility Classes */ +.hidden { + display: none !important; +} + +.show { + display: block !important; +} + +.text-center { + text-align: center; +} + +.font-secondary { + font-family: var(--font-secondary); +} + +.font-primary { + font-family: var(--font-primary); +} + +/* Loading Screen */ +.loading-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--color-white); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + transition: opacity var(--transition-slow); +} + +.loading-screen.fade-out { + opacity: 0; + pointer-events: none; +} + +.loading-content { + text-align: center; + color: var(--color-primary); +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 4px solid var(--color-gray); + border-top: 4px solid var(--color-accent); + border-radius: var(--radius-full); + animation: spin 1s linear infinite; + margin: 0 auto var(--spacing-lg); +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-content h3 { + font-size: 20px; + margin-bottom: var(--spacing-sm); + font-family: var(--font-primary); +} + +.loading-content p { + font-size: 14px; + color: var(--color-dark-gray); + font-family: var(--font-secondary); +} + +/* Focus and Accessibility */ +*:focus { + outline: 2px solid var(--color-rcaf); + outline-offset: 2px; +} + +button:focus, +[role="button"]:focus { + outline: 2px solid var(--color-rcaf); + outline-offset: 2px; +} + +/* Print Styles */ +@media print { + .sidebar-toggle, + .map-container, + .back-button { + display: none !important; + } + + .sidebar { + width: 100%; + transform: none; + box-shadow: none; + } + + .app-container { + display: block; + } + + body { + overflow: visible; + height: auto; + } +} \ No newline at end of file diff --git a/map-data.json b/map-data.json new file mode 100644 index 0000000..9297e71 --- /dev/null +++ b/map-data.json @@ -0,0 +1,164 @@ +{ + "context": { + "markers": [ + { + "lat": 29.3759, + "lng": 47.9774, + "title": "Kuwait City", + "description": "Capital of Kuwait, occupied August 2-February 26, 1991", + "type": "events", + "date": "1990-08-02", + "category": "invasion" + }, + { + "lat": 33.3152, + "lng": 44.3661, + "title": "Baghdad", + "description": "Iraqi capital and command center", + "type": "iraqi", + "category": "command" + }, + { + "lat": 30.5852, + "lng": 47.7817, + "title": "Basra", + "description": "Southern Iraq staging area", + "type": "iraqi", + "category": "military" + } + ], + "routes": [ + { + "name": "Northern Invasion Route", + "coordinates": [[30.0051, 47.7034], [29.3759, 47.9774]], + "type": "invasion", + "date": "1990-08-02" + }, + { + "name": "Central Invasion Route", + "coordinates": [[29.9234, 47.8456], [29.3759, 47.9774]], + "type": "invasion", + "date": "1990-08-02" + }, + { + "name": "Southern Invasion Route", + "coordinates": [[29.6678, 48.0234], [29.3759, 47.9774]], + "type": "invasion", + "date": "1990-08-02" + } + ] + }, + "rcaf": { + "markers": [ + { + "lat": 25.2731, + "lng": 51.6080, + "title": "Canada Dry 1 - Doha", + "description": "Main CF-18 operating base, 409 Squadron", + "type": "canadian", + "category": "airbase" + }, + { + "lat": 26.0667, + "lng": 50.5577, + "title": "CAP Patrol Area", + "description": "Combat Air Patrol protection zone", + "type": "canadian", + "category": "operations" + }, + { + "lat": 29.1449, + "lng": 48.1251, + "title": "TNC-45 Engagement", + "description": "Iraqi patrol boat engagement, January 30, 1991", + "type": "events", + "date": "1991-01-30", + "category": "combat" + } + ] + }, + "rcn": { + "markers": [ + { + "lat": 26.2285, + "lng": 50.5860, + "title": "HMCS Terra Nova", + "description": "Canadian guided-missile destroyer", + "type": "canadian", + "category": "naval" + }, + { + "lat": 27.5000, + "lng": 50.0000, + "title": "MIF Operations Area", + "description": "Maritime Interception Force patrol zone", + "type": "coalition", + "category": "naval" + }, + { + "lat": 26.0667, + "lng": 50.5577, + "title": "Bahrain Naval Base", + "description": "Coalition naval headquarters", + "type": "coalition", + "category": "base" + } + ] + }, + "army": { + "markers": [ + { + "lat": 28.4312, + "lng": 46.0897, + "title": "1 Canadian Field Hospital", + "description": "Al-Qaysumah, Saudi Arabia - 530 personnel", + "type": "canadian", + "category": "medical" + }, + { + "lat": 26.0667, + "lng": 50.5577, + "title": "Base Security - Bahrain", + "description": "RCR security operations", + "type": "canadian", + "category": "security" + }, + { + "lat": 37.0000, + "lng": 43.0000, + "title": "Operation Provide Comfort", + "description": "Kurdish refugee relief operations", + "type": "canadian", + "category": "humanitarian" + } + ] + }, + "legacy": { + "markers": [ + { + "lat": 29.3759, + "lng": 47.9774, + "title": "Environmental Cleanup Zone", + "description": "Post-war environmental restoration", + "type": "events", + "category": "cleanup" + }, + { + "lat": 30.3753, + "lng": 47.7903, + "title": "UNIKOM Peacekeeping", + "description": "Iraq-Kuwait observation mission", + "type": "coalition", + "category": "peacekeeping" + }, + { + "lat": 45.4215, + "lng": -75.6972, + "title": "Veterans Affairs Canada", + "description": "Ottawa - Veteran support services", + "type": "canadian", + "category": "veterans" + } + ] + } +} \ No newline at end of file