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;