diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fc9e618..7df8f2f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,121 +6,124 @@ on: types: [published] jobs: - list-templates: - uses: ./.github/workflows/list-templates.yml + list-templates: + uses: ./.github/workflows/list-templates.yml - deploy: - # Can't deploy on a non-published release, so publish the release first. - if: ${{ !contains(github.event.release.tag_name, 'dev') }} - needs: list-templates - runs-on: ubuntu-latest - environment: Cloud Deploy - strategy: - matrix: - folder: ${{ fromJSON(needs.list-templates.outputs.templates) }} - exclude: - # No deploy due to missing service dependencies. - - folder: reflex-llamaindex-template - fail-fast: false - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Set reflex version for deploy - run: sed -e "s/^reflex[ >=].*$/reflex==${{ github.event.release.tag_name }}/" -i ${{ matrix.folder }}/requirements.txt - - name: Set environment variables - id: set-env - run: | - case ${{ matrix.folder }} in - ai_image_gen) - echo "EXTRA_ARGS=--env REPLICATE_API_TOKEN=${{ secrets.REPLICATE_API_TOKEN }}" >> $GITHUB_ENV - ;; - text_annotation_app) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - chat_app) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - futuristic_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - retention_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - retail_analytics_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - table_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - space_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - company_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - account_management_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - stock_graph_app) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - admin_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - admin_panel) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - stock_market_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - business_analytics_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - retail_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - manufacturing_dashboard) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - customer_data_app) - cat .deploy/temporary_db.py >> ${{ matrix.folder }}/customer_data/customer_data.py - echo "EXTRA_ARGS=--vmtype ${{ vars.CUSTOMER_DATA_VM_TYPE }}" >> $GITHUB_ENV - ;; - api_admin_panel) - echo "EXTRA_ARGS=--vmtype ${{ vars.ADMIN_PANEL_VM_TYPE }}" >> $GITHUB_ENV - ;; - sales) - echo "EXTRA_ARGS=--env OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }}" >> $GITHUB_ENV - cat .deploy/temporary_db.py >> ${{ matrix.folder }}/${{ matrix.folder }}/${{ matrix.folder }}.py - ;; - reflex-chat) - echo "EXTRA_ARGS=--env OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }} --vmtype ${{ vars.CHAT_VM_TYPE }}" >> $GITHUB_ENV - echo "OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }}" >> $GITHUB_ENV - ;; - nba) - echo "EXTRA_ARGS=--vmtype ${{ vars.NBA_VM_TYPE }}" >> $GITHUB_ENV - ;; - dashboard) - echo "EXTRA_ARGS=--vmtype ${{ vars.DASHBOARD_VM_TYPE }}" >> $GITHUB_ENV - ;; - ci_template) - echo "EXTRA_ARGS=--vmtype ${{ vars.CIJOB_VM_TYPE }}" >> $GITHUB_ENV - ;; - dalle) - echo "EXTRA_ARGS=--env OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }}" >> $GITHUB_ENV - echo "EXTRA_ARGS=--vmtype ${{ vars.DALLE_VM_TYPE }}" >> $GITHUB_ENV - ;; - *) - echo "EXTRA_ARGS=" >> $GITHUB_ENV - ;; - esac - - name: Deploy to ReflexCloud - uses: reflex-dev/reflex-deploy-action@v2 - with: - auth_token: ${{ secrets.REFLEX_AUTH_TOKEN }} - project_id: ${{ secrets.REFLEX_PROJECT_ID }} - app_directory: ${{ matrix.folder }} - extra_args: ${{ env.EXTRA_ARGS }} - dry_run: ${{ vars.DRY_RUN }} - skip_checkout: "true" + deploy: + # Can't deploy on a non-published release, so publish the release first. + if: ${{ !contains(github.event.release.tag_name, 'dev') }} + needs: list-templates + runs-on: ubuntu-latest + environment: Cloud Deploy + strategy: + matrix: + folder: ${{ fromJSON(needs.list-templates.outputs.templates) }} + exclude: + # No deploy due to missing service dependencies. + - folder: reflex-llamaindex-template + fail-fast: false + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set reflex version for deploy + run: sed -e "s/^reflex[ >=].*$/reflex==${{ github.event.release.tag_name }}/" -i ${{ matrix.folder }}/requirements.txt + - name: Set environment variables + id: set-env + run: | + case ${{ matrix.folder }} in + weatherstack_app) + echo "EXTRA_ARGS=--env REPLICATE_API_TOKEN=${{ secrets.REPLICATE_API_TOKEN }}" >> $GITHUB_ENV + ;; + ai_image_gen) + echo "EXTRA_ARGS=--env REPLICATE_API_TOKEN=${{ secrets.REPLICATE_API_TOKEN }}" >> $GITHUB_ENV + ;; + text_annotation_app) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + chat_app) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + futuristic_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + retention_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + retail_analytics_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + table_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + space_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + company_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + account_management_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + stock_graph_app) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + admin_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + admin_panel) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + stock_market_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + business_analytics_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + retail_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + manufacturing_dashboard) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + customer_data_app) + cat .deploy/temporary_db.py >> ${{ matrix.folder }}/customer_data/customer_data.py + echo "EXTRA_ARGS=--vmtype ${{ vars.CUSTOMER_DATA_VM_TYPE }}" >> $GITHUB_ENV + ;; + api_admin_panel) + echo "EXTRA_ARGS=--vmtype ${{ vars.ADMIN_PANEL_VM_TYPE }}" >> $GITHUB_ENV + ;; + sales) + echo "EXTRA_ARGS=--env OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }}" >> $GITHUB_ENV + cat .deploy/temporary_db.py >> ${{ matrix.folder }}/${{ matrix.folder }}/${{ matrix.folder }}.py + ;; + reflex-chat) + echo "EXTRA_ARGS=--env OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }} --vmtype ${{ vars.CHAT_VM_TYPE }}" >> $GITHUB_ENV + echo "OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }}" >> $GITHUB_ENV + ;; + nba) + echo "EXTRA_ARGS=--vmtype ${{ vars.NBA_VM_TYPE }}" >> $GITHUB_ENV + ;; + dashboard) + echo "EXTRA_ARGS=--vmtype ${{ vars.DASHBOARD_VM_TYPE }}" >> $GITHUB_ENV + ;; + ci_template) + echo "EXTRA_ARGS=--vmtype ${{ vars.CIJOB_VM_TYPE }}" >> $GITHUB_ENV + ;; + dalle) + echo "EXTRA_ARGS=--env OPENAI_API_KEY=${{ secrets.OPEN_AI_KEY }}" >> $GITHUB_ENV + echo "EXTRA_ARGS=--vmtype ${{ vars.DALLE_VM_TYPE }}" >> $GITHUB_ENV + ;; + *) + echo "EXTRA_ARGS=" >> $GITHUB_ENV + ;; + esac + - name: Deploy to ReflexCloud + uses: reflex-dev/reflex-deploy-action@v2 + with: + auth_token: ${{ secrets.REFLEX_AUTH_TOKEN }} + project_id: ${{ secrets.REFLEX_PROJECT_ID }} + app_directory: ${{ matrix.folder }} + extra_args: ${{ env.EXTRA_ARGS }} + dry_run: ${{ vars.DRY_RUN }} + skip_checkout: "true" diff --git a/templates.json b/templates.json index 9b04ffa..4d9b167 100644 --- a/templates.json +++ b/templates.json @@ -186,6 +186,13 @@ "demo_url": "https://retail-dashboard-navy-wood.reflex.run/", "hidden": false, "reflex_build": true + }, + { + "name": "weatherstack_app", + "description": "A weather forecast app", + "demo_url": "https://retail-dashboard-navy-wood.reflex.run/", + "hidden": false, + "reflex_build": true } ] } diff --git a/weatherstack_app/.gitignore b/weatherstack_app/.gitignore new file mode 100644 index 0000000..9fd6d68 --- /dev/null +++ b/weatherstack_app/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +.web +assets/external/ +*.py[cod] +*.db +.states +.DS_Store +.idea/ \ No newline at end of file diff --git a/weatherstack_app/assets/favicon.ico b/weatherstack_app/assets/favicon.ico new file mode 100644 index 0000000..166ae99 Binary files /dev/null and b/weatherstack_app/assets/favicon.ico differ diff --git a/weatherstack_app/blocks/__init__.py b/weatherstack_app/blocks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weatherstack_app/requirements.txt b/weatherstack_app/requirements.txt new file mode 100644 index 0000000..27b5a94 --- /dev/null +++ b/weatherstack_app/requirements.txt @@ -0,0 +1,2 @@ +reflex>=0.7.13a1 +httpx diff --git a/weatherstack_app/rxconfig.py b/weatherstack_app/rxconfig.py new file mode 100644 index 0000000..0e70f2e --- /dev/null +++ b/weatherstack_app/rxconfig.py @@ -0,0 +1,3 @@ +import reflex as rx + +config = rx.Config(app_name="weatherstack_app", plugins=[rx.plugins.TailwindV3Plugin()]) diff --git a/weatherstack_app/weatherstack_app/__init__.py b/weatherstack_app/weatherstack_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weatherstack_app/weatherstack_app/components/__init__.py b/weatherstack_app/weatherstack_app/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weatherstack_app/weatherstack_app/components/preset_cards.py b/weatherstack_app/weatherstack_app/components/preset_cards.py new file mode 100644 index 0000000..40d49f9 --- /dev/null +++ b/weatherstack_app/weatherstack_app/components/preset_cards.py @@ -0,0 +1,36 @@ +import reflex as rx + +from weatherstack_app.states.weather_state import WeatherState + + +def card(flag: str, title: str, city: str) -> rx.Component: + return rx.el.button( + rx.el.div( + rx.el.span(flag, class_name="text-xl"), + rx.el.p(title, class_name="font-medium text-black text-base"), + class_name="flex flex-row gap-2 items-center", + ), + type="button", + class_name=( + "flex flex-col gap-1 border bg-white hover:bg-gray-100 " + "shadow-sm px-4 py-3.5 rounded-xl text-start transition-colors flex-1" + ), + on_click=WeatherState.get_weather_from_preset(city), + ) + + +def preset_cards() -> rx.Component: + return rx.el.div( + rx.el.div( + card("🇯🇵", "Tokyo, Japan", "Japan"), + card("🇫🇷", "Paris, France", "France"), + card("🇺🇸", "New York, USA", "USA"), + card("🇦🇺", "Sydney, Australia", "Australia"), + card("🇩🇪", "Berlin, Germany", "Germany"), + card("🇧🇷", "São Paulo, Brazil", "Brazil"), + card("🇨🇦", "Toronto, Canada", "Canada"), + card("🇮🇳", "Mumbai, India", "India"), + class_name="gap-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 w-full", + ), + class_name="flex flex-col justify-center items-center gap-8 w-full max-w-[55rem] px-6 md:pt-12", + ) diff --git a/weatherstack_app/weatherstack_app/components/weather_display.py b/weatherstack_app/weatherstack_app/components/weather_display.py new file mode 100644 index 0000000..a571075 --- /dev/null +++ b/weatherstack_app/weatherstack_app/components/weather_display.py @@ -0,0 +1,111 @@ +import reflex as rx + +from weatherstack_app.states.weather_state import WeatherState + + +def weather_display() -> rx.Component: + return rx.el.div( + rx.cond( + WeatherState.loading, + rx.el.div( + rx.spinner(class_name="text-blue-500"), + rx.el.p( + "Loading weather data...", + class_name="text-lg text-gray-600", + ), + class_name="flex flex-col items-center justify-center p-6 bg-white rounded-lg shadow-md", + ), + rx.cond( + WeatherState.error_message != "", + rx.el.div( + rx.el.p( + "Error:", + class_name="font-semibold text-red-600", + ), + rx.el.p( + WeatherState.error_message, + class_name="text-red-500", + ), + class_name="p-6 bg-red-50 rounded-lg shadow-md border border-red-200", + ), + rx.cond( + WeatherState.display_weather, + rx.el.div( + rx.el.p( + f"{WeatherState.weather_data['location']['name']}, {WeatherState.weather_data['location']['country']}", + class_name="text-3xl font-bold text-gray-800 mb-4", + ), + rx.el.div( + rx.el.img( + src=WeatherState.weather_icon_url, + alt="Weather icon", + class_name="w-20 h-20 mb-2", + ), + rx.el.p( + WeatherState.weather_description_text, + class_name="text-xl text-gray-700 capitalize", + ), + class_name="flex flex-col items-center mb-4", + ), + rx.el.div( + rx.el.div( + rx.el.p( + "Temperature:", + class_name="text-md text-gray-600", + ), + rx.el.p( + f"{WeatherState.weather_data['current']['temperature']}°C", + class_name="text-2xl font-semibold text-blue-600", + ), + class_name="p-4 bg-blue-50 rounded-lg shadow-sm text-center", + ), + rx.el.div( + rx.el.p( + "Humidity:", + class_name="text-md text-gray-600", + ), + rx.el.p( + f"{WeatherState.weather_data['current']['humidity']}%", + class_name="text-2xl font-semibold text-green-600", + ), + class_name="p-4 bg-green-50 rounded-lg shadow-sm text-center", + ), + rx.el.div( + rx.el.p( + "Feels Like:", + class_name="text-md text-gray-600", + ), + rx.el.p( + f"{WeatherState.weather_data['current']['feelslike']}°C", + class_name="text-2xl font-semibold text-orange-600", + ), + class_name="p-4 bg-orange-50 rounded-lg shadow-sm text-center", + ), + rx.el.div( + rx.el.p( + "Wind Speed:", + class_name="text-md text-gray-600", + ), + rx.el.p( + f"{WeatherState.weather_data['current']['wind_speed']} km/h", + class_name="text-2xl font-semibold text-purple-600", + ), + class_name="p-4 bg-purple-50 rounded-lg shadow-sm text-center", + ), + class_name="grid grid-cols-1 md:grid-cols-2 gap-4 w-full", + ), + rx.button("Reset", on_click=WeatherState.reset_app), + class_name="p-6 rounded-lg border shadow-sm border w-full max-w-2xl justify-center", + ), + rx.el.div( + rx.el.p( + "Enter a city to get the weather forecast.", + class_name="text-lg text-gray-500", + ), + class_name="p-6 bg-white rounded-lg shadow-md", + ), + ), + ), + ), + class_name="mt-8 w-full flex justify-center", + ) diff --git a/weatherstack_app/weatherstack_app/preview.png b/weatherstack_app/weatherstack_app/preview.png new file mode 100644 index 0000000..9a073e5 Binary files /dev/null and b/weatherstack_app/weatherstack_app/preview.png differ diff --git a/weatherstack_app/weatherstack_app/states/__init__.py b/weatherstack_app/weatherstack_app/states/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weatherstack_app/weatherstack_app/states/weather_state.py b/weatherstack_app/weatherstack_app/states/weather_state.py new file mode 100644 index 0000000..4011324 --- /dev/null +++ b/weatherstack_app/weatherstack_app/states/weather_state.py @@ -0,0 +1,172 @@ +from typing import List, TypedDict + +import httpx +import reflex as rx + +WEATHERSTACK_API_KEY = "YOUR_WEATHERSTACK_API_KEY" +WEATHERSTACK_API_URL = "http://api.weatherstack.com/current" + + +class Location(TypedDict): + name: str + country: str + region: str + lat: str + lon: str + timezone_id: str + localtime: str + localtime_epoch: int + utc_offset: str + + +class CurrentWeather(TypedDict): + observation_time: str + temperature: int + weather_code: int + weather_icons: List[str] + weather_descriptions: List[str] + wind_speed: int + wind_degree: int + wind_dir: str + pressure: int + precip: float + humidity: int + cloudcover: int + feelslike: int + uv_index: int + visibility: int + is_day: str + + +class WeatherRequest(TypedDict): + type: str + query: str + language: str + unit: str + + +class WeatherData(TypedDict): + request: WeatherRequest | None + location: Location | None + current: CurrentWeather | None + + +class WeatherState(rx.State): + city: str = "" + weather_data: WeatherData | None = None + loading: bool = False + error_message: str = "" + api_key: str = WEATHERSTACK_API_KEY + + @rx.event + def reset_app(self): + self.reset() + return + + @rx.event + def handle_form_submit(self, form_data: dict): + self.city = form_data.get("city", "").strip() + if not self.city: + self.error_message = "City name cannot be empty." + self.weather_data = None + return + self.error_message = "" + return WeatherState.get_weather + + @rx.event + def get_weather_from_preset(self, city: str): + self.city = city + if not self.city: + self.error_message = "City name cannot be empty." + self.weather_data = None + return + self.error_message = "" + return WeatherState.get_weather + + @rx.event(background=True) + async def get_weather(self): + async with self: + if not self.city: + self.error_message = "City name cannot be empty." + self.loading = False + return + if self.api_key == "YOUR_WEATHERSTACK_API_KEY": + self.error_message = "Please replace 'YOUR_WEATHERSTACK_API_KEY' with your actual WeatherStack API key in app/states/weather_state.py." + self.weather_data = None + self.loading = False + return + self.loading = True + self.error_message = "" + self.weather_data = None + + try: + params = { + "access_key": self.api_key, + "query": self.city, + } + + async with httpx.AsyncClient(timeout=10) as client: + response = await client.get(WEATHERSTACK_API_URL, params=params) + response.raise_for_status() + data = response.json() + + async with self: + if "error" in data: + self.error_message = data["error"].get( + "info", + "An error occurred while fetching weather data.", + ) + self.weather_data = None + elif "current" in data and "location" in data: + self.weather_data = data + self.error_message = "" + else: + self.error_message = "Unexpected API response format." + self.weather_data = None + + except httpx.RequestError as e: + async with self: + self.error_message = f"Network error: {e}" + self.weather_data = None + except Exception as e: + async with self: + self.error_message = f"An unexpected error occurred: {e}" + self.weather_data = None + finally: + async with self: + self.loading = False + + def set_city(self, city: str): + self.city = city.strip() + self.error_message = "" + + @rx.var + def display_weather(self) -> bool: + return ( + self.weather_data is not None + and self.weather_data.get("current") is not None + and (self.weather_data.get("location") is not None) + and (not self.error_message) + ) + + @rx.var + def weather_icon_url(self) -> str: + if ( + self.display_weather + and self.weather_data + and self.weather_data.get("current") + and self.weather_data["current"].get("weather_icons") + ): + return self.weather_data["current"]["weather_icons"][0] + return "" + + @rx.var + def weather_description_text(self) -> str: + if ( + self.display_weather + and self.weather_data + and self.weather_data.get("current") + and self.weather_data["current"].get("weather_descriptions") + ): + return ", ".join(self.weather_data["current"]["weather_descriptions"]) + return "" diff --git a/weatherstack_app/weatherstack_app/weatherstack_app.py b/weatherstack_app/weatherstack_app/weatherstack_app.py new file mode 100644 index 0000000..fb76f8a --- /dev/null +++ b/weatherstack_app/weatherstack_app/weatherstack_app.py @@ -0,0 +1,59 @@ +import reflex as rx + +from weatherstack_app.components.preset_cards import preset_cards +from weatherstack_app.components.weather_display import weather_display +from weatherstack_app.states.weather_state import WeatherState + + +def index() -> rx.Component: + return rx.el.div( + rx.cond( + WeatherState.display_weather, + weather_display(), + rx.el.div( + rx.el.p( + "Where would you like the forecast for today?", + class_name="text-2xl md:text-3xl font-medium", + ), + rx.el.form( + rx.el.div( + rx.el.div( + rx.el.button( + rx.icon( + "forward", + size=20, + class_name="absolute right-2 top-1/2 transform -translate-y-1/2 rounded-full bg-blue-500 text-white p-2 disabled:opacity-50 shadow-sm size-7 self-flex items-center justify-center cursor-pointer", + ), + type="submit", + ), + rx.el.input( + name="city", + placeholder="Enter city name...", + default_value=WeatherState.city, + class_name="px-2 py-3 w-full text-sm rounded-xl bg-transparent border shadow-sm focus:outline-none focus:border-blue-500", + ), + class_name="relative focus:outline-none w-full max-w-[400px] py-4", + ), + class_name="flex w-full justify-center", + ), + on_submit=WeatherState.handle_form_submit, + reset_on_submit=False, + prevent_default=True, + class_name="w-full justify-center items-center flex", + ), + rx.cond( + WeatherState.error_message != "", + rx.el.p( + WeatherState.error_message, + class_name="text-sm font-medium text-red-500", + ), + ), + preset_cards(), + class_name="flex flex-col items-center justify-center align-center min-h-screen gap-y-4", + ), + ) + ) + + +app = rx.App(theme=rx.theme(appearance="light")) +app.add_page(index)