Naprawa błędów awatara

This commit is contained in:
Arek Bykowski
2026-02-15 19:11:08 +01:00
parent 144e28e4c4
commit 9221287d1d
5 changed files with 76 additions and 45 deletions

BIN
.DS_Store vendored

Binary file not shown.

10
App.tsx
View File

@@ -15,6 +15,10 @@ import { getEnvVar } from './utils/envUtils';
import { AUTHOR_CONFIG } from './_EDITABLE_CONFIG/author';
import { UI_TEXT } from './_EDITABLE_CONFIG/ui_text';
// --- ASSET IMPORT ---
// Importujemy logo bezpośrednio, aby bundler poprawnie rozwiązał ścieżkę
import logoImg from './logo.png';
const STORAGE_KEY = 'gpx-storyteller-state-v6';
const AUTH_KEY = 'promptstory-auth-token';
@@ -131,7 +135,7 @@ const App: React.FC = () => {
const [isLoaded, setIsLoaded] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// LOGO & AVATAR STATE: Default to false (no error), so we try to show image first.
// LOGO & AVATAR STATE
const [logoError, setLogoError] = useState(false);
const [avatarError, setAvatarError] = useState(false);
@@ -245,10 +249,10 @@ const App: React.FC = () => {
<header className="bg-white sticky top-0 z-10 border-b border-gray-100">
<div className="max-w-4xl mx-auto px-6 py-4 flex justify-between items-center">
<div className="flex items-center space-x-3">
{/* LOGO LOGIC: Try to show image. If error, fallback to icon/text. */}
{/* LOGO LOGIC: Use imported logoImg */}
{!logoError ? (
<img
src="logo.png"
src={logoImg}
alt="Logo"
onError={() => setLogoError(true)}
className="h-10 object-contain"

View File

@@ -1,10 +1,13 @@
// Import obrazu, aby bundler go dołączył
import avatarImg from '../avatar.jpeg';
export const AUTHOR_CONFIG = {
name: "Arkadiusz AreBynd[] Bykowski",
description: "Zamieniam Twoją stronę w maszynkę do zarabiania pieniędzy. Od 12 lat projektuję strony, które sprzedają bez Twojego udziału póki Ty śpisz, one robią za Ciebie robotę. Specjalizuję się w landing page'ach, które budują zaufanie i zmieniają odwiedzających w płacących klientów.",
name: "Arkadiusz Bykowski",
description: "Zamieniam Twoją stronę w maszynkę do zarabiania pieniędzy. Od 12 lat projektuję strony, które sprzedają bez Twojego udziału.",
tags: ["Product Design", "AI Automation", "No-Code"],
websiteUrl: "https://bykowski.pro/",
websiteLabel: "Odwiedź stronę",
// Nazwa pliku w folderze public
avatarImage: "avatar.jpeg"
// Używamy zaimportowanego obrazu
avatarImage: avatarImg
};

View File

@@ -1 +0,0 @@
// Unused. Logic moved to StepDetails.tsx

View File

@@ -16,10 +16,15 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, 'text/xml');
const trkpts = Array.from(xmlDoc.getElementsByTagName('trkpt'));
// Obsługa różnych wariantów tagu 'trkpt' (z namespace lub bez)
let trkpts = Array.from(xmlDoc.getElementsByTagName('trkpt'));
if (trkpts.length === 0) {
// Spróbuj znaleźć po localName, jeśli namespace'y przeszkadzają
const allElements = xmlDoc.getElementsByTagName('*');
trkpts = Array.from(allElements).filter(el => el.localName === 'trkpt');
}
if (trkpts.length === 0) {
// Fallback logic could be added here for routes without time, but for storytelling we focus on tracks
throw new Error('Nie znaleziono punktów śledzenia (trkpt) w pliku GPX.');
}
@@ -51,44 +56,62 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
let lastLat: number | null = null;
let lastLon: number | null = null;
let lastEle: number | null = null;
let lastTime: number | null = null;
// Helper to get text content from child nodes loosely
const getChildText = (parent: Element, tagName: string): string | null => {
let el = parent.getElementsByTagName(tagName)[0];
if (!el) {
// Try finding by localName if strictly not found
for(let i=0; i<parent.children.length; i++) {
if(parent.children[i].localName === tagName) {
el = parent.children[i];
break;
}
}
}
return el ? el.textContent : null;
};
// --- ITERACJA PO PUNKTACH ---
for (let i = 0; i < trkpts.length; i++) {
const pt = trkpts[i];
const lat = parseFloat(pt.getAttribute('lat') || '0');
const lon = parseFloat(pt.getAttribute('lon') || '0');
const ele = parseFloat(pt.getElementsByTagName('ele')[0]?.textContent || '0');
const timeStr = pt.getElementsByTagName('time')[0]?.textContent;
// Extensions (HR / Cadence)
// Note: Namespaces can vary (ns3:hr, gpxtpx:hr), so we search loosely or by standard tag names inside extensions
const extensions = pt.getElementsByTagName('extensions')[0];
const eleStr = getChildText(pt, 'ele');
const ele = eleStr ? parseFloat(eleStr) : NaN;
const timeStr = getChildText(pt, 'time');
// Extensions search for HR/Cadence
// Przeszukujemy całe poddrzewo punktu w poszukiwaniu tagów hr/cad, bo struktura extensions bywa różna
const allChilds = pt.getElementsByTagName('*');
let hr = 0;
let cad = 0;
if (extensions) {
// Try standard Garmin/GPX schemes
const hrNode = extensions.getElementsByTagName('gpxtpx:hr')[0] || extensions.getElementsByTagName('ns3:hr')[0] || extensions.getElementsByTagName('hr')[0];
const cadNode = extensions.getElementsByTagName('gpxtpx:cad')[0] || extensions.getElementsByTagName('ns3:cad')[0] || extensions.getElementsByTagName('cad')[0];
if (hrNode?.textContent) {
hr = parseInt(hrNode.textContent);
if (!isNaN(hr) && hr > 0) {
totalHr += hr;
hrCount++;
if (hr > maxHr) maxHr = hr;
currentSplitHrSum += hr;
currentSplitHrCount++;
}
}
if (cadNode?.textContent) {
cad = parseInt(cadNode.textContent);
if (!isNaN(cad) && cad > 0) {
totalCadence += cad;
cadenceCount++;
}
}
for (let j = 0; j < allChilds.length; j++) {
const node = allChilds[j];
const name = node.localName.toLowerCase();
if (name === 'hr' || name === 'heartrate') {
const val = parseInt(node.textContent || '0');
if (!isNaN(val) && val > 0) hr = val;
}
if (name === 'cad' || name === 'cadence') {
const val = parseInt(node.textContent || '0');
if (!isNaN(val) && val > 0) cad = val;
}
}
if (hr > 0) {
totalHr += hr;
hrCount++;
if (hr > maxHr) maxHr = hr;
currentSplitHrSum += hr;
currentSplitHrCount++;
}
if (cad > 0) {
totalCadence += cad;
cadenceCount++;
}
let currentTime = 0;
@@ -117,8 +140,11 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
// Elevation Gain
if (lastEle !== null && !isNaN(ele) && ele > lastEle) {
const gain = ele - lastEle;
totalElevationGain += gain;
currentSplitElevGain += gain;
// Ignorujemy mikro-zmiany (szum GPS) poniżej 0.5m
if (gain > 0.2) {
totalElevationGain += gain;
currentSplitElevGain += gain;
}
}
// --- SPLIT LOGIC (Co 1 KM) ---
@@ -133,7 +159,7 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
// Reset Split
splitIndex++;
currentSplitDistance = 0; // or subtract 1.0 specifically for precision, but reset is safer for GPS drift
currentSplitDistance = currentSplitDistance - 1.0;
currentSplitTimeStart = currentTime;
currentSplitHrSum = 0;
currentSplitHrCount = 0;
@@ -144,7 +170,6 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
lastLat = lat;
lastLon = lon;
lastEle = isNaN(ele) ? lastEle : ele;
lastTime = currentTime;
}
// --- PODSUMOWANIE ---
@@ -157,7 +182,7 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
const uiStats: ActivityStats = {
distance: `${totalDistanceKm.toFixed(2)} km`,
duration: msToTime(totalDurationMs),
elevation: `${Math.round(totalElevationGain)}m (Max: ${Math.round(maxElevation)}m)`
elevation: `${Math.round(totalElevationGain)}m (Max: ${Math.round(maxElevation === -Infinity ? 0 : maxElevation)}m)`
};
// --- BUDOWANIE BOGATEGO RAPORTU DLA AI ---
@@ -166,7 +191,7 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
report += `- Dystans Całkowity: ${totalDistanceKm.toFixed(2)} km\n`;
report += `- Czas Całkowity: ${msToTime(totalDurationMs)}\n`;
report += `- Średnie Tempo: ${formatPace(avgPaceMs)} min/km\n`;
report += `- Przewyższenia: +${Math.round(totalElevationGain)}m / Max Wysokość: ${Math.round(maxElevation)}m\n`;
report += `- Przewyższenia: +${Math.round(totalElevationGain)}m / Max Wysokość: ${Math.round(maxElevation === -Infinity ? 0 : maxElevation)}m\n`;
if (avgHr > 0) {
report += `- Tętno: Średnie ${avgHr} bpm / Max ${maxHr} bpm\n`;