File size: 7,997 Bytes
ef9d0f7 cbc2e95 1051cf6 cbc2e95 ef9d0f7 cbc2e95 ef9d0f7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
import React, { useState, useEffect, useCallback } from 'react';
import Card from './components/Card';
import Loader from './components/Loader';
import Button from './components/Button';
import WeatherIcon from './components/WeatherIcon';
import { generateWeatherForecast, getOutfitSuggestion, generateOutfitImage } from './services/geminiService';
import type { Weather, OutfitSuggestion } from './types';
const App: React.FC = () => {
const [location, setLocation] = useState<GeolocationCoordinates | null>(null);
const [weather, setWeather] = useState<Weather | null>(null);
const [wardrobe, setWardrobe] = useState<string>('- A pair of blue jeans\n- A white cotton t-shirt\n- Black denim jeans\n- A pair of sneakers\n- A warm wool sweater\n- Linen shorts\n- A waterproof rain jacket');
const [suggestion, setSuggestion] = useState<OutfitSuggestion | null>(null);
const [outfitImage, setOutfitImage] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchWeather = useCallback(async (coords: GeolocationCoordinates) => {
setIsLoading(true);
setError(null);
try {
const weatherData = await generateWeatherForecast(coords.latitude, coords.longitude);
setWeather(weatherData);
} catch (err) {
setError('Could not fetch weather data. Please try again later.');
console.error(err);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
navigator.geolocation.getCurrentPosition(
(position) => {
setLocation(position.coords);
fetchWeather(position.coords);
},
(geoError) => {
setError('Geolocation is required. Please enable it in your browser settings.');
console.error(geoError);
setIsLoading(false);
}
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchWeather]);
const handleGenerateSuggestion = async () => {
if (!weather || !wardrobe.trim()) {
setError("Weather data and wardrobe inventory are required.");
return;
}
setIsGenerating(true);
setError(null);
setSuggestion(null);
setOutfitImage(null);
try {
const outfitSuggestion = await getOutfitSuggestion(weather, wardrobe);
setSuggestion(outfitSuggestion);
if (outfitSuggestion.outfit) {
const imageUrl = await generateOutfitImage(outfitSuggestion.outfit);
setOutfitImage(imageUrl);
}
} catch (err) {
setError('Failed to generate outfit suggestion. The model may be busy. Please try again.');
console.error(err);
} finally {
setIsGenerating(false);
}
};
return (
<div className="min-h-screen bg-gray-100 font-sans text-gray-800">
<main className="max-w-7xl mx-auto p-4 sm:p-6 lg:p-8">
<header className="text-center mb-10">
<h1 className="text-4xl md:text-5xl font-bold text-gray-800">ClothCast</h1>
<p className="text-lg text-gray-600 mt-2">Your AI-Powered Outfit Forecaster</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 items-start">
{/* Left Column: Inputs */}
<div className="space-y-6 lg:sticky lg:top-8">
<Card
title="Your Wardrobe"
icon={<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>}
>
<p className="text-sm text-gray-600 mb-4">List the clothes you have available. Be as descriptive as you like!</p>
<textarea
value={wardrobe}
onChange={(e) => setWardrobe(e.target.value)}
placeholder="e.g., Blue denim jacket, pair of white sneakers..."
className="w-full h-48 p-3 bg-gray-50 border border-gray-300 rounded-md focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-shadow text-gray-700"
/>
</Card>
<Button onClick={handleGenerateSuggestion} isLoading={isGenerating} disabled={isLoading || !wardrobe.trim()}>
Get Outfit Suggestion
</Button>
</div>
{/* Right Column: Outputs */}
<div className="space-y-8">
<Card
title="Today's Forecast"
icon={<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path strokeLinecap="round" strokeLinejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>}
>
{isLoading ? <Loader text="Fetching local weather..." /> :
error && !weather ? <p className="text-red-500">{error}</p> :
weather && (
<div className="flex items-center gap-6">
<WeatherIcon condition={weather.condition} className="w-20 h-20" />
<div>
f'<p className="text-3xl font-bold">{weather.temperature}°C in <span className="text-orange-600">{weather.location}</span></p>'
<p className="text-gray-600">{weather.description}</p>
<p className="text-sm text-gray-500 mt-1">Humidity: {weather.humidity}%</p>
</div>
</div>
)}
</Card>
{(isGenerating || suggestion || outfitImage || (error && !isGenerating)) && (
<Card
title="AI Suggestion"
icon={<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>}
>
{isGenerating && <Loader text="Crafting your perfect outfit..." />}
{error && !isGenerating && <p className="text-red-500 text-center">{error}</p>}
{!isGenerating && suggestion && (
<div className="space-y-6">
{outfitImage ? (
<img src={outfitImage} alt="Generated outfit" className="rounded-lg shadow-md w-full object-cover aspect-[3/4]" />
) : (
<div className="w-full aspect-[3/4] bg-gray-100 rounded-lg flex items-center justify-center animate-pulse">
<Loader text="Generating image..." />
</div>
)}
<p className="text-gray-700 leading-relaxed">{suggestion.outfit}</p>
{suggestion.laundry_alert && (
<div className="p-4 bg-amber-100 border-l-4 border-amber-500 text-amber-800 rounded-r-lg">
<p className="font-semibold">Laundry Alert!</p>
<p>{suggestion.laundry_alert}</p>
</div>
)}
</div>
)}
</Card>
)}
</div>
</div>
<footer className="text-center mt-12 text-sm text-gray-500">
<p>Powered by <a href="https://ai.google.dev/" target="_blank" rel="noopener noreferrer" className="font-semibold text-gray-600 hover:underline">Google Gemini</a></p>
</footer>
</main>
</div>
);
};
export default App; |