Skip to content
Merged
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
20 changes: 6 additions & 14 deletions src/api/weather.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
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<AQIData> {
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<UVIndexData>(url);
return this.fetchData<AQIData>(url);
}

async getUVIndex({lat, lon}: Coordinates) {
const url = this.createUrl(`https://api.openweathermap.org/data/2.5/uvi`, {
async getUVIndex({lat, lon}: Coordinates): Promise<UVIndexData> {
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
});
return this.fetchData<UVIndexData>(url);
}

async getPollen({lat, lon}: Coordinates) {
async getPollen({lat, lon}: Coordinates): Promise<PollenData> {
return Promise.resolve({
coord: { lat, lon },
pollen_types: [
Expand Down
58 changes: 58 additions & 0 deletions src/components/mood-selector.tsx
Original file line number Diff line number Diff line change
@@ -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: <Smile className="w-5 h-5" />, color: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-500/20' },
{ value: 'relaxed', label: 'Relaxed', icon: <Wind className="w-5 h-5" />, color: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-500/20' },
{ value: 'focused', label: 'Focused', icon: <Brain className="w-5 h-5" />, color: 'bg-purple-500/10 text-purple-600 dark:text-purple-400 hover:bg-purple-500/20' },
{ value: 'energetic', label: 'Energetic', icon: <Zap className="w-5 h-5" />, color: 'bg-orange-500/10 text-orange-600 dark:text-orange-400 hover:bg-orange-500/20' },
{ value: 'calm', label: 'Calm', icon: <Sparkles className="w-5 h-5" />, 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 (
<div className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium text-muted-foreground">How are you feeling?</h3>
<span className="text-xs text-muted-foreground">(Optional)</span>
</div>

<div className="flex flex-wrap gap-2">
{moodOptions.map((mood) => (
<button
key={mood.value}
onClick={() => handleMoodClick(mood.value)}
className={`
inline-flex items-center gap-2 px-4 py-2 rounded-full
transition-all duration-200 font-medium text-sm
${mood.color}
${selectedMood === mood.value
? 'ring-2 ring-offset-2 ring-offset-background scale-105'
: 'scale-100'
}
`}
>
{mood.icon}
<span>{mood.label}</span>
</button>
))}
</div>
</div>
);
};

export default MoodSelector;
74 changes: 74 additions & 0 deletions src/components/playlist-card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card className="overflow-hidden hover:shadow-lg transition-shadow duration-300 group">
<CardContent className="p-0">
<a
href={playlist.spotifyUrl}
target="_blank"
rel="noopener noreferrer"
className="block"
>
{/* Playlist Image - Smaller with fallback */}
<div className="relative aspect-square overflow-hidden bg-muted">
{!imageError ? (
<img
src={playlist.imageUrl}
alt={playlist.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
onError={() => setImageError(true)}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/20 to-primary/5">
<Music2 className="w-16 h-16 text-primary/40" />
</div>
)}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center">
<ExternalLink className="text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 w-8 h-8" />
</div>
</div>

{/* Playlist Info - Compact */}
<div className="p-3 space-y-1.5">
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-base line-clamp-1 group-hover:text-primary transition-colors">
{playlist.name}
</h3>
<Music2 className="w-4 h-4 text-muted-foreground flex-shrink-0" />
</div>

<p className="text-xs text-muted-foreground line-clamp-2">
{playlist.description}
</p>

<div className="flex items-center justify-between text-xs text-muted-foreground pt-1">
<span className="font-medium">{playlist.genre}</span>
<span>{playlist.trackCount} tracks</span>
</div>

{/* Listen Button - Compact */}
<div className="pt-1.5">
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary group-hover:underline">
Listen on Spotify
<ExternalLink className="w-3 h-3" />
</div>
</div>
</div>
</a>
</CardContent>
</Card>
);
};

export default PlaylistCard;
64 changes: 64 additions & 0 deletions src/components/weather-playlist.tsx
Original file line number Diff line number Diff line change
@@ -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<MoodType | undefined>();

const weatherCondition = data.weather[0]?.main || 'Clear';
const weatherMapping = getPlaylistsForWeather(weatherCondition);
const recommendedPlaylists = getRecommendedPlaylists(weatherCondition, selectedMood);

if (!weatherMapping && recommendedPlaylists.length === 0) {
return null;
}

return (
<Card className="col-span-full">
<CardHeader>
<div className="flex items-center gap-2">
<Music className="w-6 h-6 text-primary" />
<div>
<CardTitle>Your Weather Playlist {weatherMapping?.emoji}</CardTitle>
<CardDescription>
{weatherMapping?.description || 'Curated music for the current weather'}
</CardDescription>
</div>
</div>
</CardHeader>

<CardContent className="space-y-6">
{/* Mood Selector */}
<MoodSelector
selectedMood={selectedMood}
onMoodChange={setSelectedMood}
/>

{/* Playlist Grid */}
{recommendedPlaylists.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{recommendedPlaylists.map((playlist) => (
<PlaylistCard key={playlist.id} playlist={playlist} />
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<Music className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>No playlists available for this weather condition yet.</p>
</div>
)}
</CardContent>
</Card>
);
};

export default WeatherPlaylists;
Loading