diff --git a/src/api/weather.ts b/src/api/weather.ts index 5c0e61b..d0dcf0d 100644 --- a/src/api/weather.ts +++ b/src/api/weather.ts @@ -1,26 +1,18 @@ import { API_CONFIG } from "./config" -import type { Coordinates, ForecastData, GeocodingResponse, WeatherData } from "./types"; - -type UVIndexData = { - lat: number; - lon: number; - date_iso: string; - date: number; - value: number; -}; +import type { Coordinates, ForecastData, GeocodingResponse, WeatherData, AQIData, UVIndexData, PollenData } from "./types"; class WeatherAPI{ - async getAQI({lat, lon}: Coordinates) { + async getAQI({lat, lon}: Coordinates): Promise { const url = this.createUrl(`${API_CONFIG.BASE_URL}/air_pollution`, { lat: lat.toString(), lon: lon.toString(), appid: import.meta.env.VITE_OPENWEATHER_API_KEY }); - return this.fetchData(url); + return this.fetchData(url); } - async getUVIndex({lat, lon}: Coordinates) { - const url = this.createUrl(`https://api.openweathermap.org/data/2.5/uvi`, { + async getUVIndex({lat, lon}: Coordinates): Promise { + const url = this.createUrl("https://api.openweathermap.org/data/2.5/uvi", { lat: lat.toString(), lon: lon.toString(), appid: import.meta.env.VITE_OPENWEATHER_API_KEY @@ -28,7 +20,7 @@ class WeatherAPI{ return this.fetchData(url); } - async getPollen({lat, lon}: Coordinates) { + async getPollen({lat, lon}: Coordinates): Promise { return Promise.resolve({ coord: { lat, lon }, pollen_types: [ diff --git a/src/components/mood-selector.tsx b/src/components/mood-selector.tsx new file mode 100644 index 0000000..7c44b06 --- /dev/null +++ b/src/components/mood-selector.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { Smile, Wind, Brain, Zap, Sparkles } from 'lucide-react'; +import type { MoodType } from '@/types/playlist'; + +interface MoodSelectorProps { + selectedMood?: MoodType; + onMoodChange: (mood: MoodType | undefined) => void; +} + +const moodOptions: Array<{ value: MoodType; label: string; icon: React.ReactNode; color: string }> = [ + { value: 'happy', label: 'Happy', icon: , color: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-500/20' }, + { value: 'relaxed', label: 'Relaxed', icon: , color: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-500/20' }, + { value: 'focused', label: 'Focused', icon: , color: 'bg-purple-500/10 text-purple-600 dark:text-purple-400 hover:bg-purple-500/20' }, + { value: 'energetic', label: 'Energetic', icon: , color: 'bg-orange-500/10 text-orange-600 dark:text-orange-400 hover:bg-orange-500/20' }, + { value: 'calm', label: 'Calm', icon: , color: 'bg-green-500/10 text-green-600 dark:text-green-400 hover:bg-green-500/20' }, +]; + +const MoodSelector = ({ selectedMood, onMoodChange }: MoodSelectorProps) => { + const handleMoodClick = (mood: MoodType) => { + if (selectedMood === mood) { + onMoodChange(undefined); // Deselect if clicking the same mood + } else { + onMoodChange(mood); + } + }; + + return ( +
+
+

How are you feeling?

+ (Optional) +
+ +
+ {moodOptions.map((mood) => ( + + ))} +
+
+ ); +}; + +export default MoodSelector; \ No newline at end of file diff --git a/src/components/playlist-card.tsx b/src/components/playlist-card.tsx new file mode 100644 index 0000000..6f3f360 --- /dev/null +++ b/src/components/playlist-card.tsx @@ -0,0 +1,74 @@ +import { ExternalLink, Music2 } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import type { SpotifyPlaylist } from '@/types/playlist'; +import { useState } from 'react'; + +interface PlaylistCardProps { + playlist: SpotifyPlaylist; +} + +const PlaylistCard = ({ playlist }: PlaylistCardProps) => { + const [imageError, setImageError] = useState(false); + + return ( + + + + {/* Playlist Image - Smaller with fallback */} +
+ {!imageError ? ( + {playlist.name} setImageError(true)} + /> + ) : ( +
+ +
+ )} +
+ +
+
+ + {/* Playlist Info - Compact */} +
+
+

+ {playlist.name} +

+ +
+ +

+ {playlist.description} +

+ +
+ {playlist.genre} + {playlist.trackCount} tracks +
+ + {/* Listen Button - Compact */} +
+
+ Listen on Spotify + +
+
+
+
+
+
+ ); +}; + +export default PlaylistCard; \ No newline at end of file diff --git a/src/components/weather-playlist.tsx b/src/components/weather-playlist.tsx new file mode 100644 index 0000000..32c73b3 --- /dev/null +++ b/src/components/weather-playlist.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { Music } from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import type { WeatherData } from '@/api/types'; +import type { MoodType } from '@/types/playlist'; +import { getPlaylistsForWeather, getRecommendedPlaylists } from '@/lib/playlist-data'; +import PlaylistCard from './playlist-card'; +import MoodSelector from './mood-selector'; + +interface WeatherPlaylistsProps { + data: WeatherData; +} + +const WeatherPlaylists = ({ data }: WeatherPlaylistsProps) => { + const [selectedMood, setSelectedMood] = useState(); + + const weatherCondition = data.weather[0]?.main || 'Clear'; + const weatherMapping = getPlaylistsForWeather(weatherCondition); + const recommendedPlaylists = getRecommendedPlaylists(weatherCondition, selectedMood); + + if (!weatherMapping && recommendedPlaylists.length === 0) { + return null; + } + + return ( + + +
+ +
+ Your Weather Playlist {weatherMapping?.emoji} + + {weatherMapping?.description || 'Curated music for the current weather'} + +
+
+
+ + + {/* Mood Selector */} + + + {/* Playlist Grid */} + {recommendedPlaylists.length > 0 ? ( +
+ {recommendedPlaylists.map((playlist) => ( + + ))} +
+ ) : ( +
+ +

No playlists available for this weather condition yet.

+
+ )} +
+
+ ); +}; + +export default WeatherPlaylists; \ No newline at end of file diff --git a/src/lib/playlist-data.ts b/src/lib/playlist-data.ts new file mode 100644 index 0000000..28dccf9 --- /dev/null +++ b/src/lib/playlist-data.ts @@ -0,0 +1,266 @@ +import type { WeatherPlaylistMapping, MoodType, SpotifyPlaylist } from '@/types/playlist'; + +// Curated Spotify playlists for different weather conditions +export const weatherPlaylistMappings: WeatherPlaylistMapping[] = [ + { + weather: 'Rain', + emoji: '🌧️', + description: 'Cozy vibes for a rainy day', + playlists: [ + { + id: '37i9dQZF1DX2v8AuSe3vVh', + name: 'Rainy Day', + description: 'Cozy up with acoustic and indie tracks', + imageUrl: 'https://images.unsplash.com/photo-1519638399535-1b036603ac77?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX2v8AuSe3vVh', + trackCount: 100, + genre: 'Acoustic/Indie' + }, + { + id: '37i9dQZF1DX4PP3DA4J0N8', + name: 'Jazz Vibes', + description: 'Smooth jazz for a relaxing rainy afternoon', + imageUrl: 'https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX4PP3DA4J0N8', + trackCount: 140, + genre: 'Jazz' + }, + { + id: '37i9dQZF1DWZqd5JICZI0u', + name: 'Lofi Beats', + description: 'Chill lofi hip hop beats', + imageUrl: 'https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DWZqd5JICZI0u', + trackCount: 220, + genre: 'Lo-fi' + } + ] + }, + { + weather: 'Clear', + emoji: '☀️', + description: 'Energetic tracks to match the sunshine', + playlists: [ + { + id: '37i9dQZF1DXdPec7aLTmlC', + name: 'Happy Hits!', + description: 'Hit the feels with these happy tunes', + imageUrl: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DXdPec7aLTmlC', + trackCount: 150, + genre: 'Pop' + }, + { + id: '37i9dQZF1DX3rxVfibe1L0', + name: 'Mood Booster', + description: 'Get happy with this playlist', + imageUrl: 'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX3rxVfibe1L0', + trackCount: 130, + genre: 'Pop/Dance' + }, + { + id: '37i9dQZF1DX0XUsuxWHRQd', + name: 'RapCaviar', + description: 'New music from top hip-hop artists', + imageUrl: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX0XUsuxWHRQd', + trackCount: 50, + genre: 'Hip-Hop' + } + ] + }, + { + weather: 'Snow', + emoji: '❄️', + description: 'Calm and peaceful winter melodies', + playlists: [ + { + id: '37i9dQZF1DX4sWSpwq3LiO', + name: 'Peaceful Piano', + description: 'Relax and indulge with beautiful piano pieces', + imageUrl: 'https://images.unsplash.com/photo-1520523839897-bd0b52f945a0?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX4sWSpwq3LiO', + trackCount: 200, + genre: 'Classical' + }, + { + id: '37i9dQZF1DWVFeEut75IAL', + name: 'Winter Acoustic', + description: 'Cozy acoustic covers and chill vibes', + imageUrl: 'https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DWVFeEut75IAL', + trackCount: 90, + genre: 'Acoustic' + }, + { + id: '37i9dQZF1DX8NTLI2TtZa6', + name: 'Ambient Relaxation', + description: 'Calming ambient soundscapes', + imageUrl: 'https://images.unsplash.com/photo-1459749411175-04bf5292ceea?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX8NTLI2TtZa6', + trackCount: 120, + genre: 'Ambient' + } + ] + }, + { + weather: 'Thunderstorm', + emoji: '⛈️', + description: 'Intense and powerful electronic beats', + playlists: [ + { + id: '37i9dQZF1DX4dyzvuaRJ0n', + name: 'mint', + description: 'The music hitting right now', + imageUrl: 'https://images.unsplash.com/photo-1571330735066-03aaa9429d89?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX4dyzvuaRJ0n', + trackCount: 100, + genre: 'Electronic' + }, + { + id: '37i9dQZF1DX0hvqKTPJYMa', + name: 'Beast Mode', + description: 'Aggressive rap to fuel your workouts', + imageUrl: 'https://images.unsplash.com/photo-1571330735066-03aaa9429d89?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX0hvqKTPJYMa', + trackCount: 70, + genre: 'Hip-Hop/Rap' + }, + { + id: '37i9dQZF1DX4JAvHpjipBk', + name: 'New Music Friday', + description: 'New music from around the world', + imageUrl: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX4JAvHpjipBk', + trackCount: 100, + genre: 'Various' + } + ] + }, + { + weather: 'Clouds', + emoji: '☁️', + description: 'Mellow and contemplative tunes', + playlists: [ + { + id: '37i9dQZF1DX3YSRoSdA634', + name: 'Evening Acoustic', + description: 'Gentle acoustic songs for easy listening', + imageUrl: 'https://images.unsplash.com/photo-1487180144351-b8472da7d491?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX3YSRoSdA634', + trackCount: 120, + genre: 'Acoustic' + }, + { + id: '37i9dQZF1DWXe9gFZP0gtP', + name: 'Relax & Unwind', + description: 'Ease into relaxation with gentle tunes', + imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DWXe9gFZP0gtP', + trackCount: 110, + genre: 'Chill' + } + ] + }, + { + weather: 'Drizzle', + emoji: '🌦️', + description: 'Soft and soothing melodies', + playlists: [ + { + id: '37i9dQZF1DX2v8AuSe3vVh', + name: 'Rainy Day', + description: 'Cozy up with acoustic and indie tracks', + imageUrl: 'https://images.unsplash.com/photo-1519638399535-1b036603ac77?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX2v8AuSe3vVh', + trackCount: 100, + genre: 'Acoustic/Indie' + } + ] + } +]; + +// Mood-based playlist refinements +export const moodPlaylists: Record = { + happy: [ + { + id: '37i9dQZF1DXdPec7aLTmlC', + name: 'Happy Hits!', + description: 'Feel-good favorites to lift your spirits', + imageUrl: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DXdPec7aLTmlC', + trackCount: 150, + genre: 'Pop' + } + ], + relaxed: [ + { + id: '37i9dQZF1DWZqd5JICZI0u', + name: 'Lofi Beats', + description: 'Chill lofi hip hop beats to relax', + imageUrl: 'https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DWZqd5JICZI0u', + trackCount: 220, + genre: 'Lo-fi' + } + ], + focused: [ + { + id: '37i9dQZF1DX8NTLI2TtZa6', + name: 'Deep Focus', + description: 'Keep calm and stay focused', + imageUrl: 'https://images.unsplash.com/photo-1459749411175-04bf5292ceea?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX8NTLI2TtZa6', + trackCount: 180, + genre: 'Ambient' + } + ], + energetic: [ + { + id: '37i9dQZF1DX3rxVfibe1L0', + name: 'Mood Booster', + description: 'Get energized with upbeat tracks', + imageUrl: 'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX3rxVfibe1L0', + trackCount: 130, + genre: 'Pop/Dance' + } + ], + calm: [ + { + id: '37i9dQZF1DX4sWSpwq3LiO', + name: 'Peaceful Piano', + description: 'Relax and indulge with beautiful piano pieces', + imageUrl: 'https://images.unsplash.com/photo-1520523839897-bd0b52f945a0?w=400&h=400&fit=crop', + spotifyUrl: 'https://open.spotify.com/playlist/37i9dQZF1DX4sWSpwq3LiO', + trackCount: 200, + genre: 'Classical' + } + ] +}; + +// Helper function to get playlists based on weather +export function getPlaylistsForWeather(weatherCondition: string): WeatherPlaylistMapping | null { + return weatherPlaylistMappings.find( + mapping => mapping.weather.toLowerCase() === weatherCondition.toLowerCase() + ) || null; +} + +// Helper function to combine weather and mood playlists +export function getRecommendedPlaylists( + weatherCondition: string, + mood?: MoodType +): SpotifyPlaylist[] { + const weatherMapping = getPlaylistsForWeather(weatherCondition); + const weatherPlaylists = weatherMapping?.playlists || []; + + if (mood && moodPlaylists[mood]) { + // Combine and deduplicate + const combined = [...weatherPlaylists, ...moodPlaylists[mood]]; + const unique = Array.from(new Map(combined.map(p => [p.id, p])).values()); + return unique; + } + + return weatherPlaylists; +} \ No newline at end of file diff --git a/src/pages/city-page.tsx b/src/pages/city-page.tsx index 21eb979..e515bd1 100644 --- a/src/pages/city-page.tsx +++ b/src/pages/city-page.tsx @@ -11,6 +11,7 @@ import WeatherForecast from "@/components/weather-forecast"; import { useState } from "react"; import FavoriteButton from "@/components/favorite-button"; import HealthRecommendations from "@/components/healthRecommendations"; +import WeatherPlaylists from "@/components/weather-playlist"; const CityPage = () => { @@ -73,9 +74,10 @@ const CityPage = () => { {/* forecast */} + ); } -export default CityPage; +export default CityPage; \ No newline at end of file diff --git a/src/pages/weather-dashboard.tsx b/src/pages/weather-dashboard.tsx index d89c3ea..e4e7ee9 100644 --- a/src/pages/weather-dashboard.tsx +++ b/src/pages/weather-dashboard.tsx @@ -11,6 +11,7 @@ import WeatherDetails from "@/components/weather-details"; import WeatherForecast from "@/components/weather-forecast"; import FavoriteCities from "@/components/favorite-cities"; import HealthRecommendations from "@/components/healthRecommendations"; +import WeatherPlaylists from "@/components/weather-playlist"; const WeatherDashboard = () => { const {coordinates,error:locationError,getLocation,isLoading:locationLoading}=useGeolocation(); @@ -118,8 +119,9 @@ const WeatherDashboard = () => { {/* forecast */} + ); } -export default WeatherDashboard; +export default WeatherDashboard; \ No newline at end of file diff --git a/src/types/playlist.ts b/src/types/playlist.ts new file mode 100644 index 0000000..94f05a4 --- /dev/null +++ b/src/types/playlist.ts @@ -0,0 +1,35 @@ +export type MoodType = 'happy' | 'relaxed' | 'focused' | 'energetic' | 'calm'; + +export type WeatherConditionType = + | 'Clear' + | 'Clouds' + | 'Rain' + | 'Drizzle' + | 'Thunderstorm' + | 'Snow' + | 'Mist' + | 'Smoke' + | 'Haze' + | 'Dust' + | 'Fog' + | 'Sand' + | 'Ash' + | 'Squall' + | 'Tornado'; + +export interface SpotifyPlaylist { + id: string; + name: string; + description: string; + imageUrl: string; + spotifyUrl: string; + trackCount: number; + genre: string; +} + +export interface WeatherPlaylistMapping { + weather: WeatherConditionType; + emoji: string; + playlists: SpotifyPlaylist[]; + description: string; +} \ No newline at end of file