commit 27a3c6024fe902b29e59208147660910cdb51aed Author: Arek Bykowski Date: Sun Feb 15 13:22:48 2026 +0100 Pierwszy wrzut promptstory diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..18829a8 Binary files /dev/null and b/.DS_Store differ diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..d423735 --- /dev/null +++ b/App.tsx @@ -0,0 +1,319 @@ + +import React, { useState, useEffect } from 'react'; +import { WizardState, Step, GeneratedContent } from './types'; +import StepContext from './components/StepContext'; +import StepPlatform from './components/StepPlatform'; +import StepEventType from './components/StepEventType'; +import StepToneGoal from './components/StepToneGoal'; +import StepDetails from './components/StepDetails'; +import StepResult from './components/StepResult'; +import { ChevronLeft, ExternalLink, Sparkles, User } from 'lucide-react'; +import { generateStoryContent } from './services/geminiService'; + +const STORAGE_KEY = 'gpx-storyteller-state-v6'; // Incremented version for structure change + +const INITIAL_STATE: WizardState = { + step: Step.CONTEXT, + context: null, + storyStyle: null, + platform: null, + eventType: null, + tone: null, + goal: null, + title: 'Poznań Maraton 2025', + description: 'Byłem totalnie nieprzygotowany. Ostatnie pół roku prawie nie ćwiczyłem, a w dodatku biegłem na antybiotykach z zapaleniem osemki. To była walka a nie przyjemność z biegania. To było głupie. Ale było warto', + files: [], + stats: { + distance: '', + duration: '', + elevation: '', + }, + waypoints: [], + tripData: { + startPoint: { place: '', description: '' }, + endPoint: { place: '', description: '' }, + stops: [], + travelMode: null, + googleMapsKey: '' + } +}; + +const App: React.FC = () => { + const [data, setData] = useState(INITIAL_STATE); + const [generatedContent, setGeneratedContent] = useState(null); + const [isGenerating, setIsGenerating] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + // Image loading states + const [logoError, setLogoError] = useState(false); + const [avatarError, setAvatarError] = useState(false); + + // Persistence Logic + useEffect(() => { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved); + // Reset files because File objects cannot be serialized to local storage + parsed.files = []; + + // Ensure new fields exist if loading from old state + if (!parsed.stats) parsed.stats = { distance: '', duration: '', elevation: '' }; + if (!parsed.waypoints) parsed.waypoints = []; + + // Ensure tripData exists + if (!parsed.tripData) { + parsed.tripData = { ...INITIAL_STATE.tripData }; + } + + // Defaults for new fields if migrating + if (!parsed.tone) parsed.tone = null; + if (!parsed.goal) parsed.goal = null; + if (!parsed.storyStyle) parsed.storyStyle = null; + + setData(parsed); + } catch (e) { + console.error("Failed to load state", e); + } + } + setIsLoaded(true); + }, []); + + useEffect(() => { + if (isLoaded) { + // Exclude files from persistence to avoid quota issues and serialization errors + const stateToSave = { ...data, files: [] }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave)); + } + }, [data, isLoaded]); + + // UPDATED: Now supports functional updates to prevent stale state issues + const updateData = (updates: Partial | ((prev: WizardState) => Partial)) => { + setData(prev => { + const newValues = typeof updates === 'function' ? updates(prev) : updates; + return { ...prev, ...newValues }; + }); + }; + + const nextStep = () => { + if (data.step < Step.RESULT) { + updateData({ step: data.step + 1 }); + } + }; + + const prevStep = () => { + if (data.step > Step.CONTEXT) { + updateData({ step: data.step - 1 }); + } + }; + + const handleGenerate = async () => { + setIsGenerating(true); + setErrorMessage(null); + try { + const content = await generateStoryContent( + data, + process.env.API_KEY || '' + ); + + setGeneratedContent(content); + updateData({ step: Step.RESULT }); + } catch (error: any) { + console.error("Generowanie nie powiodło się:", error); + setErrorMessage("Błąd generowania. Sprawdź poprawność klucza API w pliku .env oraz połączenie z siecią. " + (error.message || '')); + } finally { + setIsGenerating(false); + } + }; + + const handleRegenerate = async (slideCount: number, feedback: string) => { + setIsGenerating(true); + setErrorMessage(null); + try { + const content = await generateStoryContent( + data, + process.env.API_KEY || '', + { slideCount, feedback } + ); + + setGeneratedContent(content); + } catch (error: any) { + console.error("Regenerowanie nie powiodło się:", error); + setErrorMessage("Błąd regenerowania: " + (error.message || '')); + } finally { + setIsGenerating(false); + } + }; + + const resetApp = () => { + setData({ ...INITIAL_STATE }); + setGeneratedContent(null); + localStorage.removeItem(STORAGE_KEY); + // Force reload to clear any hung states + window.location.reload(); + }; + + if (!isLoaded) return null; + + // Step Labels for Progress Bar (Order Changed) + const stepsLabels = ['Kontekst', 'Typ', 'Platforma', 'Vibe & Cel', 'Szczegóły']; + + return ( +
+ {/* Header */} +
+
+
+ {!logoError ? ( + setLogoError(true)} + alt="PromptStory Logo" + className="w-10 h-10 object-contain" + /> + ) : ( +
+ +
+ )} +

PromptStory

+
+ +
+
+ +
+ + {/* Progress Bar */} + {data.step !== Step.RESULT && ( +
+
+ {stepsLabels.map((label, idx) => ( + = idx ? 'text-[#EA4420]' : 'text-gray-300'}`}> + 0{idx + 1}. {label} + + ))} +
+
+
+
+
+ )} + + {/* Dynamic Content Container */} +
+ {/* STEP 1: CONTEXT */} + {data.step === Step.CONTEXT && } + + {/* STEP 2: EVENT TYPE (Moved UP) */} + {data.step === Step.EVENT_TYPE && } + + {/* STEP 3: PLATFORM (Moved DOWN) */} + {data.step === Step.PLATFORM && } + + {/* STEP 4: TONE & GOAL (NEW) */} + {data.step === Step.TONE_GOAL && } + + {/* STEP 5: DETAILS */} + {data.step === Step.DETAILS && ( + + )} + + {/* STEP 6: RESULT */} + {data.step === Step.RESULT && generatedContent && ( + + )} + + {/* Error Message */} + {errorMessage && ( +
+ {errorMessage} +
+ )} +
+ + {/* Navigation Controls (Back Only) */} + {data.step > Step.CONTEXT && data.step < Step.RESULT && ( +
+
+ +
+
+ )} +
+ + {/* Footer / Author Info */} +
+
+
+ {/* Avatar */} +
+
+ {!avatarError ? ( + setAvatarError(true)} + alt="Arkadiusz Bykowski" + className="relative w-20 h-20 rounded-full object-cover border border-gray-100 bg-white" + /> + ) : ( +
+ +
+ )} +
+ + {/* Text Info */} +
+

Arkadiusz Bykowski

+

+ 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. +

+
+ Product Design + AI Automation + No-Code +
+
+
+ + {/* Button */} + + Odwiedź stronę + + +
+
+
+ ); +}; + +export default App; \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..addf3a8 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1Fk1GjH6OOteqHMS-IChAeMvr785qR6xS + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/avatar.jpeg b/avatar.jpeg new file mode 100644 index 0000000..ce022a7 Binary files /dev/null and b/avatar.jpeg differ diff --git a/components/StepContext.tsx b/components/StepContext.tsx new file mode 100644 index 0000000..3cef256 --- /dev/null +++ b/components/StepContext.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { WizardState, Step } from '../types'; +import { Camera, BookOpen, Ghost, Sword } from 'lucide-react'; + +interface StepContextProps { + data: WizardState; + updateData: (updates: Partial) => void; + nextStep: () => void; +} + +const StepContext: React.FC = ({ data, updateData, nextStep }) => { + + const handleContextSelect = (context: WizardState['context']) => { + // If selecting Relacja, clear storyStyle and move on + if (context === 'relacja') { + updateData({ context, storyStyle: null }); + setTimeout(nextStep, 150); + } else { + // If selecting Opowiesc, just update context and stay for sub-step + updateData({ context, storyStyle: null }); // Reset style if switching back to opowiesc + } + }; + + const handleStyleSelect = (storyStyle: WizardState['storyStyle']) => { + updateData({ storyStyle }); + setTimeout(nextStep, 150); + }; + + return ( +
+
+

Wybierz Kontekst

+

Jaki rodzaj historii chcesz opowiedzieć?

+ + {/* Main Context Selection */} +
+ + + +
+ + {/* Sub-step for Opowiesc: Story Style */} + {data.context === 'opowiesc' && ( +
+

Wybierz Styl Opowieści

+

Nadaj historii unikalny klimat.

+ +
+ + + +
+
+ )} + +
+
+ ); +}; + +export default StepContext; \ No newline at end of file diff --git a/components/StepData.tsx b/components/StepData.tsx new file mode 100644 index 0000000..b59dbf9 --- /dev/null +++ b/components/StepData.tsx @@ -0,0 +1,128 @@ +import React, { useRef, useState } from 'react'; +import { WizardState } from '../types'; +import { UploadCloud, FileJson, AlertCircle } from 'lucide-react'; +import { parseGpxFile } from '../utils/gpxUtils'; + +interface StepDataProps { + data: WizardState; + updateData: (updates: Partial) => void; +} + +const StepData: React.FC = ({ data, updateData }) => { + const fileInputRef = useRef(null); + const [error, setError] = useState(null); + const [isParsing, setIsParsing] = useState(false); + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.name.toLowerCase().endsWith('.gpx')) { + setError('Proszę wybrać plik .gpx'); + return; + } + + setError(null); + setIsParsing(true); + + try { + const stats = await parseGpxFile(file); + updateData({ stats }); + } catch (err) { + console.error(err); + setError('Błąd parsowania pliku GPX. Spróbuj innego pliku lub wpisz dane ręcznie.'); + } finally { + setIsParsing(false); + } + }; + + const handleStatsChange = (key: keyof typeof data.stats, value: string) => { + updateData({ + stats: { + ...data.stats, + [key]: value + } + }); + }; + + return ( +
+
+

Dane Aktywności

+

Wgraj plik GPX lub wpisz dane ręcznie.

+
+ + {/* Upload Zone */} +
fileInputRef.current?.click()} + > + + + {isParsing ? ( +
+ +

Analizowanie pliku...

+
+ ) : ( + <> + +

Kliknij, aby wgrać plik GPX

+

lub przeciągnij i upuść tutaj

+ + )} +
+ + {error && ( +
+ + {error} +
+ )} + + {/* Manual Override Inputs */} +
+
+ + handleStatsChange('distance', e.target.value)} + className="w-full p-4 border border-gray-200 rounded-md focus:ring-1 focus:ring-[#EA4420] focus:border-[#EA4420] outline-none transition-all font-medium text-gray-900 placeholder-gray-300" + placeholder="np. 12.5 km" + /> +
+
+ + handleStatsChange('duration', e.target.value)} + className="w-full p-4 border border-gray-200 rounded-md focus:ring-1 focus:ring-[#EA4420] focus:border-[#EA4420] outline-none transition-all font-medium text-gray-900 placeholder-gray-300" + placeholder="np. 1h 45m" + /> +
+
+ + handleStatsChange('elevation', e.target.value)} + className="w-full p-4 border border-gray-200 rounded-md focus:ring-1 focus:ring-[#EA4420] focus:border-[#EA4420] outline-none transition-all font-medium text-gray-900 placeholder-gray-300" + placeholder="np. 350m" + /> +
+
+
+ ); +}; + +export default StepData; \ No newline at end of file diff --git a/components/StepDetails.tsx b/components/StepDetails.tsx new file mode 100644 index 0000000..bc38289 --- /dev/null +++ b/components/StepDetails.tsx @@ -0,0 +1,581 @@ + +import React, { useRef, useState, useEffect } from 'react'; +import { WizardState } from '../types'; +import { UploadCloud, FileText, X, Image as ImageIcon, Sparkles, Loader2, MapPin, Navigation, Plus, Trash2, Flag, Target, AlertCircle, CheckCircle2, Car, Footprints } from 'lucide-react'; +import { processFile } from '../utils/fileUtils'; + +// --- HELPER COMPONENT: PLACE AUTOCOMPLETE INPUT (WIDGET VERSION) --- +interface PlaceAutocompleteInputProps { + value: string; + onChange: (val: string, preview?: string) => void; + placeholder: string; + icon: React.ReactNode; + scriptLoaded: boolean; + disabled?: boolean; + onError?: (msg: string) => void; + addressPreview?: string; +} + +const PlaceAutocompleteInput: React.FC = ({ value, onChange, placeholder, icon, scriptLoaded, disabled, onError, addressPreview }) => { + const inputRef = useRef(null); + const autocompleteRef = useRef(null); + + // Initialize Google Autocomplete Widget + useEffect(() => { + if (!scriptLoaded || !inputRef.current || !(window as any).google || autocompleteRef.current) return; + + try { + const google = (window as any).google; + + // Use the standard Autocomplete widget attached to the input + const autocomplete = new google.maps.places.Autocomplete(inputRef.current, { + fields: ["place_id", "geometry", "name", "formatted_address"], + types: ["geocode", "establishment"] + }); + + autocompleteRef.current = autocomplete; + + autocomplete.addListener("place_changed", () => { + const place = autocomplete.getPlace(); + + if (!place.geometry) { + return; + } + + // FIX: Use formatted_address as fallback if name is empty/missing + const name = place.name || place.formatted_address || ""; + const address = place.formatted_address; + + // Update parent state + onChange(name, address); + }); + } catch (e) { + console.error("Autocomplete init error", e); + if(onError) onError("Błąd inicjalizacji widgetu Google Maps."); + } + }, [scriptLoaded, onError, onChange]); + + return ( +
+
+ {icon} +
+ onChange(e.target.value)} + disabled={disabled} + className="w-full pl-9 p-3 border border-gray-300 rounded-md focus:border-[#EA4420] outline-none font-medium disabled:bg-gray-100 disabled:text-gray-400 transition-colors" + placeholder={placeholder} + autoComplete="off" + /> + + {/* Address Confirmation Hint */} + {addressPreview && ( +
+ + {addressPreview} +
+ )} +
+ ); +}; + + +// --- MAIN COMPONENT --- +interface StepDetailsProps { + data: WizardState; + // Update Type Definition to allow functional updates + updateData: (updates: Partial | ((prev: WizardState) => Partial)) => void; + onGenerate: () => void; + isGenerating: boolean; +} + +const StepDetails: React.FC = ({ data, updateData, onGenerate, isGenerating }) => { + const fileInputRef = useRef(null); + const [error, setError] = useState(null); + + // Specific Error State + const [mapError, setMapError] = useState<{title: string, msg: string} | null>(null); + const [scriptLoaded, setScriptLoaded] = useState(false); + + // --- HARDCODED FALLBACK KEY --- + const AUTO_PASTE_KEY = 'AIzaSyAq9IgZswt5j7GGfH2s-ESenHmfvWFCFCg'; + + const getEffectiveKey = () => { + if (data.tripData?.googleMapsKey) return data.tripData.googleMapsKey; + // @ts-ignore + if (import.meta.env && import.meta.env.VITE_GOOGLE_MAPS_KEY) return import.meta.env.VITE_GOOGLE_MAPS_KEY; + if (process.env.GOOGLE_MAPS_KEY) return process.env.GOOGLE_MAPS_KEY; + return AUTO_PASTE_KEY; + }; + + const effectiveKey = getEffectiveKey(); + const isEnvKeyMissing = !process.env.GOOGLE_MAPS_KEY && + // @ts-ignore + !import.meta.env?.VITE_GOOGLE_MAPS_KEY && + data.tripData?.googleMapsKey !== AUTO_PASTE_KEY; + + // --- GOOGLE MAPS LOADING --- + const loadMapsScript = (apiKey: string) => { + if (!apiKey) { + setMapError({ + title: "Brak klucza API", + msg: "System nie mógł znaleźć klucza. Skontaktuj się z administratorem." + }); + return; + } + + if ((window as any).google?.maps?.places) { + setScriptLoaded(true); + return; + } + + const existingScript = document.querySelector(`script[src*="maps.googleapis.com/maps/api/js"]`); + if (existingScript) { + const interval = setInterval(() => { + if ((window as any).google?.maps?.places) { + setScriptLoaded(true); + setMapError(null); + clearInterval(interval); + } + }, 500); + return; + } + + const script = document.createElement('script'); + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&loading=async&v=weekly`; + script.async = true; + script.onload = () => { + setTimeout(() => { + if ((window as any).google?.maps?.places) { + setScriptLoaded(true); + setMapError(null); + } + }, 200); + }; + script.onerror = () => { + setMapError({ title: "Błąd sieci", msg: "Nie udało się pobrać skryptu Google Maps." }); + }; + document.head.appendChild(script); + }; + + useEffect(() => { + if (data.eventType === 'trip') { + (window as any).gm_authFailure = () => { + setMapError({ title: "Klucz odrzucony przez Google", msg: "Podany klucz jest niepoprawny." }); + setScriptLoaded(false); + }; + if (effectiveKey) loadMapsScript(effectiveKey); + } + }, [data.eventType, effectiveKey]); + + // Initialize Trip Data if missing + useEffect(() => { + if (data.eventType === 'trip') { + if (!data.tripData) { + updateData({ + tripData: { + startPoint: { place: '', description: '' }, + endPoint: { place: '', description: '' }, + stops: [{ id: crypto.randomUUID(), place: '', description: '' }], + travelMode: null, + googleMapsKey: AUTO_PASTE_KEY + } + }); + } + else if (!data.tripData.googleMapsKey) { + updateData(prev => ({ + tripData: { ...prev.tripData!, googleMapsKey: AUTO_PASTE_KEY } + })); + } + } + }, [data.eventType, updateData]); // Removed data.tripData dependency to avoid loops, handled by logic + + const handleFileChange = async (e: React.ChangeEvent) => { + const newFiles = Array.from(e.target.files || []); + if (newFiles.length === 0) return; + if (data.files.length + newFiles.length > 3) { + setError('Maksymalnie 3 pliki.'); + return; + } + setError(null); + const processedFiles = await Promise.all(newFiles.map(processFile)); + updateData(prev => ({ files: [...prev.files, ...processedFiles] })); + }; + + const removeFile = (id: string) => { + updateData(prev => ({ files: prev.files.filter(f => f.id !== id) })); + }; + + // --- TRIP DATA HELPERS (UPDATED TO USE FUNCTIONAL STATE UPDATES) --- + const updateApiKey = (val: string) => { + updateData(prev => ({ + tripData: prev.tripData ? { ...prev.tripData, googleMapsKey: val } : prev.tripData + })); + if (val.length > 10) setMapError(null); + }; + + const updatePoint = (pointType: 'startPoint' | 'endPoint', field: 'place' | 'description' | 'addressPreview', value: string) => { + updateData(prev => { + if (!prev.tripData) return {}; + return { + tripData: { + ...prev.tripData, + [pointType]: { + ...prev.tripData[pointType], + [field]: value + } + } + }; + }); + }; + + const updateStop = (id: string, field: 'place' | 'description' | 'addressPreview', value: string) => { + updateData(prev => { + if (!prev.tripData) return {}; + const newStops = prev.tripData.stops.map(s => s.id === id ? { ...s, [field]: value } : s); + return { + tripData: { ...prev.tripData, stops: newStops } + }; + }); + }; + + const addStop = () => { + updateData(prev => { + if (!prev.tripData) return {}; + return { + tripData: { + ...prev.tripData, + stops: [...prev.tripData.stops, { id: crypto.randomUUID(), place: '', description: '' }] + } + }; + }); + }; + + const removeStop = (id: string) => { + updateData(prev => { + if (!prev.tripData) return {}; + return { + tripData: { + ...prev.tripData, + stops: prev.tripData.stops.filter(s => s.id !== id) + } + }; + }); + }; + + const setTravelMode = (mode: 'DRIVING' | 'WALKING') => { + updateData(prev => { + if (!prev.tripData) return {}; + return { + tripData: { ...prev.tripData, travelMode: mode } + }; + }); + }; + + // Validation Check + const isTripModeValid = data.eventType !== 'trip' || (data.tripData && data.tripData.travelMode !== null); + const isReadyToGenerate = data.title && isTripModeValid; + + return ( +
+
+

Szczegóły

+

+ {data.eventType === 'trip' ? 'Zaplanuj trasę i opisz przebieg podróży.' : 'Uzupełnij informacje o wydarzeniu.'} +

+
+ +
+ + {/* SEKCJA DLA WYCIECZEK (TRIP) */} + {data.eventType === 'trip' && data.tripData && ( +
+ + {/* Fallback Input */} + {(!data.tripData.googleMapsKey && isEnvKeyMissing) && ( +
+
+ +
+

Nie wykryto klucza w .env

+

+ System automatycznie wklei klucz zapasowy. Jeśli to nie nastąpiło, wklej go poniżej. +

+ updateApiKey(e.target.value)} + placeholder="Wklej klucz Google Maps API (AIza...)" + className="w-full p-2 text-sm border border-yellow-300 rounded bg-white focus:border-[#EA4420] outline-none" + /> +
+
+
+ )} + + {/* Detailed Error Banner for Maps */} + {mapError && ( +
+ +
+

{mapError.title}

+

{mapError.msg}

+
+
+ )} + +
+
+ +

Plan Podróży

+ {scriptLoaded && !mapError && ( + + API OK + + )} +
+
+ + {/* BIG TRAVEL MODE SELECTOR */} +
+ + +
+ + {/* Validation Message if missing */} + {!data.tripData.travelMode && ( +

+ * Wybór rodzaju trasy jest wymagany +

+ )} + +
+ + {/* START POINT */} +
+
+
+ { + updatePoint('startPoint', 'place', val); + if(preview) updatePoint('startPoint', 'addressPreview', preview); + }} + addressPreview={data.tripData.startPoint.addressPreview} + placeholder="Punkt Startowy (np. Kraków)" + icon={} + scriptLoaded={scriptLoaded} + onError={(msg) => setMapError({title: "Błąd API Places", msg})} + /> +
+ updatePoint('startPoint', 'description', e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:border-[#EA4420] outline-none" + placeholder="Opis startu (np. Zbiórka o 6:00)" + /> +
+ {/* Placeholder for alignment */} +
+
+ + {/* STOPS */} + {data.tripData.stops.map((stop, index) => ( +
+
+
+ { + updateStop(stop.id, 'place', val); + if(preview) updateStop(stop.id, 'addressPreview', preview); + }} + addressPreview={stop.addressPreview} + placeholder={`Przystanek ${index + 1}`} + icon={} + scriptLoaded={scriptLoaded} + onError={(msg) => setMapError({title: "Błąd API Places", msg})} + /> +
+ updateStop(stop.id, 'description', e.target.value)} + className="w-full p-3 border border-gray-200 rounded-md focus:border-[#EA4420] outline-none" + placeholder="Co tam robiliście?" + /> +
+ +
+ ))} + +
+ +
+ + {/* END POINT */} +
+
+
+ { + updatePoint('endPoint', 'place', val); + if(preview) updatePoint('endPoint', 'addressPreview', preview); + }} + addressPreview={data.tripData.endPoint.addressPreview} + placeholder="Punkt Końcowy (np. Zakopane)" + icon={} + scriptLoaded={scriptLoaded} + onError={(msg) => setMapError({title: "Błąd API Places", msg})} + /> +
+ updatePoint('endPoint', 'description', e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:border-[#EA4420] outline-none" + placeholder="Opis końca (np. Nareszcie piwo)" + /> +
+ {/* Placeholder for alignment */} +
+
+ +
+
+ )} + + {/* STANDARDOWE POLA */} +
+
+ + updateData({ title: e.target.value })} + className="w-full p-4 border border-gray-200 rounded-md focus:ring-1 focus:ring-[#EA4420] focus:border-[#EA4420] outline-none transition-all font-medium text-gray-900 placeholder-gray-300" + placeholder="np. Roadtrip po Bałkanach" + /> +
+ +
+ +