Naprawa błędów awatara
This commit is contained in:
10
App.tsx
10
App.tsx
@@ -15,6 +15,10 @@ import { getEnvVar } from './utils/envUtils';
|
|||||||
import { AUTHOR_CONFIG } from './_EDITABLE_CONFIG/author';
|
import { AUTHOR_CONFIG } from './_EDITABLE_CONFIG/author';
|
||||||
import { UI_TEXT } from './_EDITABLE_CONFIG/ui_text';
|
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 STORAGE_KEY = 'gpx-storyteller-state-v6';
|
||||||
const AUTH_KEY = 'promptstory-auth-token';
|
const AUTH_KEY = 'promptstory-auth-token';
|
||||||
|
|
||||||
@@ -131,7 +135,7 @@ const App: React.FC = () => {
|
|||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
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 [logoError, setLogoError] = useState(false);
|
||||||
const [avatarError, setAvatarError] = 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">
|
<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="max-w-4xl mx-auto px-6 py-4 flex justify-between items-center">
|
||||||
<div className="flex items-center space-x-3">
|
<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 ? (
|
{!logoError ? (
|
||||||
<img
|
<img
|
||||||
src="logo.png"
|
src={logoImg}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
onError={() => setLogoError(true)}
|
onError={() => setLogoError(true)}
|
||||||
className="h-10 object-contain"
|
className="h-10 object-contain"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
|
||||||
|
// Import obrazu, aby bundler go dołączył
|
||||||
|
import avatarImg from '../avatar.jpeg';
|
||||||
|
|
||||||
export const AUTHOR_CONFIG = {
|
export const AUTHOR_CONFIG = {
|
||||||
name: "Arkadiusz AreBynd[] Bykowski",
|
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 – 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.",
|
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"],
|
tags: ["Product Design", "AI Automation", "No-Code"],
|
||||||
websiteUrl: "https://bykowski.pro/",
|
websiteUrl: "https://bykowski.pro/",
|
||||||
websiteLabel: "Odwiedź stronę",
|
websiteLabel: "Odwiedź stronę",
|
||||||
// Nazwa pliku w folderze public
|
// Używamy zaimportowanego obrazu
|
||||||
avatarImage: "avatar.jpeg"
|
avatarImage: avatarImg
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// Unused. Logic moved to StepDetails.tsx
|
|
||||||
@@ -16,10 +16,15 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
|
|||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const xmlDoc = parser.parseFromString(text, 'text/xml');
|
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) {
|
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.');
|
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 lastLat: number | null = null;
|
||||||
let lastLon: number | null = null;
|
let lastLon: number | null = null;
|
||||||
let lastEle: 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 ---
|
// --- ITERACJA PO PUNKTACH ---
|
||||||
for (let i = 0; i < trkpts.length; i++) {
|
for (let i = 0; i < trkpts.length; i++) {
|
||||||
const pt = trkpts[i];
|
const pt = trkpts[i];
|
||||||
const lat = parseFloat(pt.getAttribute('lat') || '0');
|
const lat = parseFloat(pt.getAttribute('lat') || '0');
|
||||||
const lon = parseFloat(pt.getAttribute('lon') || '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)
|
const eleStr = getChildText(pt, 'ele');
|
||||||
// Note: Namespaces can vary (ns3:hr, gpxtpx:hr), so we search loosely or by standard tag names inside extensions
|
const ele = eleStr ? parseFloat(eleStr) : NaN;
|
||||||
const extensions = pt.getElementsByTagName('extensions')[0];
|
|
||||||
|
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 hr = 0;
|
||||||
let cad = 0;
|
let cad = 0;
|
||||||
|
|
||||||
if (extensions) {
|
for (let j = 0; j < allChilds.length; j++) {
|
||||||
// Try standard Garmin/GPX schemes
|
const node = allChilds[j];
|
||||||
const hrNode = extensions.getElementsByTagName('gpxtpx:hr')[0] || extensions.getElementsByTagName('ns3:hr')[0] || extensions.getElementsByTagName('hr')[0];
|
const name = node.localName.toLowerCase();
|
||||||
const cadNode = extensions.getElementsByTagName('gpxtpx:cad')[0] || extensions.getElementsByTagName('ns3:cad')[0] || extensions.getElementsByTagName('cad')[0];
|
if (name === 'hr' || name === 'heartrate') {
|
||||||
|
const val = parseInt(node.textContent || '0');
|
||||||
if (hrNode?.textContent) {
|
if (!isNaN(val) && val > 0) hr = val;
|
||||||
hr = parseInt(hrNode.textContent);
|
}
|
||||||
if (!isNaN(hr) && hr > 0) {
|
if (name === 'cad' || name === 'cadence') {
|
||||||
totalHr += hr;
|
const val = parseInt(node.textContent || '0');
|
||||||
hrCount++;
|
if (!isNaN(val) && val > 0) cad = val;
|
||||||
if (hr > maxHr) maxHr = hr;
|
}
|
||||||
currentSplitHrSum += hr;
|
}
|
||||||
currentSplitHrCount++;
|
|
||||||
}
|
if (hr > 0) {
|
||||||
}
|
totalHr += hr;
|
||||||
if (cadNode?.textContent) {
|
hrCount++;
|
||||||
cad = parseInt(cadNode.textContent);
|
if (hr > maxHr) maxHr = hr;
|
||||||
if (!isNaN(cad) && cad > 0) {
|
currentSplitHrSum += hr;
|
||||||
totalCadence += cad;
|
currentSplitHrCount++;
|
||||||
cadenceCount++;
|
}
|
||||||
}
|
if (cad > 0) {
|
||||||
}
|
totalCadence += cad;
|
||||||
|
cadenceCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentTime = 0;
|
let currentTime = 0;
|
||||||
@@ -117,8 +140,11 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
|
|||||||
// Elevation Gain
|
// Elevation Gain
|
||||||
if (lastEle !== null && !isNaN(ele) && ele > lastEle) {
|
if (lastEle !== null && !isNaN(ele) && ele > lastEle) {
|
||||||
const gain = ele - lastEle;
|
const gain = ele - lastEle;
|
||||||
totalElevationGain += gain;
|
// Ignorujemy mikro-zmiany (szum GPS) poniżej 0.5m
|
||||||
currentSplitElevGain += gain;
|
if (gain > 0.2) {
|
||||||
|
totalElevationGain += gain;
|
||||||
|
currentSplitElevGain += gain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SPLIT LOGIC (Co 1 KM) ---
|
// --- SPLIT LOGIC (Co 1 KM) ---
|
||||||
@@ -133,7 +159,7 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
|
|||||||
|
|
||||||
// Reset Split
|
// Reset Split
|
||||||
splitIndex++;
|
splitIndex++;
|
||||||
currentSplitDistance = 0; // or subtract 1.0 specifically for precision, but reset is safer for GPS drift
|
currentSplitDistance = currentSplitDistance - 1.0;
|
||||||
currentSplitTimeStart = currentTime;
|
currentSplitTimeStart = currentTime;
|
||||||
currentSplitHrSum = 0;
|
currentSplitHrSum = 0;
|
||||||
currentSplitHrCount = 0;
|
currentSplitHrCount = 0;
|
||||||
@@ -144,7 +170,6 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
|
|||||||
lastLat = lat;
|
lastLat = lat;
|
||||||
lastLon = lon;
|
lastLon = lon;
|
||||||
lastEle = isNaN(ele) ? lastEle : ele;
|
lastEle = isNaN(ele) ? lastEle : ele;
|
||||||
lastTime = currentTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PODSUMOWANIE ---
|
// --- PODSUMOWANIE ---
|
||||||
@@ -157,7 +182,7 @@ export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
|
|||||||
const uiStats: ActivityStats = {
|
const uiStats: ActivityStats = {
|
||||||
distance: `${totalDistanceKm.toFixed(2)} km`,
|
distance: `${totalDistanceKm.toFixed(2)} km`,
|
||||||
duration: msToTime(totalDurationMs),
|
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 ---
|
// --- 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 += `- Dystans Całkowity: ${totalDistanceKm.toFixed(2)} km\n`;
|
||||||
report += `- Czas Całkowity: ${msToTime(totalDurationMs)}\n`;
|
report += `- Czas Całkowity: ${msToTime(totalDurationMs)}\n`;
|
||||||
report += `- Średnie Tempo: ${formatPace(avgPaceMs)} min/km\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) {
|
if (avgHr > 0) {
|
||||||
report += `- Tętno: Średnie ${avgHr} bpm / Max ${maxHr} bpm\n`;
|
report += `- Tętno: Średnie ${avgHr} bpm / Max ${maxHr} bpm\n`;
|
||||||
|
|||||||
Reference in New Issue
Block a user