Archaeo commited on
Commit
752d7a6
·
verified ·
1 Parent(s): 4d70b3a

Upload 18 files

Browse files
cologne-christmas-markets-map-2025_5/.env.local ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY=PLACEHOLDER_API_KEY
cologne-christmas-markets-map-2025_5/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
cologne-christmas-markets-map-2025_5/App.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback } from 'react';
2
+ import MapComponent from './components/MapComponent';
3
+ import MarketList from './components/MarketList';
4
+ import { MARKETS } from './constants';
5
+ import type { Market } from './types';
6
+
7
+ const Snowfall: React.FC = () => (
8
+ <div className="absolute top-0 left-0 w-full h-full pointer-events-none" aria-hidden="true">
9
+ {[...Array(12)].map((_, i) => (
10
+ <div key={i} className="snowflake">❅</div>
11
+ ))}
12
+ </div>
13
+ );
14
+
15
+ const App: React.FC = () => {
16
+ const [selectedMarket, setSelectedMarket] = useState<Market | null>(null);
17
+ const [hoveredMarketId, setHoveredMarketId] = useState<number | null>(null);
18
+ const [mapCenter, setMapCenter] = useState<[number, number]>([50.9375, 6.9603]);
19
+ const [mapZoom, setMapZoom] = useState<number>(13);
20
+
21
+ const handleSelectMarket = useCallback((market: Market | null) => {
22
+ setSelectedMarket(market);
23
+ if (market) {
24
+ setMapCenter([market.lat, market.lon]);
25
+ setMapZoom(15);
26
+ }
27
+ }, []);
28
+
29
+ const handleMarketHover = useCallback((marketId: number | null) => {
30
+ setHoveredMarketId(marketId);
31
+ }, []);
32
+
33
+ return (
34
+ <div className="flex flex-col md:flex-row h-screen bg-stone-900 text-stone-200 relative">
35
+ <Snowfall />
36
+ <MarketList
37
+ markets={MARKETS}
38
+ selectedMarket={selectedMarket}
39
+ hoveredMarketId={hoveredMarketId}
40
+ onSelectMarket={handleSelectMarket}
41
+ onMarketHover={handleMarketHover}
42
+ />
43
+ <div className="flex-grow h-full w-full relative">
44
+ <MapComponent
45
+ markets={MARKETS}
46
+ selectedMarket={selectedMarket}
47
+ hoveredMarketId={hoveredMarketId}
48
+ onSelectMarket={handleSelectMarket}
49
+ onMarketHover={handleMarketHover}
50
+ mapCenter={mapCenter}
51
+ mapZoom={mapZoom}
52
+ />
53
+ </div>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default App;
cologne-christmas-markets-map-2025_5/README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3
+ </div>
4
+
5
+ # Run and deploy your AI Studio app
6
+
7
+ This contains everything you need to run your app locally.
8
+
9
+ View your app in AI Studio: https://ai.studio/apps/drive/1oqoCQFp46GFqSBo-A5v8rRnvagnv70Wc
10
+
11
+ ## Run Locally
12
+
13
+ **Prerequisites:** Node.js
14
+
15
+
16
+ 1. Install dependencies:
17
+ `npm install`
18
+ 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19
+ 3. Run the app:
20
+ `npm run dev`
cologne-christmas-markets-map-2025_5/components/ImageCarousel.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+
3
+ interface ImageCarouselProps {
4
+ images: string[];
5
+ marketName: string;
6
+ }
7
+
8
+ const ImageCarousel: React.FC<ImageCarouselProps> = ({ images, marketName }) => {
9
+ const [currentIndex, setCurrentIndex] = useState(0);
10
+
11
+ const goToNext = useCallback(() => {
12
+ setCurrentIndex((prevIndex) => (prevIndex === images.length - 1 ? 0 : prevIndex + 1));
13
+ }, [images.length]);
14
+
15
+ const goToPrevious = () => {
16
+ setCurrentIndex((prevIndex) => (prevIndex === 0 ? images.length - 1 : prevIndex - 1));
17
+ };
18
+
19
+ const goToSlide = (slideIndex: number) => {
20
+ setCurrentIndex(slideIndex);
21
+ };
22
+
23
+ useEffect(() => {
24
+ if (images.length > 1) {
25
+ const timer = setTimeout(goToNext, 4000); // Autoplay every 4 seconds
26
+ return () => clearTimeout(timer);
27
+ }
28
+ }, [currentIndex, images.length, goToNext]);
29
+
30
+ if (!images || images.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ return (
35
+ <div className="relative w-full h-32 group">
36
+ <div className="w-full h-full overflow-hidden rounded-t-lg">
37
+ {images.map((image, index) => (
38
+ <img
39
+ key={index}
40
+ src={image}
41
+ alt={`${marketName} - image ${index + 1}`}
42
+ className={`absolute top-0 left-0 w-full h-full object-cover transition-opacity duration-700 ease-in-out ${index === currentIndex ? 'opacity-100' : 'opacity-0'}`}
43
+ />
44
+ ))}
45
+ </div>
46
+
47
+ {images.length > 1 && (
48
+ <>
49
+ <button
50
+ onClick={goToPrevious}
51
+ className="absolute top-1/2 left-1 transform -translate-y-1/2 bg-black/40 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
52
+ aria-label="Previous image"
53
+ >
54
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
55
+ </button>
56
+ <button
57
+ onClick={goToNext}
58
+ className="absolute top-1/2 right-1 transform -translate-y-1/2 bg-black/40 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
59
+ aria-label="Next image"
60
+ >
61
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
62
+ </button>
63
+ <div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex space-x-2">
64
+ {images.map((_, slideIndex) => (
65
+ <button
66
+ key={slideIndex}
67
+ onClick={() => goToSlide(slideIndex)}
68
+ className={`w-2 h-2 rounded-full transition-colors ${currentIndex === slideIndex ? 'bg-white' : 'bg-white/50'}`}
69
+ aria-label={`Go to slide ${slideIndex + 1}`}
70
+ ></button>
71
+ ))}
72
+ </div>
73
+ </>
74
+ )}
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default ImageCarousel;
cologne-christmas-markets-map-2025_5/components/MapComponent.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
3
+ import L from 'leaflet';
4
+ import type { Market } from '../types';
5
+ import ImageCarousel from './ImageCarousel';
6
+
7
+ interface MapComponentProps {
8
+ markets: Market[];
9
+ selectedMarket: Market | null;
10
+ hoveredMarketId: number | null;
11
+ onSelectMarket: (market: Market) => void;
12
+ onMarketHover: (marketId: number | null) => void;
13
+ mapCenter: [number, number];
14
+ mapZoom: number;
15
+ }
16
+
17
+ const categoryIcons: Record<Market['category'], string> = {
18
+ Traditional: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="{fill}" width="{width}" height="{height}" style="filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.5));"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>`,
19
+ Themed: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="{fill}" width="{width}" height="{height}" style="filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.5));"><path d="M22 11h-2V8.34l2-1.73-1-1.73-2 1.73V4h-2v2.66l-1.45-1.25-1 1.73L14.34 8H12v3.66l-1.55-1.34-1 1.73L11.66 12H9.34l2.21-2.55-1-1.73L9 9.34V7H7v2.34l-1.45-1.25-1 1.73L6.34 11H4v2h2.34l-1.73 2 1 1.73 1.73-2V17h2v-2.34l1.45 1.25 1-1.73L9.66 13h2.68l-2.21 2.55 1 1.73L12 15.66V18h2v-2.34l1.45 1.25 1-1.73L14.34 14h2.32l-1.73 2 1 1.73 2-1.73V13h2z"/></svg>`,
20
+ Community: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="{fill}" width="{width}" height="{height}" style="filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.5));"><path d="M12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81l5-4.5M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z"/></svg>`,
21
+ 'Short-Term': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="{fill}" width="{width}" height="{height}" style="filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.5));"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm-5-7h-4v4h4v-4z"/></svg>`,
22
+ };
23
+
24
+ const createIcon = (category: Market['category'], color: string, size: number, className: string = '') => {
25
+ const svgIcon = categoryIcons[category]
26
+ .replace('{fill}', color)
27
+ .replace('{width}', `${size}px`)
28
+ .replace('{height}', `${size}px`);
29
+
30
+ return L.divIcon({
31
+ html: `<div class="${className}">${svgIcon}</div>`,
32
+ className: 'bg-transparent border-0',
33
+ iconSize: [size, size],
34
+ iconAnchor: [size / 2, size / 2],
35
+ });
36
+ };
37
+
38
+ const userLocationIcon = L.divIcon({
39
+ className: 'user-location-pulse',
40
+ iconSize: [20, 20],
41
+ });
42
+
43
+ const ChangeView: React.FC<{ center: [number, number]; zoom: number }> = ({ center, zoom }) => {
44
+ const map = useMap();
45
+ useEffect(() => {
46
+ map.flyTo(center, zoom);
47
+ }, [center, zoom, map]);
48
+ return null;
49
+ };
50
+
51
+ const LocateControl: React.FC = () => {
52
+ const map = useMap();
53
+ const [position, setPosition] = useState<L.LatLng | null>(null);
54
+
55
+ const handleLocate = () => {
56
+ map.locate().on('locationfound', (e) => {
57
+ setPosition(e.latlng);
58
+ map.flyTo(e.latlng, 14);
59
+ }).on('locationerror', () => {
60
+ alert('Could not access your location. Please enable location services in your browser.');
61
+ });
62
+ };
63
+
64
+ return (
65
+ <>
66
+ <div className="absolute top-4 left-4 z-[1000]">
67
+ <button onClick={handleLocate} className="locate-me-btn" aria-label="Find my location">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 8V2"/><path d="M12 16v6"/><path d="M22 12h-6"/><path d="M8 12H2"/><path d="M12 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/></svg>
69
+ </button>
70
+ </div>
71
+ {position && <Marker position={position} icon={userLocationIcon} />}
72
+ </>
73
+ );
74
+ };
75
+
76
+ const MapComponent: React.FC<MapComponentProps> = ({ markets, selectedMarket, hoveredMarketId, onSelectMarket, onMarketHover, mapCenter, mapZoom }) => {
77
+ const markerRefs = useRef<{ [key: number]: L.Marker | null }>({});
78
+
79
+ useEffect(() => {
80
+ if (selectedMarket && markerRefs.current[selectedMarket.id]) {
81
+ setTimeout(() => {
82
+ markerRefs.current[selectedMarket.id]?.openPopup();
83
+ }, 300);
84
+ }
85
+ }, [selectedMarket]);
86
+
87
+ return (
88
+ <MapContainer center={mapCenter} zoom={mapZoom} scrollWheelZoom={true} className="h-full w-full">
89
+ <ChangeView center={mapCenter} zoom={mapZoom} />
90
+ <LocateControl />
91
+ <TileLayer
92
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
93
+ url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
94
+ />
95
+ {markets.map((market) => {
96
+ const isSelected = selectedMarket?.id === market.id;
97
+ const isHovered = hoveredMarketId === market.id;
98
+ let icon;
99
+ if (isSelected) {
100
+ icon = createIcon(market.category, '#facc15', 50, 'selected-marker-icon');
101
+ } else if (isHovered) {
102
+ icon = createIcon(market.category, '#f43f5e', 44, 'hovered-marker-icon');
103
+ } else {
104
+ icon = createIcon(market.category, '#e11d48', 38);
105
+ }
106
+
107
+ return (
108
+ <Marker
109
+ key={market.id}
110
+ position={[market.lat, market.lon]}
111
+ icon={icon}
112
+ eventHandlers={{
113
+ click: () => onSelectMarket(market),
114
+ mouseover: () => onMarketHover(market.id),
115
+ mouseout: () => onMarketHover(null),
116
+ }}
117
+ ref={(el) => (markerRefs.current[market.id] = el)}
118
+ >
119
+ <Popup>
120
+ <div className="w-full text-stone-200 font-sans">
121
+ <ImageCarousel images={market.imageUrls} marketName={market.name} />
122
+ <div className="p-4">
123
+ <h3 className="text-xl font-bold mb-2 text-amber-400" style={{ fontFamily: "'Cinzel', serif" }}>
124
+ {market.name}
125
+ </h3>
126
+ <p className="text-sm text-stone-300 mb-4 text-left">{market.description}</p>
127
+ <div className="border-t border-stone-600 pt-3 text-left">
128
+ <h4 className="font-semibold text-stone-100 text-base mb-1">Öffnungszeiten</h4>
129
+ <p className="text-stone-300 text-sm">{market.openingHours}</p>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </Popup>
134
+ </Marker>
135
+ );
136
+ })}
137
+ </MapContainer>
138
+ );
139
+ };
140
+
141
+ export default MapComponent;
cologne-christmas-markets-map-2025_5/components/MarketDetailModal.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import type { Market } from '../types';
3
+
4
+ interface MarketDetailModalProps {
5
+ market: Market;
6
+ onClose: () => void;
7
+ }
8
+
9
+ const MarketDetailModal: React.FC<MarketDetailModalProps> = ({ market, onClose }) => {
10
+ if (!market) return null;
11
+
12
+ return (
13
+ <div
14
+ className="modal-backdrop fixed inset-0 z-[2000] flex items-center justify-center bg-black/70"
15
+ onClick={onClose}
16
+ >
17
+ <div
18
+ className="modal-content bg-stone-800 rounded-lg shadow-2xl w-11/12 md:w-2/3 lg:w-1/2 max-w-4xl max-h-[90vh] flex flex-col border-2 border-stone-700 overflow-hidden"
19
+ onClick={(e) => e.stopPropagation()} // Prevent clicks inside the modal from closing it
20
+ >
21
+ <div className="relative">
22
+ <img src={market.imageUrls[0]} alt={market.name} className="w-full h-48 md:h-64 object-cover" />
23
+ <div className="absolute inset-0 bg-gradient-to-t from-stone-800 to-transparent"></div>
24
+ <button
25
+ onClick={onClose}
26
+ className="absolute top-2 right-2 text-white bg-black/50 rounded-full p-2 hover:bg-black/80 transition-colors"
27
+ aria-label="Close"
28
+ >
29
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
30
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
31
+ </svg>
32
+ </button>
33
+ </div>
34
+
35
+ <div className="p-6 md:p-8 overflow-y-auto">
36
+ <h2 className="text-3xl font-bold text-amber-400 mb-4">{market.name}</h2>
37
+ <p className="text-stone-300 leading-relaxed mb-6">{market.description}</p>
38
+
39
+ <div className="border-t border-stone-700 pt-4 space-y-3 text-stone-300">
40
+ <div className="flex items-start">
41
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mr-3 text-amber-400 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z"/></svg>
42
+ <div>
43
+ <h4 className="font-semibold text-stone-100">Zeitraum</h4>
44
+ <p>{market.start} - {market.end}</p>
45
+ </div>
46
+ </div>
47
+ <div className="flex items-start">
48
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mr-3 text-amber-400 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
49
+ <div>
50
+ <h4 className="font-semibold text-stone-100">Öffnungszeiten</h4>
51
+ <p>{market.openingHours}</p>
52
+ </div>
53
+ </div>
54
+ <div className="flex items-start">
55
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mr-3 text-amber-400 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 0 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
56
+ <div>
57
+ <h4 className="font-semibold text-stone-100">Ort</h4>
58
+ <p>{market.ort}</p>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ {market.website && (
63
+ <div className="mt-8 text-center">
64
+ <a
65
+ href={market.website}
66
+ target="_blank"
67
+ rel="noopener noreferrer"
68
+ className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-stone-900 bg-amber-400 hover:bg-amber-500 transition-colors shadow-lg hover:shadow-amber-400/30"
69
+ >
70
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
71
+ Zur Webseite
72
+ </a>
73
+ </div>
74
+ )}
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default MarketDetailModal;
cologne-christmas-markets-map-2025_5/components/MarketList.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import type { Market } from '../types';
3
+ import MarketListItem from './MarketListItem';
4
+ import WeatherWidget from './WeatherWidget';
5
+
6
+ interface MarketListProps {
7
+ markets: Market[];
8
+ selectedMarket: Market | null;
9
+ hoveredMarketId: number | null;
10
+ onSelectMarket: (market: Market | null) => void;
11
+ onMarketHover: (marketId: number | null) => void;
12
+ }
13
+
14
+ const MarketList: React.FC<MarketListProps> = ({ markets, selectedMarket, hoveredMarketId, onSelectMarket, onMarketHover }) => {
15
+ return (
16
+ <aside className="w-full md:w-96 lg:w-1/3 xl:w-1/4 bg-stone-800 shadow-lg flex flex-col h-1/3 md:h-full border-r-2 border-black/30">
17
+ <header className="p-4 bg-black/20 border-b border-black/30 shadow-md">
18
+ <h1 className="text-2xl font-bold text-stone-100 flex items-center">
19
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 mr-3 text-amber-400" viewBox="0 0 24 24" fill="currentColor">
20
+ <path d="M12 2.5l2.42 5.38 5.58.81-4.04 3.93.95 5.56L12 15.4l-5.91 3.78.95-5.56-4.04-3.93 5.58-.81L12 2.5z M12 5.8l-1.61 3.58-3.96.58 2.86 2.79-.68 3.94L12 14.5l3.35 2.19-.68-3.94 2.86-2.79-3.96-.58L12 5.8z" />
21
+ </svg>
22
+ Kölner Weihnachtsmärkte 2025
23
+ </h1>
24
+ <p className="text-base text-stone-400 mt-2">Wählen Sie einen Markt aus, um ihn auf der Karte zu sehen.</p>
25
+ </header>
26
+ <div className="overflow-y-auto flex-grow">
27
+ {markets.map((market) => (
28
+ <MarketListItem
29
+ key={market.id}
30
+ market={market}
31
+ isSelected={selectedMarket?.id === market.id}
32
+ isHovered={hoveredMarketId === market.id}
33
+ onSelect={() => onSelectMarket(market)}
34
+ onHover={onMarketHover}
35
+ />
36
+ ))}
37
+ </div>
38
+ <footer className="mt-auto bg-black/20 border-t border-black/30">
39
+ <WeatherWidget />
40
+ </footer>
41
+ </aside>
42
+ );
43
+ };
44
+
45
+ export default MarketList;
cologne-christmas-markets-map-2025_5/components/MarketListItem.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import type { Market } from '../types';
3
+
4
+ interface MarketListItemProps {
5
+ market: Market;
6
+ isSelected: boolean;
7
+ isHovered: boolean;
8
+ onSelect: () => void;
9
+ onHover: (marketId: number | null) => void;
10
+ }
11
+
12
+ const MarketListItem: React.FC<MarketListItemProps> = ({ market, isSelected, isHovered, onSelect, onHover }) => {
13
+ const itemClasses = `
14
+ p-4 border-b border-black/20 cursor-pointer transition-all duration-200 ease-in-out
15
+ ${isSelected ? 'bg-amber-900/40 border-l-4 border-amber-400' : isHovered ? 'bg-stone-700/70 border-l-4 border-stone-600' : 'hover:bg-stone-700/50 border-l-4 border-transparent'}
16
+ `;
17
+
18
+ return (
19
+ <div
20
+ className={itemClasses}
21
+ onClick={onSelect}
22
+ onMouseEnter={() => onHover(market.id)}
23
+ onMouseLeave={() => onHover(null)}
24
+ >
25
+ <h3 className={`font-semibold text-lg ${isSelected ? 'text-amber-300' : 'text-stone-200'}`}>
26
+ {market.name}
27
+ </h3>
28
+ <div className="text-sm text-stone-400 mt-1">
29
+ <p>
30
+ <span className="font-medium text-stone-300">Zeitraum:</span> {market.start} - {market.end}
31
+ </p>
32
+ </div>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export default MarketListItem;
cologne-christmas-markets-map-2025_5/components/WeatherWidget.tsx ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ // Simplified weather icons as React components
4
+ const SunIcon = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>;
5
+ const MoonIcon = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>;
6
+ const CloudIcon = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>;
7
+ const CloudRainIcon = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="16" y1="13" x2="16" y2="21"></line><line x1="8" y1="13" x2="8" y2="21"></line><line x1="12" y1="15" x2="12" y2="23"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg>;
8
+ const CloudSnowIcon = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 17.58A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"></path><line x1="8" y1="16" x2="8" y2="16"></line><line x1="8" y1="20" x2="8" y2="20"></line><line x1="12" y1="18" x2="12" y2="18"></line><line x1="12" y1="22" x2="12" y2="22"></line><line x1="16" y1="16" x2="16" y2="16"></line><line x1="16" y1="20" x2="16" y2="20"></line></svg>;
9
+ const CloudFogIcon = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M16 17H7"/><path d="M17 21H9"/></svg>;
10
+
11
+ interface WeatherData {
12
+ temperature: number;
13
+ weatherCode: number;
14
+ isDay: number;
15
+ }
16
+
17
+ const getWeatherInfo = (code: number, isDay: number): { icon: React.ReactNode; description: string } => {
18
+ switch (code) {
19
+ case 0:
20
+ return { icon: isDay ? <SunIcon /> : <MoonIcon />, description: 'Clear sky' };
21
+ case 1:
22
+ case 2:
23
+ case 3:
24
+ return { icon: <CloudIcon />, description: 'Partly cloudy' };
25
+ case 45:
26
+ case 48:
27
+ return { icon: <CloudFogIcon />, description: 'Fog' };
28
+ case 51:
29
+ case 53:
30
+ case 55:
31
+ return { icon: <CloudRainIcon />, description: 'Drizzle' };
32
+ case 61:
33
+ case 63:
34
+ case 65:
35
+ return { icon: <CloudRainIcon />, description: 'Rain' };
36
+ case 71:
37
+ case 73:
38
+ case 75:
39
+ case 77:
40
+ return { icon: <CloudSnowIcon />, description: 'Snow fall' };
41
+ case 80:
42
+ case 81:
43
+ case 82:
44
+ return { icon: <CloudRainIcon />, description: 'Rain showers' };
45
+ case 85:
46
+ case 86:
47
+ return { icon: <CloudSnowIcon />, description: 'Snow showers' };
48
+ case 95:
49
+ case 96:
50
+ case 99:
51
+ return { icon: <CloudRainIcon />, description: 'Thunderstorm' };
52
+ default:
53
+ return { icon: <CloudIcon />, description: 'Cloudy' };
54
+ }
55
+ };
56
+
57
+ const WeatherWidget: React.FC = () => {
58
+ const [weather, setWeather] = useState<WeatherData | null>(null);
59
+ const [loading, setLoading] = useState(true);
60
+ const [error, setError] = useState<string | null>(null);
61
+
62
+ useEffect(() => {
63
+ const fetchWeather = async () => {
64
+ try {
65
+ const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=50.94&longitude=6.96&current=temperature_2m,is_day,weather_code');
66
+ if (!response.ok) {
67
+ throw new Error('Failed to fetch weather data');
68
+ }
69
+ const data = await response.json();
70
+ setWeather({
71
+ temperature: Math.round(data.current.temperature_2m),
72
+ weatherCode: data.current.weather_code,
73
+ isDay: data.current.is_day,
74
+ });
75
+ } catch (err) {
76
+ setError(err instanceof Error ? err.message : 'An unknown error occurred');
77
+ } finally {
78
+ setLoading(false);
79
+ }
80
+ };
81
+
82
+ fetchWeather();
83
+ }, []);
84
+
85
+ if (loading) {
86
+ return (
87
+ <div className="p-4 text-center text-stone-400">
88
+ Loading weather...
89
+ </div>
90
+ );
91
+ }
92
+
93
+ if (error || !weather) {
94
+ return (
95
+ <div className="p-4 text-center text-rose-400">
96
+ Could not load weather.
97
+ </div>
98
+ );
99
+ }
100
+
101
+ const { icon, description } = getWeatherInfo(weather.weatherCode, weather.isDay);
102
+
103
+ return (
104
+ <div className="p-4 flex items-center justify-between text-stone-300">
105
+ <div className="flex items-center">
106
+ <div className="text-amber-400 mr-4">
107
+ {icon}
108
+ </div>
109
+ <div>
110
+ <p className="font-bold text-lg text-stone-200">Cologne</p>
111
+ <p className="text-sm">{description}</p>
112
+ </div>
113
+ </div>
114
+ <div className="text-3xl font-bold font-['Cinzel'] text-white">
115
+ {weather.temperature}°C
116
+ </div>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ export default WeatherWidget;
cologne-christmas-markets-map-2025_5/constants.ts ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Market } from './types';
2
+
3
+ export const MARKETS: Market[] = [
4
+ {
5
+ id: 1,
6
+ name: "Weihnachtsmarkt am Kölner Dom",
7
+ lat: 50.941278,
8
+ lon: 6.958281,
9
+ start: "17.11.2025",
10
+ end: "23.12.2025",
11
+ ort: "Roncalliplatz, 50667 Köln",
12
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/08/weihnachtsmarkt-am-koelner-dom-imago-futureimage-95332928-1.jpg"],
13
+ description: "Großer Markt mit Lichterzelt und hohem Weihnachtsbaum direkt am Dom.",
14
+ openingHours: "So-Mi 11:00-21:00, Do-Fr 11:00-22:00, Sa 10:00-22:00, Totensonntag geschlossen",
15
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-am-koelner-dom/",
16
+ category: 'Traditional'
17
+ },
18
+ {
19
+ id: 2,
20
+ name: "Markt der Engel auf dem Neumarkt",
21
+ lat: 50.936002,
22
+ lon: 6.947546,
23
+ start: "17.11.2025",
24
+ end: "23.12.2025",
25
+ ort: "Neumarkt, 50667 Köln",
26
+ imageUrls: ["https://koeln.mitvergnuegen.com/resizer/images/7b227769647468223a3934302c22686569676874223a3532397d/wp-content/uploads/sites/4/2020/12/markt-der-engel-weihnachtsmarkt-neumarkt-koln.jpg.webp"],
27
+ description: "Stimmungsvoller Markt mit vielen Sternenlichtern und Engel-Figuren.",
28
+ openingHours: "So-Do 11:00-21:00, Fr-Sa 11:00-22:00, Totensonntag geschlossen",
29
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-neumarkt/",
30
+ category: 'Traditional'
31
+ },
32
+ {
33
+ id: 3,
34
+ name: "Heinzels Wintermärchen Altstadt",
35
+ lat: 50.9394,
36
+ lon: 6.9599,
37
+ start: "24.11.2025",
38
+ end: "04.01.2026",
39
+ ort: "Alter Markt und Heumarkt, 50667 Köln",
40
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/08/weihnachtsmarkt-koelner-altstadt-jochen-tack-imago95344912-1-800x470.jpg"],
41
+ description: "Großer Altstadtmarkt mit Eislaufbahn und vielen Buden am Alter Markt und Heumarkt.",
42
+ openingHours: "Täglich 11:00-22:00, ab 26.12 11:00-21:00, 24. und 25.12 geschlossen",
43
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-altstadt/",
44
+ category: 'Traditional'
45
+ },
46
+ {
47
+ id: 4,
48
+ name: "Nikolausdorf Rudolfplatz",
49
+ lat: 50.9369,
50
+ lon: 6.9378,
51
+ start: "17.11.2025",
52
+ end: "23.12.2025",
53
+ ort: "Rudolfplatz, 50674 Köln",
54
+ imageUrls: ["https://nikolausdorf.com/templates/yootheme/cache/5d/footer_img_03b-5dbd2f20.webp"],
55
+ description: "Familienfreundlicher Markt als Nikolausdorf vor der Hahnentorburg.",
56
+ openingHours: "So-Do 11:00-21:00, Fr-Sa 11:00-22:00, Glühweinstände je 1 Stunde länger, Totensonntag geschlossen",
57
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-rudolfplatz/",
58
+ category: 'Themed'
59
+ },
60
+ {
61
+ id: 5,
62
+ name: "Hafen-Weihnachtsmarkt am Schokoladenmuseum",
63
+ lat: 50.9322,
64
+ lon: 6.9638,
65
+ start: "14.11.2025",
66
+ end: "23.12.2025",
67
+ ort: "Am Schokoladenmuseum, 50678 Köln",
68
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/08/hafenweihnachtsmarkt-imago-zoonar-0170166521.jpg"],
69
+ description: "Maritimer Markt im Rheinauhafen mit weißen Pagodenzelten und Riesenrad.",
70
+ openingHours: "Täglich 11:00-21:00, Fr-Sa 11:00-22:00, Volkstrauertag ab 13:00, Totensonntag ab 18:00",
71
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/hafen-weihnachtsmarkt/",
72
+ category: 'Themed'
73
+ },
74
+ {
75
+ id: 6,
76
+ name: "Weihnachtsmarkt im Stadtgarten",
77
+ lat: 50.9435,
78
+ lon: 6.9380,
79
+ start: "14.11.2025",
80
+ end: "23.12.2025",
81
+ ort: "Venloer Str. 40, 50672 Köln",
82
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2024/11/Weihnachtsmarkt-Stadtgarten06-Vincent-Alex-2000-800x470.jpg"],
83
+ description: "Beliebter Veedels-Markt mit vielen Design- und Handwerksständen unter Bäumen.",
84
+ openingHours: "Mo-Fr 16:00-21:30, Sa-So 12:00-21:30, Totensonntag ab 18:00",
85
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-stadtgarten/",
86
+ category: 'Themed'
87
+ },
88
+ {
89
+ id: 7,
90
+ name: "Veedelsadvent auf dem Chlodwigplatz",
91
+ lat: 50.9232,
92
+ lon: 6.9560,
93
+ start: "20.11.2025",
94
+ end: "23.12.2025",
95
+ ort: "Chlodwigplatz, 50678 Köln",
96
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/clodwigplatz_weihnachtsmarkt_2016-27-von-37-800x470.jpg"],
97
+ description: "Kleiner, nachbarschaftlicher Veedels-Markt mit lokalen Anbietern.",
98
+ openingHours: "Täglich 12:00-22:00, Fr-Sa bis 23:00, Totensonntag geschlossen",
99
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-chlodwigplatz/",
100
+ category: 'Community'
101
+ },
102
+ {
103
+ id: 8,
104
+ name: "Heavenue Cologne",
105
+ lat: 50.9406,
106
+ lon: 6.9421,
107
+ start: "17.11.2025",
108
+ end: "23.12.2025",
109
+ ort: "Friesenplatz, 50672 Köln",
110
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/Weihnachtsmarkt-AdobeStock_128195075-800x470.jpg"],
111
+ description: "Bunter LGBTQ+ Weihnachtsmarkt mit Shows, Musik und Food am Friesenplatz.",
112
+ openingHours: "Mo-Do 12:00-22:00, Fr 12:00-23:00, Sa 11:00-23:00, So 11:00-22:00, Totensonntag geschlossen",
113
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/heavenue-cologne/",
114
+ category: 'Themed'
115
+ },
116
+ {
117
+ id: 9,
118
+ name: "Winterzauber Eigelstein",
119
+ lat: 50.9479,
120
+ lon: 6.9563,
121
+ start: "03.12.2025",
122
+ end: "07.12.2025",
123
+ ort: "Eigelsteintorburg, 50668 Köln",
124
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
125
+ description: "Kleiner Markt an der Eigelsteintorburg mit Bühnenprogramm und Gastronomie.",
126
+ openingHours: "Mi-Fr 15:00-22:00, Sa 14:00-22:00, So 14:00-21:00",
127
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/winterzauber-eigelstein/",
128
+ category: 'Short-Term'
129
+ },
130
+ {
131
+ id: 10,
132
+ name: "Der kleinste Weihnachtsmarkt der Stadt",
133
+ lat: 50.9238,
134
+ lon: 6.9455,
135
+ start: "19.11.2025",
136
+ end: "20.12.2025",
137
+ ort: "Biergarten im Volksgarten, Volksgartenstr. 27, 50677 Köln",
138
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/Weihnachtsmarkt-AdobeStock_128195075-800x470.jpg"],
139
+ description: "Gemütlicher Benefiz-Markt im Biergarten Volksgarten, Erlös für soziale Projekte.",
140
+ openingHours: "Mo-Fr 17:00-22:00, Sa 15:00-22:00, So 15:00-20:00",
141
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/kleinster-weihnachtsmarkt-der-stadt/",
142
+ category: 'Community'
143
+ },
144
+ {
145
+ id: 11,
146
+ name: "Weihnachtsmarkt an St. Aposteln",
147
+ lat: 50.9372,
148
+ lon: 6.9443,
149
+ start: "17.11.2025",
150
+ end: "23.12.2025",
151
+ ort: "Apostelnkloster, 50672 Köln",
152
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
153
+ description: "Kirchnaher Markt mit Buden rund um St. Aposteln nahe Neumarkt.",
154
+ openingHours: "täglich ca. 11:00-22:00, freitags & samstags teils länger laut Aushang vor Ort",
155
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmarkt-st-aposteln/",
156
+ category: 'Traditional'
157
+ },
158
+ {
159
+ id: 12,
160
+ name: "Weihnachtsbasar Josef Esser Platz",
161
+ lat: 50.9634,
162
+ lon: 6.8943,
163
+ start: "29.11.2025",
164
+ end: "30.11.2025",
165
+ ort: "Josef Esser Platz, 50827 Köln",
166
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
167
+ description: "Nachbarschaftlicher Weihnachtsbasar mit Ständen auf dem Platz in Bickendorf.",
168
+ openingHours: "laut Veranstalter, meist nachmittags bis abends",
169
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
170
+ category: 'Short-Term'
171
+ },
172
+ {
173
+ id: 13,
174
+ name: "Dellbrücker Weihnachtsmarkt",
175
+ lat: 50.9882,
176
+ lon: 7.0865,
177
+ start: "29.11.2025",
178
+ end: "29.11.2025",
179
+ ort: "Beim SV Adler Dellbrück, 51069 Köln",
180
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
181
+ description: "Klassischer Veedel Markt beim SV Adler Dellbrück.",
182
+ openingHours: "ab 15:00 Uhr bis Abend",
183
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
184
+ category: 'Short-Term'
185
+ },
186
+ {
187
+ id: 14,
188
+ name: "Weihnachtsmarkt im Waldbad Dünnwald",
189
+ lat: 51.0028,
190
+ lon: 7.0425,
191
+ start: "13.12.2025",
192
+ end: "14.12.2025",
193
+ ort: "Waldbad Dünnwald, Peter Bergner Weg 2, 51069 Köln",
194
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
195
+ description: "Atmosphärischer Markt im Waldbad mit Waldkulisse.",
196
+ openingHours: "Sa 14:00-20:00, So 11:00-18:00",
197
+ website: "https://waldbad-camping.de",
198
+ category: 'Short-Term'
199
+ },
200
+ {
201
+ id: 15,
202
+ name: "Dünnwalder Adventsmarkt",
203
+ lat: 50.9995,
204
+ lon: 7.0336,
205
+ start: "30.11.2025",
206
+ end: "30.11.2025",
207
+ ort: "Kirchplatz vor St. Hermann Joseph, 51069 Köln",
208
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
209
+ description: "Kleiner Adventsmarkt vor der Kirche St. Hermann Joseph.",
210
+ openingHours: "12:00-18:00",
211
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
212
+ category: 'Short-Term'
213
+ },
214
+ {
215
+ id: 16,
216
+ name: "Weihnachtsmarkt im Herbrands",
217
+ lat: 50.9497,
218
+ lon: 6.9211,
219
+ start: "21.11.2025",
220
+ end: "23.12.2025",
221
+ ort: "Herbrandstr. 21, 50825 Köln",
222
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
223
+ description: "Indoor und Hof Markt mit Gastro und Programm im Herbrands in Ehrenfeld.",
224
+ openingHours: "Öffnungszeiten nach Spielplan, meist nachmittags bis spät abends",
225
+ website: "https://www.herbrands.de",
226
+ category: 'Themed'
227
+ },
228
+ {
229
+ id: 17,
230
+ name: "Weihnachtsmarkt bei Bumann und Sohn",
231
+ lat: 50.9482,
232
+ lon: 6.9205,
233
+ start: "13.11.2025",
234
+ end: "23.12.2025",
235
+ ort: "Bartholomäus-Schink-Str. 2, 50825 Köln",
236
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
237
+ description: "Alternativer Markt im Club Bumann und Sohn in Ehrenfeld.",
238
+ openingHours: "überwiegend ab Nachmittags/Abend laut Programm",
239
+ website: "https://bumannundsohn.de",
240
+ category: 'Themed'
241
+ },
242
+ {
243
+ id: 18,
244
+ name: "Wintermarkt im CBE",
245
+ lat: 50.9494,
246
+ lon: 6.9200,
247
+ start: "16.11.2025",
248
+ end: "23.12.2025",
249
+ ort: "Venloer Str. 429, 50825 Köln",
250
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
251
+ description: "Sozialer Wintermarkt im Bürgerzentrum Ehrenfeld.",
252
+ openingHours: "laut Veranstalter, meist nachmittags",
253
+ website: "https://www.cbe.koeln",
254
+ category: 'Community'
255
+ },
256
+ {
257
+ id: 19,
258
+ name: "Garagen Glüh'n Hofweihnachtsmarkt Körnerstrasse",
259
+ lat: 50.9507,
260
+ lon: 6.9281,
261
+ start: "06.12.2025",
262
+ end: "07.12.2025",
263
+ ort: "Körnerstr., 50823 Köln",
264
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
265
+ description: "Kreativer Hofmarkt mit vielen Garagenständen in der Körnerstraße in Ehrenfeld.",
266
+ openingHours: "jeweils tagsüber bis abends",
267
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
268
+ category: 'Short-Term'
269
+ },
270
+ {
271
+ id: 20,
272
+ name: "Advent am Geisselmarkt",
273
+ lat: 50.9491,
274
+ lon: 6.9299,
275
+ start: "23.11.2025",
276
+ end: "23.11.2025",
277
+ ort: "Geisselmarkt, 50823 Köln",
278
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
279
+ description: "Kleiner Adventstreff auf dem Geisselmarkt in Ehrenfeld.",
280
+ openingHours: "11:00-17:00",
281
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
282
+ category: 'Short-Term'
283
+ },
284
+ {
285
+ id: 21,
286
+ name: "Traditioneller Weihnachtsmarkt Esch",
287
+ lat: 51.0185,
288
+ lon: 6.8340,
289
+ start: "29.11.2025",
290
+ end: "30.11.2025",
291
+ ort: "Alter Dorfkern, Kirchgasse, 50765 Köln",
292
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
293
+ description: "Dorfkern Markt mit Buden in der Kirchgasse.",
294
+ openingHours: "laut Veranstalter",
295
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
296
+ category: 'Short-Term'
297
+ },
298
+ {
299
+ id: 22,
300
+ name: "Weihnachtsmarkt Reitstall Birkenhof",
301
+ lat: 50.9922,
302
+ lon: 7.0583,
303
+ start: "14.11.2025",
304
+ end: "21.12.2025",
305
+ ort: "Reitstall Birkenhof, 51069 Köln",
306
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
307
+ description: "Reiterhof Weihnachtsmarkt mit Buden und Aktionen in Höhenhaus.",
308
+ openingHours: "Fr-So 15:00-22:00",
309
+ website: "https://www.instagram.com/birkenhof",
310
+ category: 'Themed'
311
+ },
312
+ {
313
+ id: 23,
314
+ name: "Veganer Weihnachtsmarkt Kalk",
315
+ lat: 50.9404,
316
+ lon: 6.9999,
317
+ start: "05.12.2025",
318
+ end: "21.12.2025",
319
+ ort: "Evangelische Kirche, Steinmetzstr. 57, 51103 Köln",
320
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
321
+ description: "Veganer Markt an drei Adventswochenenden an der ev. Kirche.",
322
+ openingHours: "Fr-So an den Wochenenden, ca. 15:00-21:00",
323
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
324
+ category: 'Themed'
325
+ },
326
+ {
327
+ id: 24,
328
+ name: "Lindenthaler Adventstreff Karl-Schwering-Platz",
329
+ lat: 50.9279,
330
+ lon: 6.9157,
331
+ start: "20.11.2025",
332
+ end: "23.12.2025",
333
+ ort: "Karl-Schwering-Platz, 50931 Köln",
334
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
335
+ description: "Kleiner Adventsmarkt im Lindenthaler Zentrum.",
336
+ openingHours: "täglich 12:00-22:00, Totensonntag ab 18:00",
337
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
338
+ category: 'Community'
339
+ },
340
+ {
341
+ id: 25,
342
+ name: "Winterzauber am See Stadtwald",
343
+ lat: 50.9246,
344
+ lon: 6.9038,
345
+ start: "21.11.2025",
346
+ end: "28.12.2025",
347
+ ort: "Leonardo Royal Hotel, Dürener Str. 287, 50935 Köln",
348
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
349
+ description: "Winterzauber Terrasse am See beim Leonardo Royal Hotel.",
350
+ openingHours: "laut Hotel, meist nachmittags/abends",
351
+ website: "https://www.leonardo-hotels.de",
352
+ category: 'Themed'
353
+ },
354
+ {
355
+ id: 26,
356
+ name: "Lindweiler Weihnachtsmarkt",
357
+ lat: 51.0069,
358
+ lon: 6.9056,
359
+ start: "12.12.2025",
360
+ end: "14.12.2025",
361
+ ort: "Marienberger Weg 19, 50767 Köln",
362
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
363
+ description: "Veedel Weihnachtsmarkt am Marienberger Weg.",
364
+ openingHours: "laut Bürgerverein, meist nachmittags",
365
+ website: "https://www.bv-lindweiler.de",
366
+ category: 'Short-Term'
367
+ },
368
+ {
369
+ id: 27,
370
+ name: "Kleinster Weihnachtsmarkt Merheim",
371
+ lat: 50.9469,
372
+ lon: 7.0543,
373
+ start: "06.12.2025",
374
+ end: "06.12.2025",
375
+ ort: "Bauspielplatz, Walnussweg, 51109 Köln",
376
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
377
+ description: "Kleiner Markt auf dem Bauspielplatz Walnussweg.",
378
+ openingHours: "13:30-17:30",
379
+ website: "https://bauspielplatz-merheim.de",
380
+ category: 'Short-Term'
381
+ },
382
+ {
383
+ id: 28,
384
+ name: "Max und Moritz Markt Lenauplatz",
385
+ lat: 50.9559,
386
+ lon: 6.9231,
387
+ start: "28.11.2025",
388
+ end: "23.12.2025",
389
+ ort: "Lenauplatz, 50825 Köln",
390
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
391
+ description: "Veedel Weihnachtsmarkt mit Buden und Gastro in Neuehrenfeld.",
392
+ openingHours: "Mo-Do 16:30-21:30, Fr-So ab 14:00",
393
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
394
+ category: 'Community'
395
+ },
396
+ {
397
+ id: 29,
398
+ name: "Weihnachtsmarkt Lutherkirche",
399
+ lat: 50.9620,
400
+ lon: 6.9467,
401
+ start: "13.12.2025",
402
+ end: "14.12.2025",
403
+ ort: "Lutherkirche Nippes, 50733 Köln",
404
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
405
+ description: "Kirchnaher Markt an der Lutherkirche in Nippes.",
406
+ openingHours: "12:00-18:00",
407
+ website: "https://www.lutherkirche-nippes.de",
408
+ category: 'Short-Term'
409
+ },
410
+ {
411
+ id: 30,
412
+ name: "Ostheimer Weihnachtsmarkt Sparkassen Vorplatz",
413
+ lat: 50.9385,
414
+ lon: 7.0438,
415
+ start: "07.12.2025",
416
+ end: "07.12.2025",
417
+ ort: "Vorplatz ehem. Sparkasse, 51109 Köln",
418
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
419
+ description: "Kleiner Markt auf dem Vorplatz der ehemaligen Sparkasse.",
420
+ openingHours: "12:00-18:00",
421
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
422
+ category: 'Short-Term'
423
+ },
424
+ {
425
+ id: 31,
426
+ name: "Pescher Christkindlmarkt",
427
+ lat: 51.0076,
428
+ lon: 6.8550,
429
+ start: "07.12.2025",
430
+ end: "07.12.2025",
431
+ ort: "Pesch, 50767 Köln",
432
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
433
+ description: "Traditioneller Christkindlmarkt in Pesch.",
434
+ openingHours: "11:00-18:00",
435
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
436
+ category: 'Short-Term'
437
+ },
438
+ {
439
+ id: 32,
440
+ name: "Waldweihnacht Gut Leidenhausen",
441
+ lat: 50.8879,
442
+ lon: 7.0825,
443
+ start: "29.11.2025",
444
+ end: "30.11.2025",
445
+ ort: "Gut Leidenhausen, 51147 Köln",
446
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
447
+ description: "Stimmungsvoller Markt im Wald rund um Gut Leidenhausen in Porz.",
448
+ openingHours: "11:00-20:00",
449
+ website: "https://www.gut-leidenhausen.de",
450
+ category: 'Short-Term'
451
+ },
452
+ {
453
+ id: 33,
454
+ name: "Mittelalterlicher Weihnachtsmarkt Alexianer",
455
+ lat: 50.8753,
456
+ lon: 7.0435,
457
+ start: "21.11.2025",
458
+ end: "22.11.2025",
459
+ ort: "Alexianer, Kölner Str. 64, 51149 Köln",
460
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
461
+ description: "Mittelalter-Markt der Alexianer mit Programm in Porz.",
462
+ openingHours: "Fr 07:00-20:00, Sa 10:00-19:00",
463
+ website: "https://www.alexianer.de",
464
+ category: 'Short-Term'
465
+ },
466
+ {
467
+ id: 34,
468
+ name: "Porzer Weihnachtsmarkt City Center",
469
+ lat: 50.8879,
470
+ lon: 7.0427,
471
+ start: "12.12.2025",
472
+ end: "14.12.2025",
473
+ ort: "City Center Porz, 51143 Köln",
474
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
475
+ description: "Stadtteil-Markt mit Wunschbaum-Aktion im City Center.",
476
+ openingHours: "13:00-18:00",
477
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
478
+ category: 'Short-Term'
479
+ },
480
+ {
481
+ id: 35,
482
+ name: "Adventsmarkt Wahner Wibbelstetze",
483
+ lat: 50.8770,
484
+ lon: 7.0801,
485
+ start: "29.11.2025",
486
+ end: "30.11.2025",
487
+ ort: "Marktplatz Wahn, 51147 Köln",
488
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
489
+ description: "Adventsmarkt des Karnevalsvereins Wahner Wibbelstetze.",
490
+ openingHours: "laut Verein",
491
+ website: "https://wahner-wibbelstetze.de",
492
+ category: 'Short-Term'
493
+ },
494
+ {
495
+ id: 36,
496
+ name: "Weihnachtsmarkt Rath Heumar",
497
+ lat: 50.9332,
498
+ lon: 7.0722,
499
+ start: "05.12.2025",
500
+ end: "07.12.2025",
501
+ ort: "Kurt-Henn-Platz, 51107 Köln",
502
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
503
+ description: "Veedel Markt auf dem Kurt-Henn-Platz.",
504
+ openingHours: "laut Veranstalter",
505
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
506
+ category: 'Short-Term'
507
+ },
508
+ {
509
+ id: 37,
510
+ name: "Winterzauber Maternusplatz",
511
+ lat: 50.8953,
512
+ lon: 6.9740,
513
+ start: "05.12.2025",
514
+ end: "07.12.2025",
515
+ ort: "Maternusplatz, 50996 Köln",
516
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
517
+ description: "Kleiner Wintermarkt auf dem Maternusplatz in Rodenkirchen.",
518
+ openingHours: "laut Veranstalter, meist nachmittags/abends",
519
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
520
+ category: 'Short-Term'
521
+ },
522
+ {
523
+ id: 38,
524
+ name: "Sülzer Weihnachtsdorf",
525
+ lat: 50.9192,
526
+ lon: 6.9189,
527
+ start: "25.11.2025",
528
+ end: "23.12.2025",
529
+ ort: "Elisabeth-von-Mumm-Platz, 50937 Köln",
530
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
531
+ description: "Bekannter Veedel Markt auf dem Elisabeth-von-Mumm-Platz.",
532
+ openingHours: "Mo-Fr 16:00-22:00, Sa-So 12:00-22:00",
533
+ website: "https://www.suelzer-weihnachtsdorf.de",
534
+ category: 'Community'
535
+ },
536
+ {
537
+ id: 39,
538
+ name: "Christmas G'art'en Wachsfabrik",
539
+ lat: 50.8703,
540
+ lon: 6.9959,
541
+ start: "13.12.2025",
542
+ end: "14.12.2025",
543
+ ort: "Wachsfabrik, Industriestr. 170, 50999 Köln",
544
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
545
+ description: "Kreativ-Markt in der Wachsfabrik mit Kunst und Design in Sürth.",
546
+ openingHours: "laut Veranstalter",
547
+ website: "https://www.wachsfabrik.de",
548
+ category: 'Short-Term'
549
+ },
550
+ {
551
+ id: 40,
552
+ name: "Sürther Weihnachtsmarkt",
553
+ lat: 50.8745,
554
+ lon: 6.9995,
555
+ start: "05.12.2025",
556
+ end: "07.12.2025",
557
+ ort: "Marktplatz Sürth, 50999 Köln",
558
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
559
+ description: "Kleiner Markt auf dem Sürther Marktplatz.",
560
+ openingHours: "laut Veranstalter",
561
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
562
+ category: 'Short-Term'
563
+ },
564
+ {
565
+ id: 41,
566
+ name: "Zollstocker Adventsmarkt",
567
+ lat: 50.9069,
568
+ lon: 6.9365,
569
+ start: "06.12.2025",
570
+ end: "07.12.2025",
571
+ ort: "Rund um St. Pius, 50969 Köln",
572
+ imageUrls: ["https://www.koeln.de/wp-content/uploads/2023/10/weihnachtsmakt-koeln-altstadt-2019-sw-0029.jpg"],
573
+ description: "Adventsmarkt rund um die Kirche St. Pius in Zollstock.",
574
+ openingHours: "Sa 14:00-20:00, So 12:00-18:00",
575
+ website: "https://www.koeln.de/weihnachten/weihnachtsmaerkte-koeln/weihnachtsmaerkte-koelner-stadtteile/",
576
+ category: 'Short-Term'
577
+ }
578
+ ];
cologne-christmas-markets-map-2025_5/index.html ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Cologne Christmas Markets Map 2025</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700&family=EB+Garamond&display=swap" rel="stylesheet">
11
+ <link
12
+ rel="stylesheet"
13
+ href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
14
+ integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
15
+ crossorigin=""
16
+ />
17
+ <style>
18
+ html, body, #root {
19
+ height: 100%;
20
+ width: 100%;
21
+ margin: 0;
22
+ padding: 0;
23
+ overflow: hidden;
24
+ font-family: 'EB Garamond', serif;
25
+ }
26
+ h1, h2, h3 {
27
+ font-family: 'Cinzel', serif;
28
+ }
29
+ .leaflet-container {
30
+ height: 100%;
31
+ width: 100%;
32
+ background-color: #f3f4f6; /* gray-100 */
33
+ }
34
+
35
+ /* Festive Popup Styling */
36
+ .leaflet-popup-content-wrapper {
37
+ background-color: #1c1917; /* stone-900 */
38
+ background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M12 2L12 22M2 12L22 12M5.636 5.636L18.364 18.364M5.636 18.364L18.364 5.636M12 6L14.121 8.121M12 6L9.879 8.121M12 18L14.121 15.879M12 18L9.879 15.879M18.364 12L15.879 9.879M18.364 12L15.879 14.121M5.636 12L8.121 9.879M5.636 12L8.121 14.121" stroke="rgba(250, 204, 21, 0.08)" stroke-width="1" fill="none"/%3E%3C/svg%3E');
39
+ color: #d6d3d1; /* stone-300 */
40
+ border-radius: 8px;
41
+ border: 1px solid #f59e0b; /* amber-500 */
42
+ box-shadow: 0 0 15px rgba(245, 158, 11, 0.4), 0 4px 10px rgba(0,0,0,0.6);
43
+ padding: 0;
44
+ transform-origin: bottom center;
45
+ animation: popup-open 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
46
+ }
47
+ .leaflet-popup-content {
48
+ margin: 0;
49
+ width: 288px !important;
50
+ line-height: 1.6;
51
+ }
52
+ .leaflet-popup-content > div {
53
+ animation: content-fade-in 0.5s ease-out 0.2s forwards;
54
+ opacity: 0;
55
+ }
56
+ .leaflet-popup-tip {
57
+ background: #1c1917;
58
+ }
59
+ .leaflet-popup-close-button {
60
+ color: #facc15 !important; /* amber-400 */
61
+ padding: 4px 4px 0 0 !important;
62
+ transition: all 0.2s ease;
63
+ }
64
+ .leaflet-popup-close-button:hover {
65
+ color: #f59e0b !important; /* amber-500 */
66
+ transform: scale(1.2);
67
+ }
68
+
69
+ @keyframes popup-open {
70
+ from {
71
+ opacity: 0;
72
+ transform: scale(0.8);
73
+ }
74
+ to {
75
+ opacity: 1;
76
+ transform: scale(1);
77
+ }
78
+ }
79
+ @keyframes content-fade-in {
80
+ from {
81
+ opacity: 0;
82
+ transform: translateY(10px);
83
+ }
84
+ to {
85
+ opacity: 1;
86
+ transform: translateY(0);
87
+ }
88
+ }
89
+
90
+ .locate-me-btn {
91
+ background-color: #292524; /* stone-800 */
92
+ border: 2px solid #facc15; /* amber-400 */
93
+ color: #facc15;
94
+ font-size: 1.5rem;
95
+ line-height: 1;
96
+ border-radius: 8px;
97
+ width: 40px;
98
+ height: 40px;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ cursor: pointer;
103
+ box-shadow: 0 2px 8px rgba(0,0,0,0.7);
104
+ transition: background-color 0.2s ease-in-out;
105
+ }
106
+ .locate-me-btn:hover {
107
+ background-color: #44403c; /* stone-700 */
108
+ }
109
+ .user-location-pulse {
110
+ background-color: #38bdf8; /* sky-400 */
111
+ border: 2px solid white;
112
+ border-radius: 50%;
113
+ box-shadow: 0 0 0 0 rgba(56, 189, 248, 1);
114
+ transform: scale(1);
115
+ animation: pulse 2s infinite;
116
+ }
117
+ @keyframes pulse {
118
+ 0% {
119
+ transform: scale(0.95);
120
+ box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7);
121
+ }
122
+ 70% {
123
+ transform: scale(1);
124
+ box-shadow: 0 0 0 12px rgba(56, 189, 248, 0);
125
+ }
126
+ 100% {
127
+ transform: scale(0.95);
128
+ box-shadow: 0 0 0 0 rgba(56, 189, 248, 0);
129
+ }
130
+ }
131
+ .modal-backdrop {
132
+ animation: fadeIn 0.3s ease-out forwards;
133
+ }
134
+ .modal-content {
135
+ animation: fadeIn 0.3s ease-out forwards, slideUp 0.4s ease-out forwards;
136
+ }
137
+ @keyframes fadeIn {
138
+ from { opacity: 0; }
139
+ to { opacity: 1; }
140
+ }
141
+ @keyframes slideUp {
142
+ from { transform: translateY(20px); }
143
+ to { transform: translateY(0); }
144
+ }
145
+ .selected-marker-icon {
146
+ animation: pulseGold 1.5s infinite ease-in-out;
147
+ }
148
+ @keyframes pulseGold {
149
+ 0% {
150
+ filter: drop-shadow(0 0 2px #fde047);
151
+ transform: scale(1) rotate(0deg);
152
+ }
153
+ 50% {
154
+ filter: drop-shadow(0 0 12px #fde047);
155
+ transform: scale(1.1) rotate(5deg);
156
+ }
157
+ 100% {
158
+ filter: drop-shadow(0 0 2px #fde047);
159
+ transform: scale(1) rotate(0deg);
160
+ }
161
+ }
162
+ .hovered-marker-icon {
163
+ animation: festiveBob 2s infinite ease-in-out;
164
+ }
165
+ @keyframes festiveBob {
166
+ 0%, 100% {
167
+ transform: translateY(0);
168
+ filter: drop-shadow(0 0 3px #f43f5e);
169
+ }
170
+ 50% {
171
+ transform: translateY(-3px);
172
+ filter: drop-shadow(0 0 8px #f43f5e);
173
+ }
174
+ }
175
+
176
+ /* Snowfall Animation */
177
+ .snowflake {
178
+ color: #fff;
179
+ font-size: 1em;
180
+ font-family: Arial, sans-serif;
181
+ text-shadow: 0 0 5px #000;
182
+ position: absolute;
183
+ top: -10%;
184
+ z-index: 9999;
185
+ -webkit-user-select: none;
186
+ -moz-user-select: none;
187
+ -ms-user-select: none;
188
+ user-select: none;
189
+ cursor: default;
190
+ animation-name: snowflakes-fall, snowflakes-shake;
191
+ animation-duration: 10s, 3s;
192
+ animation-timing-function: linear, ease-in-out;
193
+ animation-iteration-count: infinite, infinite;
194
+ animation-play-state: running, running;
195
+ }
196
+ .snowflake:nth-of-type(0) { left: 1%; animation-delay: 0s, 0s; }
197
+ .snowflake:nth-of-type(1) { left: 10%; animation-delay: 1s, 1s; }
198
+ .snowflake:nth-of-type(2) { left: 20%; animation-delay: 6s, 0.5s; }
199
+ .snowflake:nth-of-type(3) { left: 30%; animation-delay: 4s, 2s; }
200
+ .snowflake:nth-of-type(4) { left: 40%; animation-delay: 2s, 2s; }
201
+ .snowflake:nth-of-type(5) { left: 50%; animation-delay: 8s, 3s; }
202
+ .snowflake:nth-of-type(6) { left: 60%; animation-delay: 6s, 2s; }
203
+ .snowflake:nth-of-type(7) { left: 70%; animation-delay: 2.5s, 1s; }
204
+ .snowflake:nth-of-type(8) { left: 80%; animation-delay: 1s, 0s; }
205
+ .snowflake:nth-of-type(9) { left: 90%; animation-delay: 3s, 1.5s; }
206
+ .snowflake:nth-of-type(10) { left: 25%; animation-delay: 2s, 0s; }
207
+ .snowflake:nth-of-type(11) { left: 65%; animation-delay: 4s, 2.5s; }
208
+
209
+ @keyframes snowflakes-fall {
210
+ 0% { top: -10%; }
211
+ 100% { top: 100%; }
212
+ }
213
+ @keyframes snowflakes-shake {
214
+ 0%, 100% { transform: translateX(0); }
215
+ 50% { transform: translateX(80px); }
216
+ }
217
+ </style>
218
+ <script type="importmap">
219
+ {
220
+ "imports": {
221
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
222
+ "@react-leaflet/core": "https://aistudiocdn.com/@react-leaflet/core@^5.0.0",
223
+ "react-leaflet": "https://aistudiocdn.com/react-leaflet@^5.0.0",
224
+ "leaflet": "https://aistudiocdn.com/leaflet@^1.9.4",
225
+ "react": "https://aistudiocdn.com/react@^19.2.0",
226
+ "react/": "https://aistudiocdn.com/react@^19.2.0/"
227
+ }
228
+ }
229
+ </script>
230
+ <link rel="stylesheet" href="/index.css">
231
+ </head>
232
+ <body>
233
+ <div id="root"></div>
234
+ <script type="module" src="/index.tsx"></script>
235
+ </body>
236
+ </html>
cologne-christmas-markets-map-2025_5/index.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App';
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error("Could not find root element to mount to");
9
+ }
10
+
11
+ const root = ReactDOM.createRoot(rootElement);
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
cologne-christmas-markets-map-2025_5/metadata.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Cologne Christmas Markets Map 2025",
3
+ "description": "An interactive map showcasing the Christmas markets in Cologne for 2025. Explore market locations, opening times, and details with a modern and festive design.",
4
+ "requestFramePermissions": [
5
+ "geolocation"
6
+ ]
7
+ }
cologne-christmas-markets-map-2025_5/package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cologne-christmas-markets-map-2025",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react-dom": "^19.2.0",
13
+ "@react-leaflet/core": "^5.0.0",
14
+ "react-leaflet": "^5.0.0",
15
+ "leaflet": "^1.9.4",
16
+ "react": "^19.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.14.0",
20
+ "@vitejs/plugin-react": "^5.0.0",
21
+ "typescript": "~5.8.2",
22
+ "vite": "^6.2.0"
23
+ }
24
+ }
cologne-christmas-markets-map-2025_5/tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
cologne-christmas-markets-map-2025_5/types.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export interface Market {
3
+ id: number;
4
+ name: string;
5
+ lat: number;
6
+ lon: number;
7
+ start: string;
8
+ end:string;
9
+ ort: string;
10
+ imageUrls: string[];
11
+ description: string;
12
+ openingHours: string;
13
+ website?: string;
14
+ category: 'Traditional' | 'Themed' | 'Community' | 'Short-Term';
15
+ }
cologne-christmas-markets-map-2025_5/vite.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const env = loadEnv(mode, '.', '');
7
+ return {
8
+ server: {
9
+ port: 3000,
10
+ host: '0.0.0.0',
11
+ },
12
+ plugins: [react()],
13
+ define: {
14
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
15
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
16
+ },
17
+ resolve: {
18
+ alias: {
19
+ '@': path.resolve(__dirname, '.'),
20
+ }
21
+ }
22
+ };
23
+ });