Try the app now: StyleSync Outfit Generator
Have you ever stood in front of your closet, staring blankly at your clothes while wondering what to wear? The decision becomes even more complicated when you factor in the weather. What if you could have a personal stylist that considers both the current weather conditions and your preferred style aesthetic to suggest the perfect outfit? That's exactly what we're building today: StyleSync, a smart outfit generator that takes the guesswork out of dressing well.
🌟 What We're Building
StyleSync is a web application that combines weather data with style preferences to create personalized outfit recommendations. The app will:
- Allow users to select their location using an interactive map
- Fetch real-time weather data for the selected location
- Generate outfit suggestions based on the temperature and user's style preference
- Let users save their favorite outfit combinations for future reference
By the end of this tutorial, you'll have a fully functional web application that not only looks great but also solves a practical daily problem.
🛠️ Technology Stack
For this project, we'll leverage several technologies:
- HTML5 for structuring our application
- CSS3 for creating a visually appealing and responsive design
- Vanilla JavaScript for implementing the core functionality
- Leaflet.js for the interactive map component
- OpenStreetMap API for location search and geocoding
- Open-Meteo API for weather data
No frameworks or complex build tools are required—just good old HTML, CSS, and JavaScript!
📋 Project Structure Overview
Before diving into the code, let's understand the overall structure of our application:
-
User Interface Components:
- Location search with autocomplete
- Interactive map for precise location selection
- Weather information display
- Style preference selector
- Outfit recommendation display
- Saved outfits section
-
Core Functionality:
- Location search and geocoding
- Weather data retrieval
- Outfit generation algorithm
- Local storage for saving preferences
Now, let's explore how each part of the application works.
🗺️ Setting Up the HTML Structure
Our HTML structure creates the foundation for our application. It includes all the necessary elements for user interaction and display of information.
</span>
lang="en">
charset="UTF-8">
name="viewport" content="width=device-width, initial-scale=1.0">
StyleSync - Outfit Generator
rel="stylesheet" href="styles.css">
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
class="container">
StyleSync
Your personal outfit curator
class="weather-input">
type="text" id="locationSearch" placeholder="Search for a location">
id="autocompleteResults" class="autocomplete-dropdown">
id="map" style="height: 200px;">
id="useLocation">Use Selected Location
class="weather-display" id="weatherInfo">
class="weather-card">
id="city">
id="temp">
id="condition">
class="style-selector">
id="stylePref">
value="casual">Casual
value="formal">Formal
value="sporty">Sporty
value="bohemian">Bohemian
value="business_casual">Business Casual
value="streetwear">Streetwear
value="vintage">Vintage
id="generateOutfit">Generate Outfit
class="outfit-display" id="outfitResult">
class="outfit-item" id="top">
class="outfit-item" id="bottom">
class="outfit-item" id="outerwear">
class="outfit-item" id="accessories">
class="save-section">
id="saveOutfit">Save Outfit
id="savedOutfits">
<span class="na">src="https://unpkg.com/[email protected]/dist/leaflet.js">
<span class="na">src="script.js">
Enter fullscreen mode
Exit fullscreen mode
The HTML structure includes:
A header section with the app title and tagline
A weather input section with a search box and map
A weather display area to show current conditions
A style selector dropdown and generate button
An outfit display section to show recommendations
A save section for storing favorite outfits
Each section is carefully structured to create a logical flow for the user experience. The interactive map is powered by Leaflet.js, which is included via CDN.
💅 Styling with CSS
The CSS for our application creates a clean, modern aesthetic that's both visually appealing and functional. We've used CSS to enhance the user experience with subtle animations, thoughtful spacing, and responsive design.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
color: #2c3e50;
font-size: 2.5em;
font-weight: 600;
}
.weather-input {
margin-bottom: 20px;
position: relative;
}
#locationSearch {
width: 100%;
padding: 12px 20px;
border: none;
border-radius: 25px;
font-size: 1em;
background: #f0f2f5;
margin-bottom: 10px;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.autocomplete-item {
padding: 10px 20px;
cursor: pointer;
transition: background 0.2s;
}
.autocomplete-item:hover {
background: #f0f2f5;
}
#map {
border-radius: 15px;
margin-bottom: 10px;
}
button {
padding: 12px 20px;
border: none;
border-radius: 25px;
font-size: 1em;
background: #3498db;
color: white;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
button:hover {
background: #2980b9;
transform: translateY(-2px);
}
.weather-card {
background: linear-gradient(45deg, #3498db, #2ecc71);
color: white;
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
animation: fadeIn 0.5s ease-in;
}
.style-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
select {
padding: 12px 20px;
border: none;
border-radius: 25px;
font-size: 1em;
background: #f0f2f5;
cursor: pointer;
flex: 1;
}
.outfit-display {
display: grid;
gap: 15px;
margin-bottom: 20px;
}
.outfit-item {
background: #fff;
padding: 20px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease;
}
.outfit-item:hover {
transform: translateY(-5px);
}
.save-section {
text-align: center;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@media (max-width: 600px) {
.style-selector {
flex-direction: column;
}
}
Enter fullscreen mode
Exit fullscreen mode
Some key styling features include:
A gradient background that creates a pleasing visual environment
Card-style elements with subtle shadows for a clean, modern look
Interactive elements with hover effects to provide visual feedback
Responsive design that adapts to different screen sizes
Smooth animations for state transitions
The CSS uses flexbox and grid layouts to create a responsive design that works well on both desktop and mobile devices. The color scheme is carefully chosen to create a cohesive and professional appearance.
🧠 Building the Core Functionality with JavaScript
The JavaScript code is organized into a single OutfitGenerator class that encapsulates all the functionality of our application. This object-oriented approach keeps our code organized and maintainable.
class OutfitGenerator {
constructor() {
this.weatherData = null;
this.savedOutfits = JSON.parse(localStorage.getItem('savedOutfits')) || [];
this.map = null;
this.marker = null;
this.initMap();
this.initEventListeners();
this.loadSavedOutfits();
}
initMap() {
this.map = L.map('map').setView([51.505, -0.09], 13); // Default to London
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this.map);
this.marker = L.marker([51.505, -0.09]).addTo(this.map);
this.map.on('click', (e) => {
this.marker.setLatLng(e.latlng);
});
}
initEventListeners() {
document.getElementById('useLocation').addEventListener('click', () => this.fetchWeather());
document.getElementById('generateOutfit').addEventListener('click', () => this.generateOutfit());
document.getElementById('saveOutfit').addEventListener('click', () => this.saveCurrentOutfit());
const searchInput = document.getElementById('locationSearch');
searchInput.addEventListener('input', () => this.searchLocation(searchInput.value));
searchInput.addEventListener('focus', () => this.showAutocomplete());
}
async searchLocation(query) {
if (query.length < 3) {
this.hideAutocomplete();
return;
}
try {
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5`);
const results = await response.json();
this.displayAutocomplete(results);
} catch (error) {
console.error('Error searching location:', error);
}
}
displayAutocomplete(results) {
const autocompleteDiv = document.getElementById('autocompleteResults');
autocompleteDiv.innerHTML = '';
autocompleteDiv.style.display = 'block';
results.forEach(result => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.textContent = result.display_name;
item.addEventListener('click', () => {
const lat = parseFloat(result.lat);
const lon = parseFloat(result.lon);
this.map.setView([lat, lon], 13);
this.marker.setLatLng([lat, lon]);
this.hideAutocomplete();
document.getElementById('locationSearch').value = result.display_name;
});
autocompleteDiv.appendChild(item);
});
}
showAutocomplete() {
const autocompleteDiv = document.getElementById('autocompleteResults');
if (autocompleteDiv.children.length > 0) {
autocompleteDiv.style.display = 'block';
}
}
hideAutocomplete() {
document.getElementById('autocompleteResults').style.display = 'none';
}
async fetchWeather() {
const { lat, lng } = this.marker.getLatLng();
try {
const weatherResponse = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,weather_code`);
this.weatherData = await weatherResponse.json();
const geocodeResponse = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`);
const geocodeData = await geocodeResponse.json();
this.weatherData.city = geocodeData.address.city || geocodeData.address.town || 'Selected Location';
this.displayWeather();
} catch (error) {
alert('Error fetching weather data: ' + error.message);
}
}
displayWeather() {
const weatherInfo = document.getElementById('weatherInfo');
weatherInfo.style.display = 'block';
const temp = this.weatherData.current.temperature_2m;
const weatherCode = this.weatherData.current.weather_code;
document.getElementById('city').textContent = this.weatherData.city;
document.getElementById('temp').textContent = `${temp}°C`;
document.getElementById('condition').textContent = this.getWeatherCondition(weatherCode);
}
getWeatherCondition(code) {
const conditions = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Fog',
51: 'Light drizzle',
61: 'Light rain',
63: 'Rain',
65: 'Heavy rain',
71: 'Light snow',
73: 'Snow',
75: 'Heavy snow'
};
return conditions[code] || 'Unknown';
}
generateOutfit() {
const style = document.getElementById('stylePref').value;
const temp = this.weatherData ? this.weatherData.current.temperature_2m : 20;
const outfits = {
casual: {
cold: { top: 'Sweatshirt', bottom: 'Jeans', outerwear: 'Puffer Jacket', accessories: 'Beanie' },
mild: { top: 'T-shirt', bottom: 'Chinos', outerwear: 'Cardigan', accessories: 'Watch' },
hot: { top: 'Tank Top', bottom: 'Shorts', outerwear: 'None', accessories: 'Sunglasses' }
},
formal: {
cold: { top: 'Dress Shirt', bottom: 'Slacks', outerwear: 'Overcoat', accessories: 'Scarf' },
mild: { top: 'Button-up', bottom: 'Trousers', outerwear: 'Blazer', accessories: 'Tie' },
hot: { top: 'Polo', bottom: 'Light Trousers', outerwear: 'None', accessories: 'Pocket Square' }
},
sporty: {
cold: { top: 'Hoodie', bottom: 'Joggers', outerwear: 'Windbreaker', accessories: 'Cap' },
mild: { top: 'Tech Tee', bottom: 'Track Pants', outerwear: 'Light Jacket', accessories: 'Sports Watch' },
hot: { top: 'Sleeveless Tee', bottom: 'Athletic Shorts', outerwear: 'None', accessories: 'Headband' }
},
bohemian: {
cold: { top: 'Knit Sweater', bottom: 'Maxi Skirt', outerwear: 'Poncho', accessories: 'Wide Hat' },
mild: { top: 'Flowy Blouse', bottom: 'Wide Pants', outerwear: 'Kimono', accessories: 'Layered Necklaces' },
hot: { top: 'Crop Top', bottom: 'Flowy Skirt', outerwear: 'None', accessories: 'Anklet' }
},
business_casual: {
cold: { top: 'Sweater', bottom: 'Dress Pants', outerwear: 'Trench Coat', accessories: 'Leather Belt' },
mild: { top: 'Oxford Shirt', bottom: 'Chinos', outerwear: 'Light Blazer', accessories: 'Loafers' },
hot: { top: 'Short-sleeve Button-up', bottom: 'Slim Trousers', outerwear: 'None', accessories: 'Watch' }
},
streetwear: {
cold: { top: 'Graphic Hoodie', bottom: 'Cargo Pants', outerwear: 'Bomber Jacket', accessories: 'Snapback' },
mild: { top: 'Oversized Tee', bottom: 'Ripped Jeans', outerwear: 'Denim Jacket', accessories: 'Chain Necklace' },
hot: { top: 'Sleeveless Hoodie', bottom: 'Jogger Shorts', outerwear: 'None', accessories: 'Bucket Hat' }
},
vintage: {
cold: { top: 'Turtleneck', bottom: 'Corduroy Pants', outerwear: 'Pea Coat', accessories: 'Beret' },
mild: { top: 'Retro Shirt', bottom: 'High-waisted Trousers', outerwear: 'Cardigan', accessories: 'Suspenders' },
hot: { top: 'Hawaiian Shirt', bottom: 'Linen Shorts', outerwear: 'None', accessories: 'Round Sunglasses' }
}
};
const tempRange = temp < 15 ? 'cold' : temp < 25 ? 'mild' : 'hot';
const outfit = outfits[style][tempRange];
document.getElementById('top').textContent = `Top: ${outfit.top}`;
document.getElementById('bottom').textContent = `Bottom: ${outfit.bottom}`;
document.getElementById('outerwear').textContent = `Outerwear: ${outfit.outerwear}`;
document.getElementById('accessories').textContent = `Accessories: ${outfit.accessories}`;
}
saveCurrentOutfit() {
const outfit = {
top: document.getElementById('top').textContent,
bottom: document.getElementById('bottom').textContent,
outerwear: document.getElementById('outerwear').textContent,
accessories: document.getElementById('accessories').textContent,
date: new Date().toLocaleDateString()
};
this.savedOutfits.push(outfit);
localStorage.setItem('savedOutfits', JSON.stringify(this.savedOutfits));
this.loadSavedOutfits();
}
loadSavedOutfits() {
const savedDiv = document.getElementById('savedOutfits');
savedDiv.innerHTML = 'Saved Outfits';
this.savedOutfits.forEach((outfit, index) => {
savedDiv.innerHTML += `
${outfit.date}
${outfit.top}
${outfit.bottom}
${outfit.outerwear}
${outfit.accessories}
`;
});
}
}
new OutfitGenerator();
Enter fullscreen mode
Exit fullscreen mode
Let's break down the key components of our JavaScript implementation:
🗺️ Interactive Map Implementation
The map is a central feature of our application, allowing users to visually select their location:
initMap() {
this.map = L.map('map').setView([51.505, -0.09], 13); // Default to London
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this.map);
this.marker = L.marker([51.505, -0.09]).addTo(this.map);
this.map.on('click', (e) => {
this.marker.setLatLng(e.latlng);
});
}
Enter fullscreen mode
Exit fullscreen mode
This method initializes a Leaflet map centered on London (as a default location) and adds a marker. The map responds to click events, allowing users to place the marker anywhere on the map. This provides a precise way to select a location for weather data retrieval.
🔍 Location Search with Autocomplete
To enhance user experience, we've implemented a location search feature with autocomplete functionality:
async searchLocation(query) {
if (query.length < 3) {
this.hideAutocomplete();
return;
}
try {
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5`);
const results = await response.json();
this.displayAutocomplete(results);
} catch (error) {
console.error('Error searching location:', error);
}
}
Enter fullscreen mode
Exit fullscreen mode
This function queries the Nominatim API (OpenStreetMap's geocoding service) when the user types at least three characters. The results are displayed in a dropdown menu, allowing users to quickly select their desired location without typing the full address.
⛅ Weather Data Retrieval
Once a location is selected, we fetch weather data from the Open-Meteo API:
async fetchWeather() {
const { lat, lng } = this.marker.getLatLng();
try {
const weatherResponse = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,weather_code`);
this.weatherData = await weatherResponse.json();
const geocodeResponse = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`);
const geocodeData = await geocodeResponse.json();
this.weatherData.city = geocodeData.address.city || geocodeData.address.town || 'Selected Location';
this.displayWeather();
} catch (error) {
alert('Error fetching weather data: ' + error.message);
}
}
Enter fullscreen mode
Exit fullscreen mode
This method performs two API calls:
It fetches current weather data (temperature and weather code) from Open-Meteo
It performs reverse geocoding to get the city name for the selected coordinates
The combined data is then displayed to the user, providing context for the outfit recommendations.
👕 Outfit Generation Algorithm
The heart of our application is the outfit generation algorithm:
generateOutfit() {
const style = document.getElementById('stylePref').value;
const temp = this.weatherData ? this.weatherData.current.temperature_2m : 20;
const outfits = {
casual: {
cold: { top: 'Sweatshirt', bottom: 'Jeans', outerwear: 'Puffer Jacket', accessories: 'Beanie' },
mild: { top: 'T-shirt', bottom: 'Chinos', outerwear: 'Cardigan', accessories: 'Watch' },
hot: { top: 'Tank Top', bottom: 'Shorts', outerwear: 'None', accessories: 'Sunglasses' }
},
// More styles and temperatures...
};
const tempRange = temp < 15 ? 'cold' : temp < 25 ? 'mild' : 'hot';
const outfit = outfits[style][tempRange];
document.getElementById('top').textContent = `Top: ${outfit.top}`;
document.getElementById('bottom').textContent = `Bottom: ${outfit.bottom}`;
document.getElementById('outerwear').textContent = `Outerwear: ${outfit.outerwear}`;
document.getElementById('accessories').textContent = `Accessories: ${outfit.accessories}`;
}
Enter fullscreen mode
Exit fullscreen mode
This function:
Gets the user's selected style preference from the dropdown
Determines the temperature range (cold, mild, or hot) based on the current temperature
Selects appropriate clothing items from our predefined outfit database
Updates the UI to display the recommended outfit
The outfit database is organized by style preference and temperature range, allowing for precise recommendations based on both factors.
💾 Saving and Loading Outfits
To enhance the user experience, we've implemented a feature to save favorite outfits:
saveCurrentOutfit() {
const outfit = {
top: document.getElementById('top').textContent,
bottom: document.getElementById('bottom').textContent,
outerwear: document.getElementById('outerwear').textContent,
accessories: document.getElementById('accessories').textContent,
date: new Date().toLocaleDateString()
};
this.savedOutfits.push(outfit);
localStorage.setItem('savedOutfits', JSON.stringify(this.savedOutfits));
this.loadSavedOutfits();
}
Enter fullscreen mode
Exit fullscreen mode
This function saves the current outfit recommendation to the browser's local storage, along with the current date. The loadSavedOutfits() method retrieves these saved outfits and displays them, allowing users to keep track of their favorite combinations.
🔄 Putting It All Together
The final piece of our application is the initialization code:
new OutfitGenerator();
Enter fullscreen mode
Exit fullscreen mode
This simple line creates an instance of our OutfitGenerator class, which sets up all the necessary event listeners and initializes the map. When the page loads, the application is ready to use.
💡 Understanding the Weather Code Mapping
One interesting aspect of our implementation is how we map weather codes to human-readable descriptions:
getWeatherCondition(code) {
const conditions = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Fog',
51: 'Light drizzle',
61: 'Light rain',
63: 'Rain',
65: 'Heavy rain',
71: 'Light snow',
73: 'Snow',
75: 'Heavy snow'
};
return conditions[code] || 'Unknown';
}
Enter fullscreen mode
Exit fullscreen mode
The Open-Meteo API returns numerical weather codes that need to be translated into human-readable descriptions. This method provides that translation, making the weather information more understandable for users.
📱 Responsive Design Considerations
Our application is designed to work well on devices of all sizes. The CSS includes media queries that adjust the layout for smaller screens:
@media (max-width: 600px) {
.style-selector {
flex-direction: column;
}
}
Enter fullscreen mode
Exit fullscreen mode
This ensures that the application remains usable and visually appealing on both desktop and mobile devices.
🚀 Potential Enhancements
While our application is fully functional, there are several ways it could be enhanced:
Outfit Images: Add visual representations of the recommended outfits
User Accounts: Implement authentication to store outfits across devices
Personalized Recommendations: Allow users to input their wardrobe items for more tailored suggestions
Detailed Weather Integration: Factor in precipitation, wind, and other weather conditions
Sharing Features: Add the ability to share outfit combinations on social media
Expanded Style Options: Add more styles and more detailed categories within each style
🎯 Conclusion
StyleSync demonstrates how web technologies can be combined to create a practical, user-friendly application that solves a real-world problem. By leveraging APIs for location data and weather information, we've created a tool that provides personalized outfit recommendations based on real-time conditions.This project serves as an excellent example of how to integrate multiple APIs, implement interactive maps, use local storage for data persistence, and create a responsive user interface. The resulting application is not only functional but also visually appealing and enjoyable to use.Next time you're wondering what to wear, remember that StyleSync has got you covered—literally! 🧥👗Try the app now: StyleSync Outfit GeneratorHappy coding! 👩💻👨💻