Auto commit: 2025-12-02T15:20:58.075Z

This commit is contained in:
Flatlogic Bot 2025-12-02 15:20:58 +00:00
parent e06d66bb12
commit 5ad904b265
6 changed files with 560 additions and 223 deletions

View File

@ -3,24 +3,39 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); ini_set('display_startup_errors', 1);
error_reporting(E_ALL); error_reporting(E_ALL);
session_start();
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once __DIR__ . '/../ai/LocalAIApi.php'; require_once __DIR__ . '/../ai/LocalAIApi.php';
require_once __DIR__ . '/../includes/pexels.php'; require_once __DIR__ . '/../includes/pexels.php';
require_once __DIR__ . '/../db/config.php';
$action = $_GET['action'] ?? 'get_suggestion';
switch ($action) {
case 'get_suggestion':
handle_get_suggestion();
break;
case 'save_suggestion':
handle_save_suggestion();
break;
default:
echo json_encode(['error' => 'Invalid action']);
break;
}
function handle_get_suggestion() {
$requestBody = file_get_contents('php://input'); $requestBody = file_get_contents('php://input');
$data = json_decode($requestBody, true); $data = json_decode($requestBody, true);
$query = $data['query'] ?? ''; $query = $data['query'] ?? '';
$persona = $data['persona'] ?? 'travel_agent'; $persona = $data['persona'] ?? 'travel_agent';
$conversation = $data['conversation'] ?? [];
if (empty($query)) { if (empty($query)) {
echo json_encode(['error' => 'Query is empty']); echo json_encode(['error' => 'Query is empty']);
exit; exit;
} }
$conversation = $_SESSION['conversation'] ?? [];
$conversation[] = ['role' => 'user', 'content' => $query]; $conversation[] = ['role' => 'user', 'content' => $query];
$persona_prompts = [ $persona_prompts = [
@ -30,7 +45,7 @@ $persona_prompts = [
'adventurer' => 'You are an adventure travel guide who loves exploring the wild side of Poland. Your responses should be exciting, energetic, and focus on outdoor activities, hiking, and unique experiences.' 'adventurer' => 'You are an adventure travel guide who loves exploring the wild side of Poland. Your responses should be exciting, energetic, and focus on outdoor activities, hiking, and unique experiences.'
]; ];
$system_prompt = $persona_prompts[$persona] . ' The user is looking for: ' . $query . '. Provide a travel suggestion with a title, a short description, a 3-day itinerary, and the latitude and longitude of the location. Your response must be in JSON format, like this: {"title": "...", "description": "...", "itinerary": [{"day": 1, "activities": ["...", "..."]}, {"day": 2, "activities": ["...", "..."]}, {"day": 3, "activities": ["...", "..."]}], "location": {"lat": ..., "lng": ...}}'; $system_prompt = $persona_prompts[$persona] . ' Provide a travel suggestion with a title, a short description, a 3-day itinerary, and the latitude and longitude of the location. Your response must be in JSON format, like this: {"title": "...", "description": "...", "itinerary": [{"day": 1, "activities": ["...", "..."]}, {"day": 2, "activities": ["...", "..."]}, {"day": 3, "activities": ["...", "..."]}], "location": {"lat": ..., "lng": ...}}. The user is on a conversation, so answer the last query based on the context of the conversation.';
$messages = array_merge([['role' => 'system', 'content' => $system_prompt]], $conversation); $messages = array_merge([['role' => 'system', 'content' => $system_prompt]], $conversation);
@ -45,11 +60,12 @@ if (!empty($resp['success'])) {
$aiResponse = json_decode($text, true); $aiResponse = json_decode($text, true);
if (json_last_error() !== JSON_ERROR_NONE) { if (json_last_error() !== JSON_ERROR_NONE) {
// If the response is not a valid JSON, we try to extract the title and description manually preg_match('/"title":\s*"(.*?)"/', $text, $title_matches);
// This is a fallback mechanism preg_match('/"description":\s*"(.*?)"/', $text, $description_matches);
$title = "AI Generated Response"; $title = $title_matches[1] ?? 'AI Generated Response';
$description = $text; $description = $description_matches[1] ?? $text;
$itinerary = []; $itinerary = [];
$location = null;
} else { } else {
$title = $aiResponse['title'] ?? 'AI Generated Response'; $title = $aiResponse['title'] ?? 'AI Generated Response';
$description = $aiResponse['description'] ?? 'No description available.'; $description = $aiResponse['description'] ?? 'No description available.';
@ -57,13 +73,10 @@ if (!empty($resp['success'])) {
$location = $aiResponse['location'] ?? null; $location = $aiResponse['location'] ?? null;
} }
$conversation[] = ['role' => 'assistant', 'content' => $text];
$_SESSION['conversation'] = $conversation;
$pexelsUrl = 'https://api.pexels.com/v1/search?query=' . urlencode($title) . '&orientation=landscape&per_page=1&page=1'; $pexelsUrl = 'https://api.pexels.com/v1/search?query=' . urlencode($title) . '&orientation=landscape&per_page=1&page=1';
$pexelsData = pexels_get($pexelsUrl); $pexelsData = pexels_get($pexelsUrl);
$imageUrl = 'https://images.pexels.com/photos/1699030/pexels-photo-1699030.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'; // Default image $imageUrl = 'https://images.pexels.com/photos/1699030/pexels-photo-1699030.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1';
if ($pexelsData && !empty($pexelsData['photos'])) { if ($pexelsData && !empty($pexelsData['photos'])) {
$photo = $pexelsData['photos'][0]; $photo = $pexelsData['photos'][0];
$imageUrl = $photo['src']['large2x'] ?? ($photo['src']['large'] ?? $photo['src']['original']); $imageUrl = $photo['src']['large2x'] ?? ($photo['src']['large'] ?? $photo['src']['original']);
@ -80,3 +93,39 @@ if (!empty($resp['success'])) {
error_log('AI error: ' . ($resp['error'] ?? 'unknown')); error_log('AI error: ' . ($resp['error'] ?? 'unknown'));
echo json_encode(['error' => 'Failed to get AI response']); echo json_encode(['error' => 'Failed to get AI response']);
} }
}
function handle_save_suggestion() {
$requestBody = file_get_contents('php://input');
$data = json_decode($requestBody, true);
$title = $data['title'] ?? null;
$description = $data['description'] ?? null;
$itinerary = $data['itinerary'] ?? null;
$location = $data['location'] ?? null;
$image_url = $data['image'] ?? null;
if (empty($title)) {
echo json_encode(['error' => 'Title is required to save a suggestion.']);
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("
INSERT INTO saved_suggestions (title, description, itinerary, location, image_url)
VALUES (:title, :description, :itinerary, :location, :image_url)
");
$stmt->execute([
':title' => $title,
':description' => $description,
':itinerary' => json_encode($itinerary),
':location' => json_encode($location),
':image_url' => $image_url
]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
} catch (PDOException $e) {
error_log('DB error: ' . $e->getMessage());
echo json_encode(['error' => 'Failed to save suggestion to the database.']);
}
}

View File

@ -68,3 +68,57 @@ body {
.spinner-border { .spinner-border {
color: #DB2777 !important; color: #DB2777 !important;
} }
#chat-container {
max-height: 600px;
overflow-y: auto;
padding: 1rem;
background-color: #fff;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.message-wrapper {
display: flex;
margin-bottom: 1rem;
}
.user-wrapper {
justify-content: flex-end;
}
.assistant-wrapper {
justify-content: flex-start;
}
.message-bubble {
max-width: 80%;
padding: 1rem;
border-radius: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.user-bubble {
background-color: #DB2777;
color: white;
border-top-right-radius: 0;
}
.assistant-bubble {
background-color: #f1f0f0;
border-top-left-radius: 0;
}
.assistant-bubble .card-body {
padding: 0;
}
.assistant-bubble img {
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
max-width: 100%;
}
.itinerary-section {
margin-top: 1rem;
}

View File

@ -1,23 +1,18 @@
document.getElementById('suggestion-form').addEventListener('submit', function(e) {
e.preventDefault(); document.addEventListener('DOMContentLoaded', () => {
const query = document.getElementById('query').value; const suggestionForm = document.getElementById('suggestion-form');
const persona = document.getElementById('persona').value; const queryInput = document.getElementById('query');
const suggestionCard = document.getElementById('suggestion-card'); const personaSelect = document.getElementById('persona');
const loading = document.getElementById('loading'); const loadingIndicator = document.getElementById('loading');
const errorMessage = document.getElementById('error-message'); const errorMessage = document.getElementById('error-message');
const errorText = document.getElementById('error-text'); const errorText = document.getElementById('error-text');
const suggestionImage = document.getElementById('suggestion-image'); const chatContainer = document.getElementById('chat-container');
const suggestionTitle = document.getElementById('suggestion-title'); const luckyButton = document.getElementById('lucky-button');
const suggestionText = document.getElementById('suggestion-text');
const funFact = document.getElementById('fun-fact'); const funFact = document.getElementById('fun-fact');
const historySection = document.getElementById('history-section');
const historyList = document.getElementById('history-list');
const itinerarySection = document.getElementById('itinerary-section');
const itineraryList = document.getElementById('itinerary-list');
const mapElement = document.getElementById('map');
const suggestionHistory = []; let conversation = [];
let map; let map;
let funFactInterval;
const funFacts = [ const funFacts = [
"Poland is home to the world's largest castle, Malbork Castle.", "Poland is home to the world's largest castle, Malbork Castle.",
@ -30,140 +25,22 @@ document.getElementById('suggestion-form').addEventListener('submit', function(e
"Poland's Białowieża Forest is one of the last and largest remaining parts of the immense primeval forest that once stretched across the European Plain." "Poland's Białowieża Forest is one of the last and largest remaining parts of the immense primeval forest that once stretched across the European Plain."
]; ];
let funFactInterval; suggestionForm.addEventListener('submit', function(e) {
suggestionCard.style.display = 'none';
errorMessage.style.display = 'none';
loading.style.display = 'block';
mapElement.style.display = 'none';
function showRandomFunFact() {
const randomIndex = Math.floor(Math.random() * funFacts.length);
funFact.textContent = funFacts[randomIndex];
}
showRandomFunFact();
funFactInterval = setInterval(showRandomFunFact, 3000);
function showError(message) {
errorText.textContent = message;
errorMessage.style.display = 'block';
loading.style.display = 'none';
clearInterval(funFactInterval);
}
function updateHistory() {
historyList.innerHTML = '';
suggestionHistory.slice(0, 5).forEach(item => {
const a = document.createElement('a');
a.href = '#';
a.className = 'list-group-item list-group-item-action';
a.textContent = item.title;
a.onclick = (e) => {
e.preventDefault(); e.preventDefault();
suggestionImage.src = item.image; const query = queryInput.value.trim();
suggestionTitle.textContent = item.title; if (!query) return;
suggestionText.textContent = item.description;
if (item.itinerary) { const persona = personaSelect.value;
itineraryList.innerHTML = ''; const userMessage = { role: 'user', content: query };
item.itinerary.forEach(day => { conversation.push(userMessage);
const li = document.createElement('li');
li.className = 'list-group-item';
li.innerHTML = `<b>Day ${day.day}:</b> ${day.activities.join(', ')}`;
itineraryList.appendChild(li);
});
itinerarySection.style.display = 'block';
} else {
itinerarySection.style.display = 'none';
}
if (item.location && item.location.lat && item.location.lng) { appendMessage('user', { content: query });
mapElement.style.display = 'block'; queryInput.value = '';
if (!map) {
map = L.map('map').setView([item.location.lat, item.location.lng], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
} else {
map.setView([item.location.lat, item.location.lng], 13);
}
L.marker([item.location.lat, item.location.lng]).addTo(map)
.bindPopup(item.title)
.openPopup();
} else {
mapElement.style.display = 'none';
}
suggestionCard.style.display = 'block'; fetchSuggestions(persona);
};
historyList.appendChild(a);
});
historySection.style.display = suggestionHistory.length > 0 ? 'block' : 'none';
}
fetch('api/index.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query: query, persona: persona })
})
.then(response => response.json())
.then(data => {
clearInterval(funFactInterval);
if (data.error) {
showError(data.error);
return;
}
suggestionImage.src = data.image;
suggestionTitle.textContent = data.title;
suggestionText.textContent = data.description;
if (data.itinerary) {
itineraryList.innerHTML = '';
data.itinerary.forEach(day => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.innerHTML = `<b>Day ${day.day}:</b> ${day.activities.join(', ')}`;
itineraryList.appendChild(li);
});
itinerarySection.style.display = 'block';
} else {
itinerarySection.style.display = 'none';
}
if (data.location && data.location.lat && data.location.lng) {
mapElement.style.display = 'block';
if (!map) {
map = L.map('map').setView([data.location.lat, data.location.lng], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
} else {
map.setView([data.location.lat, data.location.lng], 13);
}
L.marker([data.location.lat, data.location.lng]).addTo(map)
.bindPopup(data.title)
.openPopup();
} else {
mapElement.style.display = 'none';
}
suggestionHistory.unshift(data);
updateHistory();
loading.style.display = 'none';
suggestionCard.style.display = 'block';
})
.catch(error => {
console.error('Error:', error);
showError('An error occurred while fetching the suggestion.');
});
}); });
document.getElementById('lucky-button').addEventListener('click', function() { luckyButton.addEventListener('click', function() {
const queries = [ const queries = [
"a hidden gem in Warsaw", "a hidden gem in Warsaw",
"the best pierogi in Krakow", "the best pierogi in Krakow",
@ -172,14 +49,189 @@ document.getElementById('lucky-button').addEventListener('click', function() {
"a quirky museum in Gdansk" "a quirky museum in Gdansk"
]; ];
const randomQuery = queries[Math.floor(Math.random() * queries.length)]; const randomQuery = queries[Math.floor(Math.random() * queries.length)];
document.getElementById('query').value = randomQuery; queryInput.value = randomQuery;
document.getElementById('suggestion-form').dispatchEvent(new Event('submit')); suggestionForm.dispatchEvent(new Event('submit'));
}); });
// Optional: Add event listener to close the error message function showRandomFunFact() {
const randomIndex = Math.floor(Math.random() * funFacts.length);
funFact.textContent = funFacts[randomIndex];
}
function startLoading() {
loadingIndicator.style.display = 'block';
showRandomFunFact();
funFactInterval = setInterval(showRandomFunFact, 3000);
}
function stopLoading() {
loadingIndicator.style.display = 'none';
clearInterval(funFactInterval);
}
function showError(message) {
errorText.textContent = message;
errorMessage.style.display = 'block';
}
function hideError() {
errorMessage.style.display = 'none';
}
function appendMessage(role, data) {
const messageWrapper = document.createElement('div');
messageWrapper.classList.add('message-wrapper', `${role}-wrapper`);
const messageBubble = document.createElement('div');
messageBubble.classList.add('message-bubble', `${role}-bubble`);
if (role === 'user') {
messageBubble.textContent = data.content;
} else {
if (data.image) {
const img = document.createElement('img');
img.src = data.image;
img.classList.add('card-img-top');
messageBubble.appendChild(img);
}
const cardBody = document.createElement('div');
cardBody.classList.add('card-body');
if (data.title) {
const title = document.createElement('h5');
title.classList.add('card-title');
title.textContent = data.title;
cardBody.appendChild(title);
}
if (data.description) {
const description = document.createElement('p');
description.classList.add('card-text');
description.innerHTML = data.description; // Using innerHTML to render line breaks
cardBody.appendChild(description);
}
if (data.itinerary && data.itinerary.length > 0) {
const itinerarySection = document.createElement('div');
itinerarySection.classList.add('itinerary-section');
const itineraryTitle = document.createElement('h6');
itineraryTitle.textContent = 'Itinerary';
itinerarySection.appendChild(itineraryTitle);
const itineraryList = document.createElement('ul');
itineraryList.classList.add('list-group');
data.itinerary.forEach(day => {
const li = document.createElement('li');
li.classList.add('list-group-item');
li.innerHTML = `<b>Day ${day.day}:</b> ${day.activities.join(', ')}`;
itineraryList.appendChild(li);
});
itinerarySection.appendChild(itineraryList);
cardBody.appendChild(itinerarySection);
}
if (data.location && data.location.lat && data.location.lng) {
const mapId = `map-${Date.now()}`;
const mapDiv = document.createElement('div');
mapDiv.id = mapId;
mapDiv.style.height = '300px';
mapDiv.classList.add('mt-3');
cardBody.appendChild(mapDiv);
// Use a timeout to ensure the div is in the DOM before initializing the map
setTimeout(() => {
const map = L.map(mapId).setView([data.location.lat, data.location.lng], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
L.marker([data.location.lat, data.location.lng]).addTo(map)
.bindPopup(data.title || 'Location')
.openPopup();
}, 100);
}
messageBubble.appendChild(cardBody);
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
saveButton.classList.add('btn', 'btn-sm', 'btn-outline-primary', 'mt-2');
saveButton.addEventListener('click', () => saveSuggestion(data));
cardBody.appendChild(saveButton);
}
messageWrapper.appendChild(messageBubble);
chatContainer.appendChild(messageWrapper);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function saveSuggestion(suggestionData) {
fetch('api/index.php?action=save_suggestion', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(suggestionData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Suggestion saved!');
} else {
showError(data.error || 'Could not save suggestion.');
}
})
.catch(error => {
console.error('Error:', error);
showError('An error occurred while saving the suggestion.');
});
}
function fetchSuggestions(persona) {
startLoading();
hideError();
fetch('api/index.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: conversation[conversation.length - 1].content,
persona: persona,
conversation: conversation.slice(0, -1) // Send previous conversation
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
stopLoading();
if (data.error) {
showError(data.error);
// Do not add error to conversation history
return;
}
const assistantMessage = { role: 'assistant', content: data };
conversation.push(assistantMessage);
appendMessage('assistant', data);
})
.catch(error => {
console.error('Error:', error);
stopLoading();
showError('An error occurred while fetching the suggestion. Please try again.');
});
}
const errorAlert = document.getElementById('error-message'); const errorAlert = document.getElementById('error-message');
if (errorAlert) { if (errorAlert) {
errorAlert.querySelector('.btn-close').addEventListener('click', function () { errorAlert.querySelector('.btn-close').addEventListener('click', function () {
errorAlert.style.display = 'none'; hideError();
}); });
} }
});

21
db/migrate.php Normal file
View File

@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
$pdo->exec("
CREATE TABLE IF NOT EXISTS saved_suggestions (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
itinerary JSON,
location JSON,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
echo "Migration successful!\n";
} catch (PDOException $e) {
die("Migration failed: " . $e->getMessage() . "\n");
}

View File

@ -11,6 +11,7 @@
<div class="container"> <div class="container">
<h1>Your Next Adventure Awaits</h1> <h1>Your Next Adventure Awaits</h1>
<p class="lead">Discover Poland with AI.</p> <p class="lead">Discover Poland with AI.</p>
<a href="saved.php" class="btn btn-light">View Saved Suggestions</a>
</div> </div>
</div> </div>
@ -54,24 +55,7 @@
<p>Thinking...</p> <p>Thinking...</p>
<p id="fun-fact" class="text-muted mt-2"></p> <p id="fun-fact" class="text-muted mt-2"></p>
</div> </div>
<div class="card" id="suggestion-card"> <div id="chat-container" class="mt-4"></div>
<img id="suggestion-image" src="" class="card-img-top" alt="Suggestion Image">
<div class="card-body">
<h5 class="card-title" id="suggestion-title"></h5>
<p class="card-text" id="suggestion-text"></p>
<div id="itinerary-section" style="display: none;">
<h6>Itinerary</h6>
<ul id="itinerary-list" class="list-group"></ul>
</div>
</div>
</div>
<div id="map" style="height: 400px; display: none;" class="mt-4"></div>
<div id="history-section" class="mt-4" style="display: none;">
<h4>History</h4>
<div id="history-list" class="list-group"></div>
</div>
</div> </div>
</div> </div>
</div> </div>

177
saved.php Normal file
View File

@ -0,0 +1,177 @@
<?php
require_once __DIR__ . '/db/config.php';
$pdo = db();
$single_suggestion = null;
if (isset($_GET['id'])) {
$stmt = $pdo->prepare("SELECT * FROM saved_suggestions WHERE id = ?");
$stmt->execute([$_GET['id']]);
$single_suggestion = $stmt->fetch();
}
$stmt = $pdo->query("SELECT * FROM saved_suggestions ORDER BY created_at DESC");
$suggestions = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Saved Suggestions</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link href="assets/css/custom.css?v=<?php echo time(); ?>" rel="stylesheet">
<style>
.suggestion-card {
margin-bottom: 2rem;
}
</style>
</head>
<body>
<div class="hero">
<div class="container">
<h1><?php echo $single_suggestion ? htmlspecialchars($single_suggestion['title']) : 'Your Saved Adventures'; ?></h1>
<p class="lead"><?php echo $single_suggestion ? 'A shared travel idea' : 'Revisit your favorite Polish destinations.'; ?></p>
<a href="/" class="btn btn-light">Home</a>
</div>
</div>
<div class="container mt-5">
<div class="row <?php echo $single_suggestion ? 'justify-content-center' : ''; ?>">
<?php if ($single_suggestion): ?>
<div class="col-md-8">
<div class="card suggestion-card">
<img src="<?php echo htmlspecialchars($single_suggestion['image_url']); ?>" class="card-img-top" alt="<?php echo htmlspecialchars($single_suggestion['title']); ?>">
<div class="card-body">
<h5 class="card-title"><?php echo htmlspecialchars($single_suggestion['title']); ?></h5>
<p class="card-text"><?php echo nl2br(htmlspecialchars($single_suggestion['description'])); ?></p>
<?php
$itinerary = json_decode($single_suggestion['itinerary'], true);
if ($itinerary && !empty($itinerary)):
?>
<div class="itinerary-section">
<h6>Itinerary</h6>
<ul class="list-group">
<?php foreach ($itinerary as $day): ?>
<li class="list-group-item">
<b>Day <?php echo htmlspecialchars($day['day']); ?>:</b> <?php echo htmlspecialchars(implode(', ', $day['activities'])); ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php
$location = json_decode($single_suggestion['location'], true);
if ($location && !empty($location['lat']) && !empty($location['lng'])):
?>
<div id="map-single" style="height: 400px;" class="mt-3"></div>
<?php endif; ?>
</div>
</div>
</div>
<?php elseif (empty($suggestions)):
?>
<div class="col text-center">
<p>You haven't saved any suggestions yet.</p>
<a href="/" class="btn btn-primary">Find a new adventure</a>
</div>
<?php else: ?>
<?php foreach ($suggestions as $index => $suggestion): ?>
<div class="col-md-4">
<div class="card suggestion-card">
<img src="<?php echo htmlspecialchars($suggestion['image_url']); ?>" class="card-img-top" alt="<?php echo htmlspecialchars($suggestion['title']); ?>">
<div class="card-body">
<h5 class="card-title"><?php echo htmlspecialchars($suggestion['title']); ?></h5>
<p class="card-text"><?php echo nl2br(htmlspecialchars($suggestion['description'])); ?></p>
<?php
$itinerary = json_decode($suggestion['itinerary'], true);
if ($itinerary && !empty($itinerary)):
?>
<div class="itinerary-section">
<h6>Itinerary</h6>
<ul class="list-group">
<?php foreach ($itinerary as $day): ?>
<li class="list-group-item">
<b>Day <?php echo htmlspecialchars($day['day']); ?>:</b> <?php echo htmlspecialchars(implode(', ', $day['activities'])); ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php
$location = json_decode($suggestion['location'], true);
if ($location && !empty($location['lat']) && !empty($location['lng'])):
?>
<div id="map-<?php echo $index; ?>" style="height: 200px;" class="mt-3"></div>
<?php endif; ?>
<button class="btn btn-sm btn-outline-secondary mt-2 share-btn" data-id="<?php echo $suggestion['id']; ?>">Share</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
<?php if ($single_suggestion): ?>
<?php
$location = json_decode($single_suggestion['location'], true);
if ($location && !empty($location['lat']) && !empty($location['lng'])):
?>
var mapSingle = L.map('map-single').setView([<?php echo $location['lat']; ?>, <?php echo $location['lng']; ?>], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(mapSingle);
L.marker([<?php echo $location['lat']; ?>, <?php echo $location['lng']; ?>]).addTo(mapSingle)
.bindPopup(<?php echo json_encode($single_suggestion['title']); ?>)
.openPopup();
<?php endif; ?>
<?php else: ?>
<?php foreach ($suggestions as $index => $suggestion): ?>
<?php
$location = json_decode($suggestion['location'], true);
if ($location && !empty($location['lat']) && !empty($location['lng'])):
?>
var map<?php echo $index; ?> = L.map('map-<?php echo $index; ?>').setView([<?php echo $location['lat']; ?>, <?php echo $location['lng']; ?>], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map<?php echo $index; ?>);
L.marker([<?php echo $location['lat']; ?>, <?php echo $location['lng']; ?>]).addTo(map<?php echo $index; ?>)
.bindPopup(<?php echo json_encode($suggestion['title']); ?>)
.openPopup();
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
document.querySelectorAll('.share-btn').forEach(button => {
button.addEventListener('click', function() {
const suggestionId = this.dataset.id;
const shareUrl = `${window.location.origin}/saved.php?id=${suggestionId}`;
navigator.clipboard.writeText(shareUrl).then(() => {
this.textContent = 'Copied!';
this.disabled = true;
setTimeout(() => {
this.textContent = 'Share';
this.disabled = false;
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
});
});
});
</script>
</body>
</html>