This commit is contained in:
alexandrev-tibco
2026-01-13 17:29:39 +01:00
commit 92de2ea68d
23 changed files with 4113 additions and 0 deletions
+1733
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180" viewBox="0 0 180 180" role="img" aria-label="PorfolioJournal">
<defs>
<linearGradient id="pj-gradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#667eea"/>
<stop offset="100%" stop-color="#764ba2"/>
</linearGradient>
</defs>
<rect x="12" y="12" width="156" height="156" rx="36" fill="url(#pj-gradient)"/>
<path d="M56 124c0-28 20-48 44-48s44 20 44 48" fill="none" stroke="white" stroke-width="10" stroke-linecap="round"/>
<path d="M64 74h72" fill="none" stroke="white" stroke-width="10" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="PorfolioJournal">
<defs>
<linearGradient id="pj-gradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#667eea"/>
<stop offset="100%" stop-color="#764ba2"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="56" height="56" rx="14" fill="url(#pj-gradient)"/>
<path d="M20 44c0-10 8-18 16-18s16 8 16 18" fill="none" stroke="white" stroke-width="4" stroke-linecap="round"/>
<path d="M24 26h24" fill="none" stroke="white" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="PorfolioJournal app icon">
<defs>
<linearGradient id="pj-gradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#667eea"/>
<stop offset="100%" stop-color="#764ba2"/>
</linearGradient>
</defs>
<rect x="32" y="32" width="448" height="448" rx="96" fill="url(#pj-gradient)"/>
<path d="M160 336c0-64 48-112 96-112s96 48 96 112" fill="none" stroke="white" stroke-width="28" stroke-linecap="round"/>
<path d="M192 208h128" fill="none" stroke="white" stroke-width="28" stroke-linecap="round"/>
<circle cx="208" cy="176" r="12" fill="white"/>
<circle cx="304" cy="176" r="12" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 747 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-label="PorfolioJournal">
<defs>
<linearGradient id="pj-gradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#667eea"/>
<stop offset="100%" stop-color="#764ba2"/>
</linearGradient>
</defs>
<rect width="1200" height="630" fill="#f5f5f7"/>
<rect x="96" y="96" width="144" height="144" rx="36" fill="url(#pj-gradient)"/>
<path d="M130 190c0-26 20-46 46-46s46 20 46 46" fill="none" stroke="white" stroke-width="10" stroke-linecap="round"/>
<path d="M138 138h72" fill="none" stroke="white" stroke-width="10" stroke-linecap="round"/>
<text x="96" y="310" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif" font-size="64" fill="#1d1d1f" font-weight="600">PorfolioJournal</text>
<text x="96" y="380" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="32" fill="#6e6e73">Offline-first portfolio tracking with calm monthly check-ins.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1800" viewBox="0 0 900 1800" role="img" aria-label="PorfolioJournal screenshot placeholder">
<defs>
<linearGradient id="pj-grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f5f5f7"/>
<stop offset="100%" stop-color="#ffffff"/>
</linearGradient>
</defs>
<rect width="900" height="1800" rx="140" fill="url(#pj-grad)" stroke="#e5e5ea" stroke-width="8"/>
<rect x="110" y="180" width="680" height="120" rx="36" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="340" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="600" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="860" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<text x="110" y="140" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="40" fill="#6e6e73">Screenshot placeholder</text>
<text x="110" y="1150" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="36" fill="#8e8e93">Replace with real app screens</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1800" viewBox="0 0 900 1800" role="img" aria-label="PorfolioJournal screenshot placeholder">
<defs>
<linearGradient id="pj-grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f5f5f7"/>
<stop offset="100%" stop-color="#ffffff"/>
</linearGradient>
</defs>
<rect width="900" height="1800" rx="140" fill="url(#pj-grad)" stroke="#e5e5ea" stroke-width="8"/>
<rect x="110" y="180" width="680" height="120" rx="36" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="340" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="600" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="860" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<text x="110" y="140" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="40" fill="#6e6e73">Screenshot placeholder</text>
<text x="110" y="1150" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="36" fill="#8e8e93">Replace with real app screens</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1800" viewBox="0 0 900 1800" role="img" aria-label="PorfolioJournal screenshot placeholder">
<defs>
<linearGradient id="pj-grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f5f5f7"/>
<stop offset="100%" stop-color="#ffffff"/>
</linearGradient>
</defs>
<rect width="900" height="1800" rx="140" fill="url(#pj-grad)" stroke="#e5e5ea" stroke-width="8"/>
<rect x="110" y="180" width="680" height="120" rx="36" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="340" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="600" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="860" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<text x="110" y="140" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="40" fill="#6e6e73">Screenshot placeholder</text>
<text x="110" y="1150" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="36" fill="#8e8e93">Replace with real app screens</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1800" viewBox="0 0 900 1800" role="img" aria-label="PorfolioJournal screenshot placeholder">
<defs>
<linearGradient id="pj-grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f5f5f7"/>
<stop offset="100%" stop-color="#ffffff"/>
</linearGradient>
</defs>
<rect width="900" height="1800" rx="140" fill="url(#pj-grad)" stroke="#e5e5ea" stroke-width="8"/>
<rect x="110" y="180" width="680" height="120" rx="36" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="340" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="600" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<rect x="110" y="860" width="680" height="220" rx="48" fill="#ffffff" stroke="#e5e5ea"/>
<text x="110" y="140" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="40" fill="#6e6e73">Screenshot placeholder</text>
<text x="110" y="1150" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif" font-size="36" fill="#8e8e93">Replace with real app screens</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+28
View File
@@ -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);
+280
View File
@@ -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;
})();