Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions javascriptapp/cities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
"London",
"Pinehurst",
"New York",
"Tokyo",
"Paris",
"Berlin",
"Sydney",
"Moscow",
"Cairo",
"Rio de Janeiro",
"Beijing",
"Los Angeles",
"Chicago",
"Toronto",
"Mexico City",
"Mumbai"
]

140 changes: 101 additions & 39 deletions javascriptapp/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<title>My Weather App</title>
<style>
/* ... (keep existing styles from previous dropdown example) ... */
body {
font-family: 'Segoe UI', system-ui, sans-serif;
margin: 0;
Expand Down Expand Up @@ -41,35 +42,45 @@
.form-group {
margin-bottom: 1.5rem;
transition: all 0.3s ease;
text-align: center; /* Center the dropdown */
}

input {
width: 200px;
padding: 8px;
border-radius: 4px;
/* --- Style the select dropdown --- */
select {
width: 250px; /* Adjust width as needed */
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #ccc;
}
button {
padding: 8px 12px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
font-size: 1rem;
cursor: pointer;
background-color: white;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007bff%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 12px 12px;
}
button:hover {
background: #0056b3;
select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.weather-container {
text-align: center;
margin-top: 1.5rem;
}
#weatherIcon {
width: 100px;
height: 100px;
margin-top: 10px;
}
.error {
color: red;
margin-top: 10px;
font-weight: bold;
}
</style>
</head>
Expand All @@ -79,8 +90,12 @@
<h1>My Weather App</h1>

<div class="form-group">
<input type="text" id="cityInput" placeholder="Enter a city..." />
<button id="getWeatherBtn">Get Weather</button>
<!-- Replace input and button with a select dropdown -->
<label for="citySelect" style="display: none;">Select City:</label>
<select id="citySelect">
<option value="">-- Select a City --</option>
<!-- Options will be populated by JavaScript -->
</select>
</div>

<div class="weather-container">
Expand All @@ -95,9 +110,8 @@ <h3 id="weatherTitle"></h3>
</div>

<script>
const getWeatherBtn = document.getElementById('getWeatherBtn');
const cityInput = document.getElementById('cityInput');

// Get references to DOM elements
const citySelect = document.getElementById('citySelect'); // Use the select element
const weatherTitle = document.getElementById('weatherTitle');
const weatherIcon = document.getElementById('weatherIcon');
const weatherDescription = document.getElementById('weatherDescription');
Expand All @@ -106,50 +120,98 @@ <h3 id="weatherTitle"></h3>
const windSpeed = document.getElementById('windSpeed');
const errorMessage = document.getElementById('errorMessage');

getWeatherBtn.addEventListener('click', async () => {
const city = cityInput.value.trim();
if (!city) {
errorMessage.textContent = "Please enter a city!";
return;
}

// Clear previous content
// Function to clear weather display
function clearWeatherData() {
weatherTitle.textContent = "";
weatherIcon.src = "";
weatherIcon.alt = "";
weatherIcon.style.display = "none";
weatherDescription.textContent = "";
temperature.textContent = "";
feelsLike.textContent = "";
windSpeed.textContent = "";
errorMessage.textContent = "";
}

// Function to fetch and display weather
async function fetchAndDisplayWeather(city) {
clearWeatherData();

if (!city) { // Don't fetch if "-- Select a City --" is chosen
return;
}

console.log(`Fetching weather for selected city: ${city}`);

try {
// Request weather from our own server endpoint
const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
const data = await response.json();

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Error fetching weather');
throw new Error(data.error || `Error ${response.status}: Failed to fetch weather`);
}

const data = await response.json();

// Display weather data
weatherTitle.textContent = `Weather in ${data.name}`;
weatherDescription.textContent = `Condition: ${data.description}`;
temperature.textContent = `Temperature: ${data.temp} °F`;
feelsLike.textContent = `Feels like: ${data.feels_like} °F`;
windSpeed.textContent = `Wind speed: ${data.wind_speed} mph`;
temperature.textContent = `Temperature: ${data.temp !== undefined ? data.temp + ' °F' : 'N/A'}`;
feelsLike.textContent = `Feels like: ${data.feels_like !== undefined ? data.feels_like + ' °F' : 'N/A'}`;
windSpeed.textContent = `Wind speed: ${data.wind_speed !== undefined ? data.wind_speed + ' mph' : 'N/A'}`;

// Use OpenWeatherMap icon if available
// Icon base URL: https://openweathermap.org/img/wn/{ICON_CODE}@2x.png
if (data.icon) {
weatherIcon.src = `https://openweathermap.org/img/wn/${data.icon}@2x.png`;
weatherIcon.alt = data.description || "Weather Icon";
weatherIcon.style.display = "inline-block";
weatherIcon.style.display = "inline-block";
} else {
weatherIcon.style.display = "none";
}
} catch (error) {
console.error("Error fetching or displaying weather:", error);
errorMessage.textContent = error.message || "Could not fetch weather data.";
clearWeatherData();
weatherTitle.textContent = `Could not load weather for ${city}`;
}
}

// Function to populate the city dropdown
async function populateCityDropdown() {
try {
// Fetch the list from the server's new endpoint
const response = await fetch('/api/cities');
if (!response.ok) {
throw new Error(`Could not fetch city list (${response.status})`);
}
const cities = await response.json();

// Clear any existing options except the placeholder
citySelect.innerHTML = '<option value="">-- Select a City --</option>';

cities.forEach(city => {
const option = document.createElement('option');
option.value = city;
option.textContent = city;
citySelect.appendChild(option);
});

} catch (error) {
errorMessage.textContent = error.message;
console.error("Error populating cities:", error);
errorMessage.textContent = "Could not load city list from server.";
// Optionally disable the dropdown
// citySelect.disabled = true;
}
}

// Event Listener for Dropdown Change
citySelect.addEventListener('change', (event) => {
const selectedCity = event.target.value;
fetchAndDisplayWeather(selectedCity); // Fetch weather for the selected city
});

// Initial setup when the page loads
document.addEventListener('DOMContentLoaded', () => {
populateCityDropdown(); // Fetch cities from server and populate dropdown
clearWeatherData(); // Ensure weather display is initially empty
});

</script>

</body>
Expand Down
85 changes: 61 additions & 24 deletions javascriptapp/server.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,90 @@
// /home/rob/repos/example-projects/javascriptapp/server.js
require('dotenv').config();
const express = require('express');
// const fetch = require('node-fetch'); // If on Node <=17; on Node 18+ you can use the built-in fetch
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = process.env.PORT || 3000;
const PORT = process.env.PORT || 3000; // Ensure PORT is defined before use
const API_KEY = process.env.OPENWEATHER_API_KEY;

// Serve static files from the "public" folder
// --- Load Available Cities ---
let availableCities = [];
try {
const citiesJsonPath = path.join(__dirname, 'cities.json');
const citiesJsonData = fs.readFileSync(citiesJsonPath, 'utf8');
availableCities = JSON.parse(citiesJsonData);
console.log(`Successfully loaded ${availableCities.length} cities from cities.json`);
} catch (err) { // Branch 1 (covered by 'Server Startup Error Handling' tests)
console.error("Error reading or parsing cities.json:", err);
availableCities = ['London', 'New York'];
console.warn("Using fallback city list.");
} // End Branch 1

// --- Middleware and Routes ---
app.use(express.static(path.join(__dirname, 'public')));

// GET /api/weather?city=London
app.get('/api/cities', (req, res) => {
res.json(availableCities);
});

app.get('/api/weather', async (req, res) => {
try {
try { // Branch 2 (covered by successful weather tests)
const city = req.query.city;
if (!city) {
if (!city) { // Branch 3 (covered by missing city param test)
return res.status(400).json({ error: 'Missing city parameter' });
}
} // End Branch 3

const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=imperial`;
console.log(`Fetching weather for ${city} from: ${url}`);
const response = await fetch(url);
if (!response.ok) {
return res.status(response.status).json({ error: 'Failed to fetch weather data' });
}

if (!response.ok) { // Branch 4 (covered by fetch mocking tests for non-OK)
let errorData = { message: 'Failed to fetch weather data' };
try { // Branch 5 (covered by 404 fetch mock test)
const owmError = await response.json();
errorData.message = owmError.message || errorData.message; // Branch 6 (short-circuit logic)
} catch (parseError) { // Branch 7 (covered by 503 JSON parse fail test)
errorData.message = response.statusText || errorData.message; // Branch 8 (short-circuit logic)
} // End Branch 7 & 5
console.error(`Error fetching from OpenWeatherMap: ${response.status} - ${errorData.message}`);
return res.status(response.status).json({ error: errorData.message });
} // End Branch 4

const data = await response.json();
// Return relevant fields (including icon, feels_like, wind speed)
// Optional chaining provides implicit branches, usually covered if the properties exist/don't exist
res.json({
name: data.name,
description: data.weather?.[0]?.description || '',
description: data.weather?.[0]?.description || '', // Branch 9 & 10 (short-circuit)
temp: data.main?.temp,
feels_like: data.main?.feels_like,
wind_speed: data.wind?.speed,
icon: data.weather?.[0]?.icon
});
} catch (err) {
console.error(err);
} catch (err) { // Branch 11 (covered by fetch network error test)
console.error('Server error in /api/weather:', err);
res.status(500).json({ error: 'Server error' });
}
} // End Branch 11 & 2
});

// If this file is run directly, start the server.
/* istanbul ignore next */
if (require.main === module) {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
// --- START: Refactored Server Start Logic ---
// Function to start the server listening
function startServer(port) {
// Make sure to use the 'app' instance defined above
const server = app.listen(port, () => { // Callback is Branch 12 (covered by 'Server Starting Function' test)
console.log(`Server running on http://localhost:${port}`); // Line inside Branch 12
}); // End Branch 12
return server; // Return the server instance (useful for closing in tests if needed)
}

// Export app for testing
module.exports = app;
// If this file is run directly, call the start function.
// Keep the ignore comment for the condition itself if desired.
/* istanbul ignore if */ // Ignores the 'if' condition itself (Branch 13)
if (require.main === module) { // Branch 13 (ignored)
startServer(PORT); // Call the extracted function
} // End Branch 13
// --- END: Refactored Server Start Logic ---


// Export the app for testing AND the startServer function
module.exports = { app, startServer, PORT }; // Export PORT as well for convenience in tests
Loading