+
{
setActivePlaceId(id);
setIsMapSheetOpen(false);
+ const element = document.getElementById(`place-${id}`);
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
}}
/>
diff --git a/app-9xzmfic2e4g1/supabase/functions/generate-itinerary/index.ts b/app-9xzmfic2e4g1/supabase/functions/generate-itinerary/index.ts
index 33dc31f..3dd6c9d 100644
--- a/app-9xzmfic2e4g1/supabase/functions/generate-itinerary/index.ts
+++ b/app-9xzmfic2e4g1/supabase/functions/generate-itinerary/index.ts
@@ -39,6 +39,71 @@ function normalizePlaceName(name: string): string {
.replace(/\s*(open air museum|underground city|valley|village|castle|church)\s*$/i, (match) => ' ' + match.trim().toLowerCase())
}
+/**
+ * Generate a mock itinerary when OpenAI is unavailable.
+ */
+function generateMockItinerary(startDate: string, endDate: string, interests: string[]) {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ const diffTime = Math.abs(end.getTime() - start.getTime());
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
+ const numDays = Math.min(diffDays, 14);
+
+ const pool = [
+ { place_name: "Göreme Open Air Museum", category: "museum", duration: 120, desc: "UNESCO World Heritage site with rock-cut churches." },
+ { place_name: "Uçhisar Castle", category: "landmark", duration: 90, desc: "The highest point in Cappadocia with panoramic views." },
+ { place_name: "Pasabag (Monks Valley)", category: "nature", duration: 60, desc: "Famous fairy chimneys with multiple caps." },
+ { place_name: "Devrent Valley (Imagination Valley)", category: "nature", duration: 45, desc: "Unique rock formations resembling animals." },
+ { place_name: "Kaymakli Underground City", category: "history", duration: 90, desc: "Ancient multi-level underground city." },
+ { place_name: "Derinkuyu Underground City", category: "history", duration: 120, desc: "The deepest underground city in the region." },
+ { place_name: "Ihlara Valley", category: "nature", duration: 180, desc: "A stunning canyon with a river and rock-cut churches." },
+ { place_name: "Selime Monastery", category: "history", duration: 60, desc: "The largest religious structure in Cappadocia." },
+ { place_name: "Pigeon Valley", category: "nature", duration: 90, desc: "Valley named after the countless man-made dovecotes." },
+ { place_name: "Love Valley", category: "nature", duration: 60, desc: "Famous for its phallic-shaped fairy chimneys." },
+ { place_name: "Avanos Pottery Workshop", category: "culture", duration: 60, desc: "Traditional pottery making in the Red River town." },
+ { place_name: "Zelve Open Air Museum", category: "museum", duration: 120, desc: "An abandoned cave town with ancient churches." },
+ { place_name: "Ortahisar Castle", category: "landmark", duration: 60, desc: "A massive rock formation used as a fortress." },
+ { place_name: "Rose Valley Sunset Hike", category: "nature", duration: 120, desc: "Beautiful valley that turns pink at sunset." },
+ { place_name: "Red Valley", category: "nature", duration: 90, desc: "Stunning valley with sharp ridges and red hues." },
+ { place_name: "Çavuşin Village", category: "culture", duration: 60, desc: "An old Greek village with a massive rock castle." },
+ ];
+
+ const days = [];
+ for (let i = 1; i <= numDays; i++) {
+ const dailyItems = [];
+
+ // Always add Hot Air Balloon on Day 1 or 2 if duration is 120
+ if (i <= 2) {
+ dailyItems.push({
+ place_name: "Hot Air Balloon Flight",
+ category: "activity",
+ estimated_duration_minutes: 180,
+ description: "Breathtaking sunrise flight over the fairy chimneys."
+ });
+ }
+
+ // Pick 3-4 random items from the pool
+ const shuffled = [...pool].sort(() => 0.5 - Math.random());
+ const selected = shuffled.slice(0, 3);
+
+ selected.forEach(item => {
+ dailyItems.push({
+ place_name: item.place_name,
+ category: item.category,
+ estimated_duration_minutes: item.duration,
+ description: item.desc
+ });
+ });
+
+ days.push({
+ day: i,
+ items: dailyItems
+ });
+ }
+
+ return { days };
+}
+
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
@@ -53,57 +118,73 @@ serve(async (req) => {
SUPABASE_SERVICE_ROLE_KEY!
)
- // 1. Call OpenAI to generate itinerary
- const systemPrompt = `You are an expert travel planner for Cappadocia, Turkey.
- Generate a personalized travel itinerary based on user preferences.
- Allowed regions: Göreme, Uçhisar, Ürgüp, Avanos, Ortahisar, Çavuşin, Derinkuyu, Kaymaklı.
- Rules:
- - Balloon rides must be at sunrise (05:00 - 08:00) on Day 1 or 2.
- - Sunset valley visits after 17:30.
- - Max 5 places per day.
- - No duplicate places.
- - Only return JSON in the specified format.
- - Interests: ${interests.join(', ')}.
- - Daily Schedule: ${dailySchedule}.
-
- Response Format:
- {
- "days": [
+ let itinerary;
+
+ // 1. Call OpenAI to generate itinerary (if API key exists)
+ if (OPENAI_API_KEY && OPENAI_API_KEY !== 'PASTE_YOUR_OPENAI_API_KEY_HERE') {
+ try {
+ const systemPrompt = `You are an expert travel planner for Cappadocia, Turkey.
+ Generate a personalized travel itinerary based on user preferences.
+ Allowed regions: Göreme, Uçhisar, Ürgüp, Avanos, Ortahisar, Çavuşin, Derinkuyu, Kaymaklı.
+ Rules:
+ - Balloon rides must be at sunrise (05:00 - 08:00) on Day 1 or 2.
+ - Sunset valley visits after 17:30.
+ - Max 5 places per day.
+ - No duplicate places.
+ - Only return JSON in the specified format.
+ - Interests: ${interests.join(', ')}.
+ - Daily Schedule: ${dailySchedule}.
+
+ Response Format:
{
- "day": 1,
- "items": [
+ "days": [
{
- "place_name": "Göreme Open Air Museum",
- "category": "museum",
- "estimated_duration_minutes": 120,
- "description": "UNESCO World Heritage site with rock-cut churches."
+ "day": 1,
+ "items": [
+ {
+ "place_name": "Göreme Open Air Museum",
+ "category": "museum",
+ "estimated_duration_minutes": 120,
+ "description": "UNESCO World Heritage site with rock-cut churches."
+ }
+ ]
}
]
+ }`
+
+ const userPrompt = `Trip Dates: ${startDate} to ${endDate}. Preferences: ${preferences}`
+
+ const openaiRes = await fetch('https://api.openai.com/v1/chat/completions', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${OPENAI_API_KEY}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: 'gpt-4o-mini',
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt }
+ ],
+ temperature: 0.3,
+ response_format: { type: 'json_object' }
+ }),
+ })
+
+ if (!openaiRes.ok) {
+ throw new Error(`OpenAI API error: ${openaiRes.statusText}`);
}
- ]
- }`
- const userPrompt = `Trip Dates: ${startDate} to ${endDate}. Preferences: ${preferences}`
-
- const openaiRes = await fetch('https://api.openai.com/v1/chat/completions', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${OPENAI_API_KEY}`,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- model: 'gpt-4o-mini',
- messages: [
- { role: 'system', content: systemPrompt },
- { role: 'user', content: userPrompt }
- ],
- temperature: 0.3,
- response_format: { type: 'json_object' }
- }),
- })
-
- const openaiData = await openaiRes.json()
- const itinerary = JSON.parse(openaiData.choices[0].message.content)
+ const openaiData = await openaiRes.json()
+ itinerary = JSON.parse(openaiData.choices[0].message.content)
+ } catch (err) {
+ console.error('OpenAI failed, falling back to mock:', err)
+ itinerary = generateMockItinerary(startDate, endDate, interests)
+ }
+ } else {
+ console.log('No OpenAI API key found, using mock itinerary')
+ itinerary = generateMockItinerary(startDate, endDate, interests)
+ }
// 2. Verify places with Google Places API (with cache layer)
const verifiedDays = []
@@ -139,25 +220,38 @@ serve(async (req) => {
// Cache MISS - fetching from Google and caching
console.log(`Cache MISS for "${item.place_name}" (normalized: "${normalizedName}") - fetching from Google and caching`)
- // Text Search to get place_id and basic info
- const searchUrl = `https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(item.place_name + ' Cappadocia')}&key=${GOOGLE_MAPS_API_KEY}`
- const searchRes = await fetch(searchUrl)
- const searchData = await searchRes.json()
+ let placeInfo = null;
- if (searchData.results && searchData.results.length > 0) {
- const place = searchData.results[0]
-
+ if (GOOGLE_MAPS_API_KEY && GOOGLE_MAPS_API_KEY !== 'PASTE_YOUR_GOOGLE_MAPS_API_KEY_HERE') {
+ try {
+ // Text Search to get place_id and basic info
+ const searchUrl = `https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(item.place_name + ' Cappadocia')}&key=${GOOGLE_MAPS_API_KEY}`
+ const searchRes = await fetch(searchUrl)
+ const searchData = await searchRes.json()
+
+ if (searchData.results && searchData.results.length > 0) {
+ const place = searchData.results[0]
+ placeInfo = {
+ place_id: place.place_id,
+ name: place.name,
+ formatted_address: place.formatted_address,
+ lat: place.geometry.location.lat,
+ lng: place.geometry.location.lng,
+ rating: place.rating || null,
+ user_ratings_total: place.user_ratings_total || null,
+ photo_reference: place.photos?.[0]?.photo_reference || null
+ }
+ }
+ } catch (err) {
+ console.error(`Google Places API error for ${item.place_name}:`, err)
+ }
+ }
+
+ if (placeInfo) {
// Store in cache for future use with normalized name
const cacheEntry = {
place_name_normalized: normalizedName,
- place_id: place.place_id,
- name: place.name,
- formatted_address: place.formatted_address,
- lat: place.geometry.location.lat,
- lng: place.geometry.location.lng,
- rating: place.rating || null,
- user_ratings_total: place.user_ratings_total || null,
- photo_reference: place.photos?.[0]?.photo_reference || null
+ ...placeInfo
}
await supabase
@@ -166,14 +260,13 @@ serve(async (req) => {
verifiedItems.push({
...item,
- place_id: place.place_id,
- name: place.name,
- formatted_address: place.formatted_address,
- lat: place.geometry.location.lat,
- lng: place.geometry.location.lng,
- rating: place.rating,
- user_ratings_total: place.user_ratings_total,
- photo_reference: place.photos?.[0]?.photo_reference
+ ...placeInfo
+ })
+ } else {
+ // No Google info found or key missing, use item as is but mark as unverified
+ verifiedItems.push({
+ ...item,
+ unverified: true
})
}
}
@@ -193,7 +286,7 @@ serve(async (req) => {
}
const startTime = currentTime.toTimeString().slice(0, 5)
- currentTime.setMinutes(currentTime.getMinutes() + item.estimated_duration_minutes)
+ currentTime.setMinutes(currentTime.getMinutes() + (item.estimated_duration_minutes || 60))
const endTime = currentTime.toTimeString().slice(0, 5)
currentTime.setMinutes(currentTime.getMinutes() + 30) // 30 min travel/buffer
@@ -206,9 +299,10 @@ serve(async (req) => {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
} catch (error) {
+ console.error('Final catch error:', error)
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
-})
+})
\ No newline at end of file
diff --git a/app-9xzmfic2e4g1/supabase/functions/get-directions/index.ts b/app-9xzmfic2e4g1/supabase/functions/get-directions/index.ts
index 3a960e2..8b614eb 100644
--- a/app-9xzmfic2e4g1/supabase/functions/get-directions/index.ts
+++ b/app-9xzmfic2e4g1/supabase/functions/get-directions/index.ts
@@ -44,35 +44,62 @@ serve(async (req) => {
}
}
- // Cache miss or expired - call Google Directions API
- console.log('Cache miss for:', cache_key)
- let url = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin}&destination=${destination}&key=${GOOGLE_MAPS_API_KEY}&mode=driving`
-
- if (waypoints && waypoints.length > 0) {
- url += `&waypoints=${waypoints.join('|')}`
+ // Cache miss or expired - call Google Directions API (if key exists)
+ if (GOOGLE_MAPS_API_KEY && GOOGLE_MAPS_API_KEY !== 'PASTE_YOUR_GOOGLE_MAPS_API_KEY_HERE') {
+ try {
+ console.log('Cache miss for:', cache_key)
+ let url = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin}&destination=${destination}&key=${GOOGLE_MAPS_API_KEY}&mode=driving`
+
+ if (waypoints && waypoints.length > 0) {
+ url += `&waypoints=${waypoints.join('|')}`
+ }
+
+ const res = await fetch(url)
+ const data = await res.json()
+
+ if (data.status === 'OK') {
+ // Upsert the result into cache
+ await supabase
+ .from('directions_cache')
+ .upsert({
+ cache_key,
+ response: data,
+ created_at: new Date().toISOString()
+ }, {
+ onConflict: 'cache_key'
+ })
+ }
+
+ return new Response(JSON.stringify(data), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ })
+ } catch (err) {
+ console.error('Google Directions API error, falling back to dummy:', err)
+ }
}
- const res = await fetch(url)
- const data = await res.json()
+ // No key or error - return dummy response
+ const dummyResponse = {
+ status: "OK",
+ routes: [{
+ summary: "Dummy route for demonstration",
+ legs: [{
+ distance: { text: "10 km", value: 10000 },
+ duration: { text: "15 mins", value: 900 },
+ steps: []
+ }],
+ overview_polyline: { points: "" }
+ }]
+ }
- // Upsert the result into cache
- await supabase
- .from('directions_cache')
- .upsert({
- cache_key,
- response: data,
- created_at: new Date().toISOString()
- }, {
- onConflict: 'cache_key'
- })
-
- return new Response(JSON.stringify(data), {
+ return new Response(JSON.stringify(dummyResponse), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
+
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
-})
+})
\ No newline at end of file
diff --git a/app-9xzmfic2e4g1/supabase/functions/get-place-photo/index.ts b/app-9xzmfic2e4g1/supabase/functions/get-place-photo/index.ts
index 091deea..7aaa7b3 100644
--- a/app-9xzmfic2e4g1/supabase/functions/get-place-photo/index.ts
+++ b/app-9xzmfic2e4g1/supabase/functions/get-place-photo/index.ts
@@ -10,6 +10,12 @@ const corsHeaders = {
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
+const FALLBACK_PHOTO_URLS = [
+ 'https://images.unsplash.com/photo-1544833316-64d88e00182a?q=80&w=800&auto=format&fit=crop', // Cappadocia Valley
+ 'https://images.unsplash.com/photo-1570168007204-dfb528c6958f?q=80&w=800&auto=format&fit=crop', // Hot Air Balloons
+ 'https://images.unsplash.com/photo-1524231757912-21f4fe3a7200?q=80&w=800&auto=format&fit=crop', // Stone Houses
+]
+
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
@@ -20,7 +26,9 @@ serve(async (req) => {
const photoReference = urlObj.searchParams.get('photo_reference')
if (!photoReference) {
- return new Response('Missing photo_reference', { status: 400 })
+ // If missing photo_reference, redirect to a random fallback image
+ const fallbackUrl = FALLBACK_PHOTO_URLS[Math.floor(Math.random() * FALLBACK_PHOTO_URLS.length)]
+ return Response.redirect(fallbackUrl, 302)
}
// Initialize Supabase client with service role
@@ -37,51 +45,57 @@ serve(async (req) => {
// Try to HEAD the file to check if it exists
try {
const headResponse = await fetch(publicUrlData.publicUrl, { method: 'HEAD' })
-
if (headResponse.ok) {
// File exists in storage, redirect to it
return Response.redirect(publicUrlData.publicUrl, 302)
}
} catch (headError) {
// File doesn't exist, continue to fetch from Google
- console.log('File not in cache, fetching from Google:', headError)
+ console.log('File not in cache, fetching from Google...')
}
- // File doesn't exist in storage, fetch from Google
- const googlePhotoUrl = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=800&photo_reference=${photoReference}&key=${GOOGLE_MAPS_API_KEY}`
-
- const googleResponse = await fetch(googlePhotoUrl)
-
- if (!googleResponse.ok) {
- throw new Error(`Failed to fetch photo from Google: ${googleResponse.statusText}`)
+ // File doesn't exist in storage, fetch from Google (if key exists)
+ if (GOOGLE_MAPS_API_KEY && GOOGLE_MAPS_API_KEY !== 'PASTE_YOUR_GOOGLE_MAPS_API_KEY_HERE') {
+ try {
+ const googlePhotoUrl = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=800&photo_reference=${photoReference}&key=${GOOGLE_MAPS_API_KEY}`
+
+ const googleResponse = await fetch(googlePhotoUrl)
+
+ if (googleResponse.ok) {
+ // Get the image blob
+ const imageBlob = await googleResponse.blob()
+
+ // Upload to Supabase Storage
+ const { error: uploadError } = await supabase.storage
+ .from('place-photos')
+ .upload(storagePath, imageBlob, {
+ contentType: 'image/jpeg',
+ cacheControl: '31536000', // Cache for 1 year
+ upsert: false
+ })
+
+ if (!uploadError) {
+ // Successfully uploaded, redirect to storage URL
+ return Response.redirect(publicUrlData.publicUrl, 302)
+ } else {
+ console.error('Failed to upload to storage:', uploadError)
+ // If upload fails, still redirect to Google URL as fallback
+ return Response.redirect(googlePhotoUrl, 302)
+ }
+ }
+ } catch (err) {
+ console.error('Failed to fetch photo from Google, using fallback:', err)
+ }
}
- // Get the image blob
- const imageBlob = await googleResponse.blob()
-
- // Upload to Supabase Storage
- const { error: uploadError } = await supabase.storage
- .from('place-photos')
- .upload(storagePath, imageBlob, {
- contentType: 'image/jpeg',
- cacheControl: '31536000', // Cache for 1 year
- upsert: false
- })
-
- if (uploadError) {
- console.error('Failed to upload to storage:', uploadError)
- // If upload fails, still redirect to Google URL as fallback
- return Response.redirect(googlePhotoUrl, 302)
- }
-
- // Successfully uploaded, redirect to storage URL
- return Response.redirect(publicUrlData.publicUrl, 302)
+ // If key is missing or fetch fails, redirect to a random fallback image
+ const fallbackUrl = FALLBACK_PHOTO_URLS[Math.floor(Math.random() * FALLBACK_PHOTO_URLS.length)]
+ return Response.redirect(fallbackUrl, 302)
} catch (error) {
console.error('Error in get-place-photo:', error)
- return new Response(JSON.stringify({ error: error.message }), {
- status: 500,
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- })
+ // Always fallback to something visual
+ const fallbackUrl = FALLBACK_PHOTO_URLS[Math.floor(Math.random() * FALLBACK_PHOTO_URLS.length)]
+ return Response.redirect(fallbackUrl, 302)
}
-})
+})
\ No newline at end of file
diff --git a/app-9xzmfic2e4g1/vite.config.ts b/app-9xzmfic2e4g1/vite.config.ts
index d0343b7..446d916 100644
--- a/app-9xzmfic2e4g1/vite.config.ts
+++ b/app-9xzmfic2e4g1/vite.config.ts
@@ -15,4 +15,9 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
-});
+ server: {
+ host: '0.0.0.0',
+ port: 3001,
+ allowedHosts: ['.dev.flatlogic.app', '.dev.appwizzy.dev', 'localhost', '127.0.0.1']
+ }
+});
\ No newline at end of file
diff --git a/assets/pasted-20260301-145131-09fe8a2c.jpg b/assets/pasted-20260301-145131-09fe8a2c.jpg
new file mode 100644
index 0000000..f94be92
--- /dev/null
+++ b/assets/pasted-20260301-145131-09fe8a2c.jpg
@@ -0,0 +1,9 @@
+
+
+
403 Forbidden
+
+
Forbidden
+
You don't have permission to access this resource.
+
+
Apache/2.4.66 (Debian) Server at app-9xzmfic2e4g1appversion-9y0h01so4s8w-273b.dev.appwizzy.dev Port 80
+
diff --git a/assets/pasted-20260301-171654-a321b828.png b/assets/pasted-20260301-171654-a321b828.png
new file mode 100644
index 0000000..cab45ec
Binary files /dev/null and b/assets/pasted-20260301-171654-a321b828.png differ
diff --git a/assets/vm-shot-2026-03-01T14-49-50-527Z.jpg b/assets/vm-shot-2026-03-01T14-49-50-527Z.jpg
new file mode 100644
index 0000000..7197d9d
Binary files /dev/null and b/assets/vm-shot-2026-03-01T14-49-50-527Z.jpg differ
diff --git a/assets/vm-shot-2026-03-01T17-15-38-914Z.jpg b/assets/vm-shot-2026-03-01T17-15-38-914Z.jpg
new file mode 100644
index 0000000..cd63053
Binary files /dev/null and b/assets/vm-shot-2026-03-01T17-15-38-914Z.jpg differ
diff --git a/db/config.php b/db/config.php
new file mode 100644
index 0000000..6d1c4fb
--- /dev/null
+++ b/db/config.php
@@ -0,0 +1,17 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ ]);
+ }
+ return $pdo;
+}