init
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* PorfolioJournal Site Configuration
|
||||
* Edit these values to customize the site
|
||||
*/
|
||||
const CONFIG = {
|
||||
APP_NAME: 'PorfolioJournal',
|
||||
TAGLINE: 'Your personal portfolio, thoughtfully tracked.',
|
||||
DESCRIPTION: 'A calm, offline-first iOS app for tracking your portfolio with monthly check-ins, reflections, and long-term insights.',
|
||||
APP_STORE_URL: 'https://apps.apple.com/app/porfoliojournal/id000000000',
|
||||
SUPPORT_EMAIL: 'support@portfoliojournal.app',
|
||||
COMPANY_NAME: 'PorfolioJournal',
|
||||
WEBSITE_URL: 'https://portfoliojournal.app',
|
||||
EFFECTIVE_DATE: 'today',
|
||||
COPYRIGHT_YEAR: new Date().getFullYear(),
|
||||
ENABLE_ANALYTICS: false,
|
||||
ANALYTICS_ID: '', // Add your analytics ID if enabled
|
||||
|
||||
// Social links (optional)
|
||||
SOCIAL: {
|
||||
TWITTER: '',
|
||||
GITHUB: '',
|
||||
MASTODON: ''
|
||||
}
|
||||
};
|
||||
|
||||
// Freeze config to prevent accidental modifications
|
||||
Object.freeze(CONFIG);
|
||||
Object.freeze(CONFIG.SOCIAL);
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* PortfolioJournal Main JavaScript
|
||||
* Handles dark mode, navigation, and dynamic content
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Dark Mode Management
|
||||
const DarkMode = {
|
||||
STORAGE_KEY: 'pj-theme',
|
||||
|
||||
init() {
|
||||
const saved = localStorage.getItem(this.STORAGE_KEY);
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (saved === 'dark' || (!saved && prefersDark)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
|
||||
// Listen for system preference changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (!localStorage.getItem(this.STORAGE_KEY)) {
|
||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggle() {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem(this.STORAGE_KEY, next);
|
||||
this.updateToggleButton();
|
||||
},
|
||||
|
||||
updateToggleButton() {
|
||||
const btn = document.querySelector('.theme-toggle');
|
||||
if (btn) {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
btn.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode');
|
||||
btn.innerHTML = isDark ? this.getSunIcon() : this.getMoonIcon();
|
||||
}
|
||||
},
|
||||
|
||||
getSunIcon() {
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`;
|
||||
},
|
||||
|
||||
getMoonIcon() {
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`;
|
||||
}
|
||||
};
|
||||
|
||||
// Navigation Management
|
||||
const Navigation = {
|
||||
init() {
|
||||
this.setActiveLink();
|
||||
this.setupMobileMenu();
|
||||
this.setupKeyboardNav();
|
||||
},
|
||||
|
||||
setActiveLink() {
|
||||
const currentPath = window.location.pathname;
|
||||
const links = document.querySelectorAll('.nav-link');
|
||||
|
||||
links.forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
const isActive = currentPath.endsWith(href) ||
|
||||
(href === 'index.html' && (currentPath === '/' || currentPath.endsWith('/')));
|
||||
|
||||
if (isActive) {
|
||||
link.classList.add('active');
|
||||
link.setAttribute('aria-current', 'page');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupMobileMenu() {
|
||||
const toggle = document.querySelector('.mobile-menu-toggle');
|
||||
const nav = document.querySelector('.nav-links');
|
||||
|
||||
if (toggle && nav) {
|
||||
toggle.addEventListener('click', () => {
|
||||
const isOpen = nav.classList.toggle('open');
|
||||
toggle.setAttribute('aria-expanded', isOpen);
|
||||
toggle.innerHTML = isOpen ? this.getCloseIcon() : this.getMenuIcon();
|
||||
});
|
||||
|
||||
// Close menu on link click
|
||||
nav.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
nav.classList.remove('open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.innerHTML = this.getMenuIcon();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupKeyboardNav() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const nav = document.querySelector('.nav-links');
|
||||
const toggle = document.querySelector('.mobile-menu-toggle');
|
||||
if (nav && nav.classList.contains('open')) {
|
||||
nav.classList.remove('open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.innerHTML = this.getMenuIcon();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getMenuIcon() {
|
||||
return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18M3 6h18M3 18h18"/></svg>`;
|
||||
},
|
||||
|
||||
getCloseIcon() {
|
||||
return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`;
|
||||
}
|
||||
};
|
||||
|
||||
// Dynamic Content
|
||||
const Content = {
|
||||
init() {
|
||||
this.insertYear();
|
||||
this.insertConfig();
|
||||
this.insertEffectiveDate();
|
||||
},
|
||||
|
||||
insertYear() {
|
||||
document.querySelectorAll('[data-year]').forEach(el => {
|
||||
el.textContent = CONFIG.COPYRIGHT_YEAR;
|
||||
});
|
||||
},
|
||||
|
||||
insertConfig() {
|
||||
document.querySelectorAll('[data-config]').forEach(el => {
|
||||
const key = el.getAttribute('data-config');
|
||||
if (CONFIG[key]) {
|
||||
if (el.tagName === 'A') {
|
||||
el.href = CONFIG[key];
|
||||
} else {
|
||||
el.textContent = CONFIG[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
insertEffectiveDate() {
|
||||
document.querySelectorAll('[data-effective-date]').forEach(el => {
|
||||
const dateValue = CONFIG.EFFECTIVE_DATE === 'today' ? new Date() : new Date(CONFIG.EFFECTIVE_DATE);
|
||||
const date = Number.isNaN(dateValue.getTime()) ? new Date() : dateValue;
|
||||
el.textContent = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Smooth Scroll
|
||||
const SmoothScroll = {
|
||||
init() {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', (e) => {
|
||||
const targetId = anchor.getAttribute('href');
|
||||
if (targetId === '#') return;
|
||||
|
||||
const target = document.querySelector(targetId);
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
target.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Cookie Notice (only if analytics enabled)
|
||||
const CookieNotice = {
|
||||
STORAGE_KEY: 'pj-cookies-accepted',
|
||||
|
||||
init() {
|
||||
if (!CONFIG.ENABLE_ANALYTICS) return;
|
||||
if (localStorage.getItem(this.STORAGE_KEY)) return;
|
||||
|
||||
this.show();
|
||||
},
|
||||
|
||||
show() {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'cookie-notice';
|
||||
notice.innerHTML = `
|
||||
<div class="cookie-notice-content">
|
||||
<p>We use analytics to improve your experience. No personal data is shared with third parties.</p>
|
||||
<div class="cookie-notice-actions">
|
||||
<button class="btn btn-small btn-secondary" onclick="CookieNotice.decline()">Decline</button>
|
||||
<button class="btn btn-small btn-primary" onclick="CookieNotice.accept()">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(notice);
|
||||
},
|
||||
|
||||
accept() {
|
||||
localStorage.setItem(this.STORAGE_KEY, 'true');
|
||||
this.hide();
|
||||
// Initialize analytics here if needed
|
||||
},
|
||||
|
||||
decline() {
|
||||
localStorage.setItem(this.STORAGE_KEY, 'false');
|
||||
this.hide();
|
||||
},
|
||||
|
||||
hide() {
|
||||
const notice = document.querySelector('.cookie-notice');
|
||||
if (notice) notice.remove();
|
||||
}
|
||||
};
|
||||
|
||||
// FAQ Accordion
|
||||
const FAQ = {
|
||||
init() {
|
||||
document.querySelectorAll('.faq-question').forEach(question => {
|
||||
question.addEventListener('click', () => {
|
||||
const item = question.parentElement;
|
||||
const isOpen = item.classList.contains('open');
|
||||
|
||||
// Close all others
|
||||
document.querySelectorAll('.faq-item.open').forEach(openItem => {
|
||||
openItem.classList.remove('open');
|
||||
openItem.querySelector('.faq-question').setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
// Toggle current
|
||||
if (!isOpen) {
|
||||
item.classList.add('open');
|
||||
question.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard support
|
||||
question.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
question.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize everything on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
DarkMode.init();
|
||||
DarkMode.updateToggleButton();
|
||||
Navigation.init();
|
||||
Content.init();
|
||||
SmoothScroll.init();
|
||||
FAQ.init();
|
||||
CookieNotice.init();
|
||||
|
||||
// Setup theme toggle button
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => DarkMode.toggle());
|
||||
}
|
||||
});
|
||||
|
||||
// Expose necessary functions globally
|
||||
window.DarkMode = DarkMode;
|
||||
window.CookieNotice = CookieNotice;
|
||||
})();
|
||||
Reference in New Issue
Block a user