From d2e91b53c6f2ee19f94f30dda5cee014e1257e91 Mon Sep 17 00:00:00 2001 From: Brais Moure Date: Fri, 23 Feb 2024 13:52:01 +0100 Subject: [PATCH] Clase 6 [Avanzado] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature flags y migración a Radix UI --- README.md | 7 +- link_bio/README.md | 2 +- link_bio/link_bio/api/ConfigCatAPI.py | 6 +- link_bio/link_bio/api/SupabaseAPI.py | 3 +- link_bio/link_bio/api/api.py | 6 ++ .../link_bio/components/ant_components.py | 2 +- link_bio/link_bio/components/featured_link.py | 5 +- link_bio/link_bio/components/footer.py | 13 +++- link_bio/link_bio/components/info_text.py | 3 +- link_bio/link_bio/components/link_button.py | 20 +++-- link_bio/link_bio/components/navbar.py | 4 +- link_bio/link_bio/pages/index.py | 5 +- link_bio/link_bio/state/PageState.py | 6 +- link_bio/link_bio/styles/styles.py | 22 ++++-- link_bio/link_bio/utils.py | 39 ++++++++++ link_bio/link_bio/views/courses_links.py | 4 +- link_bio/link_bio/views/header.py | 73 ++++++++++++------- link_bio/link_bio/views/index_links.py | 12 +-- link_bio/link_bio/views/sponsors.py | 10 +-- link_bio/requirements.txt | 2 +- 20 files changed, 172 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 827f9ab9..b69d9af2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Python Web [![Python](https://img.shields.io/badge/Python-3.11+-yellow?style=for-the-badge&logo=python&logoColor=white&labelColor=101010)](https://python.org) -[![Reflex](https://img.shields.io/badge/Reflex-0.3.10+-5646ED?style=for-the-badge&logo=reflex&logoColor=white&labelColor=101010)](https://reflex.dev) +[![Reflex](https://img.shields.io/badge/Reflex-0.4.1+-5646ED?style=for-the-badge&logo=reflex&logoColor=white&labelColor=101010)](https://reflex.dev) ## Curso de 6 horas en vídeo para aprender desarrollo web frontend con Python puro y Reflex desde cero. @@ -12,8 +12,8 @@ ## ⚠️ [NUEVO] Curso de Python Web Avanzado -### 🗓️ Próxima clase: 22/02/24 a las 20h (España) en directo desde [Twitch](https://twitch.tv/mouredev) -[**Consulta aquí el horario en tu país y crea un recordatorio**](https://discord.gg/mouredev?event=1208033343644368917) +### 🗓️ Próxima clase: 07/03/24 a las 20h (España) en directo desde [Twitch](https://twitch.tv/mouredev) +[**Consulta aquí el horario en tu país y crea un recordatorio**](https://discord.gg/mouredev?event=1210569183126093855) ### Clases anteriores: @@ -22,6 +22,7 @@ - [Clase 3 [01/02/24]: Estados, API REST e integración API Twitch](https://www.twitch.tv/videos/2050175668?t=00h16m05s) - [Clase 4 [08/02/24]: Integración Supabase PostgreSQL](https://www.twitch.tv/videos/2057000877?t=00h16m10s) - [Clase 5 [16/02/24]: Variables custom](https://www.twitch.tv/videos/2063647841?t=00h12m08s) +- [Clase 6 [23/02/24]: Feature flags y migración a Radix UI](https://www.twitch.tv/videos/2070550680?t=00h17m52s) Continuación del curso desde cero de 6 horas. En esta sección más avanzada se aprenderán diferentes conceptos relacionados con el desarrollo web con Python y Reflex: Router, backend, APIs, eventos, estados, base de datos, Docker, y más... diff --git a/link_bio/README.md b/link_bio/README.md index e10d10f7..8e9db070 100644 --- a/link_bio/README.md +++ b/link_bio/README.md @@ -1,7 +1,7 @@ # Web de links de MoureDev [![Python](https://img.shields.io/badge/Python-3.11+-yellow?style=for-the-badge&logo=python&logoColor=white&labelColor=101010)](https://python.org) -[![FastAPI](https://img.shields.io/badge/Reflex-0.3.10+-5646ED?style=for-the-badge&logo=reflex&logoColor=white&labelColor=101010)](https://fastapi.tiangolo.com) +[![FastAPI](https://img.shields.io/badge/Reflex-0.4.1+-5646ED?style=for-the-badge&logo=reflex&logoColor=white&labelColor=101010)](https://fastapi.tiangolo.com) ## Proyecto desarrollado con [Python](https://www.python.org/) y [Reflex](https://reflex.dev/) que representa un sitio web personal estilo "[link in bio](https://moure.dev/)" diff --git a/link_bio/link_bio/api/ConfigCatAPI.py b/link_bio/link_bio/api/ConfigCatAPI.py index 28fd441f..da7b1fd0 100644 --- a/link_bio/link_bio/api/ConfigCatAPI.py +++ b/link_bio/link_bio/api/ConfigCatAPI.py @@ -1,6 +1,7 @@ import os import dotenv import configcatclient +import json class ConfigCatAPI: @@ -13,5 +14,6 @@ def __init__(self) -> None: if self.CONFIGCAT_SDK_KEY != None: self.configcat = configcatclient.get(self.CONFIGCAT_SDK_KEY) - def schedule(self) -> str: - self.configcat.get_value("live_schedule", "") + def schedule(self) -> dict: + response = self.configcat.get_value("live_schedule", "") + return json.loads(response) diff --git a/link_bio/link_bio/api/SupabaseAPI.py b/link_bio/link_bio/api/SupabaseAPI.py index 26dc0ca3..e1822717 100644 --- a/link_bio/link_bio/api/SupabaseAPI.py +++ b/link_bio/link_bio/api/SupabaseAPI.py @@ -19,7 +19,8 @@ def __init__(self) -> None: def featured(self) -> list[Featured]: - response = self.supabase.table("featured").select("*").execute() + response = self.supabase.table( + "featured").select("*").order("init_date", desc=True).limit(2).execute() featured_data = [] diff --git a/link_bio/link_bio/api/api.py b/link_bio/link_bio/api/api.py index 22c1c434..2a2d560f 100644 --- a/link_bio/link_bio/api/api.py +++ b/link_bio/link_bio/api/api.py @@ -3,9 +3,11 @@ from link_bio.model.Live import Live from .TwitchAPI import TwitchAPI from .SupabaseAPI import SupabaseAPI +from .ConfigCatAPI import ConfigCatAPI TWITCH_API = TwitchAPI() SUPABASE_API = SupabaseAPI() +CONFIGCAT_API = ConfigCatAPI() async def repo() -> str: @@ -18,3 +20,7 @@ async def live(user: str) -> Live: async def featured() -> list[Featured]: return SUPABASE_API.featured() + + +async def schedule() -> dict: + return CONFIGCAT_API.schedule() diff --git a/link_bio/link_bio/components/ant_components.py b/link_bio/link_bio/components/ant_components.py index 31069613..b94527bf 100644 --- a/link_bio/link_bio/components/ant_components.py +++ b/link_bio/link_bio/components/ant_components.py @@ -5,7 +5,7 @@ class FloatButton(rx.Component): library = "antd" tag = "FloatButton" - icon: rx.Var[rx.Image] + icon: rx.Var[rx.el.Img] href: rx.Var[str] target = "_blank" badge = {"dot": True, "color": Color.PRIMARY.value} diff --git a/link_bio/link_bio/components/featured_link.py b/link_bio/link_bio/components/featured_link.py index 9fcc1c73..874c40fc 100644 --- a/link_bio/link_bio/components/featured_link.py +++ b/link_bio/link_bio/components/featured_link.py @@ -1,6 +1,6 @@ import reflex as rx import link_bio.styles.styles as styles -from link_bio.styles.styles import Size +from link_bio.styles.styles import Size, Spacing from link_bio.model.Featured import Featured @@ -13,9 +13,10 @@ def featured_link(featured: Featured) -> rx.Component: ), rx.text( featured.title, + size=Spacing.VERY_SMALL.value, style=styles.button_body_style ), - spacing=Size.SMALL.value, + spacing=Spacing.SMALL.value, align_items="start" ), href=featured.url, diff --git a/link_bio/link_bio/components/footer.py b/link_bio/link_bio/components/footer.py index f058f2ff..e010f4a5 100644 --- a/link_bio/link_bio/components/footer.py +++ b/link_bio/link_bio/components/footer.py @@ -1,7 +1,7 @@ import reflex as rx import datetime import link_bio.constants as const -from link_bio.styles.styles import Size +from link_bio.styles.styles import Size, Spacing from link_bio.styles.colors import Color, TextColor from link_bio.components.ant_components import float_button @@ -17,7 +17,11 @@ def footer() -> rx.Component: rx.link( rx.box( f"© 2014-{datetime.date.today().year} ", - rx.span("MoureDev by Brais Moure", color=Color.PRIMARY.value), + rx.text( + "MoureDev by Brais Moure", + as_="span", + color=Color.PRIMARY.value + ), " v3.", padding_top=Size.DEFAULT.value ), @@ -42,12 +46,13 @@ def footer() -> rx.Component: is_external=True ), float_button( - icon=rx.Image(src="/icons/donate.svg"), + icon=rx.image(src="/icons/donate.svg"), href=const.COFFEE_URL ), + align="center", margin_bottom=Size.BIG.value, padding_bottom=Size.VERY_BIG.value, padding_x=Size.BIG.value, - spacing=Size.ZERO.value, + spacing=Spacing.ZERO.value, color=TextColor.FOOTER.value ) diff --git a/link_bio/link_bio/components/info_text.py b/link_bio/link_bio/components/info_text.py index 0faabc73..2e8c9a8e 100644 --- a/link_bio/link_bio/components/info_text.py +++ b/link_bio/link_bio/components/info_text.py @@ -5,8 +5,9 @@ def info_text(title: str, body: str) -> rx.Component: return rx.box( - rx.span( + rx.text( title, + as_="span", font_weight="bold", color=Color.PRIMARY.value ), diff --git a/link_bio/link_bio/components/link_button.py b/link_bio/link_bio/components/link_button.py index e82470d2..55018af1 100644 --- a/link_bio/link_bio/components/link_button.py +++ b/link_bio/link_bio/components/link_button.py @@ -1,6 +1,6 @@ import reflex as rx import link_bio.styles.styles as styles -from link_bio.styles.styles import Size, Color +from link_bio.styles.styles import Size, Color, Spacing def link_button(title: str, body: str, image: str, url: str, is_external=True, highlight_color=None) -> rx.Component: @@ -15,17 +15,25 @@ def link_button(title: str, body: str, image: str, url: str, is_external=True, h alt=title ), rx.vstack( - rx.text(title, style=styles.button_title_style), - rx.text(body, style=styles.button_body_style), + rx.text( + title, + size=Spacing.SMALL.value, + style=styles.button_title_style + ), + rx.text( + body, + size=Spacing.VERY_SMALL.value, + style=styles.button_body_style + ), align_items="start", - spacing=Size.SMALL.value, + spacing=Spacing.VERY_SMALL.value, padding_y=Size.SMALL.value, padding_right=Size.SMALL.value ), + align="center", width="100%" ), - border_color=highlight_color, - border_width="2px" if highlight_color != None else None + border=f"{'2px' if highlight_color != None else '0px'} solid {highlight_color}", ), href=url, is_external=is_external, diff --git a/link_bio/link_bio/components/navbar.py b/link_bio/link_bio/components/navbar.py index e6959090..08baa91d 100644 --- a/link_bio/link_bio/components/navbar.py +++ b/link_bio/link_bio/components/navbar.py @@ -9,8 +9,8 @@ def navbar() -> rx.Component: return rx.hstack( rx.link( rx.box( - rx.span("moure", color=Color.PRIMARY.value), - rx.span("dev", color=Color.SECONDARY.value), + rx.text("moure", as_="span", color=Color.PRIMARY.value), + rx.text("dev", as_="span", color=Color.SECONDARY.value), style=styles.navbar_title_style ), href=Route.INDEX.value diff --git a/link_bio/link_bio/pages/index.py b/link_bio/link_bio/pages/index.py index a89a0230..f7aac2c5 100644 --- a/link_bio/link_bio/pages/index.py +++ b/link_bio/link_bio/pages/index.py @@ -23,7 +23,10 @@ def index() -> rx.Component: navbar(), rx.center( rx.vstack( - header(live_status=PageState.live_status), + header( + live_status=PageState.live_status, + next_live=PageState.next_live + ), index_links(PageState.featured_info), sponsors(), max_width=styles.MAX_WIDTH, diff --git a/link_bio/link_bio/state/PageState.py b/link_bio/link_bio/state/PageState.py index b52c135e..10692c6f 100644 --- a/link_bio/link_bio/state/PageState.py +++ b/link_bio/link_bio/state/PageState.py @@ -1,6 +1,7 @@ from enum import Flag import reflex as rx -from link_bio.api.api import live, featured +import link_bio.utils as utils +from link_bio.api.api import live, featured, schedule from link_bio.model.Featured import Featured from link_bio.model.Live import Live @@ -10,10 +11,13 @@ class PageState(rx.State): live_status = Live(live=False, title="") + next_live: str = "" featured_info: list[Featured] async def check_live(self): self.live_status = await live(USER) + if not self.live_status.live: + self.next_live = utils.next_date(await schedule()) async def featured_links(self): self.featured_info = await featured() diff --git a/link_bio/link_bio/styles/styles.py b/link_bio/link_bio/styles/styles.py index 8c9c0808..ef47f818 100644 --- a/link_bio/link_bio/styles/styles.py +++ b/link_bio/link_bio/styles/styles.py @@ -24,6 +24,17 @@ class Size(Enum): BIG = "2em" VERY_BIG = "4em" + +class Spacing(Enum): + ZERO = "0" + VERY_SMALL = "1" + SMALL = "3" + DEFAULT = "4" + LARGE = "5" + BIG = "6" + MEDIUM_BIG = "7" + VERY_BIG = "9" + # Styles @@ -31,12 +42,12 @@ class Size(Enum): "font_family": Font.DEFAULT.value, "font_weight": FontWeight.LIGHT.value, "background_color": Color.BACKGROUND.value, - rx.Heading: { + rx.heading: { "color": TextColor.HEADER.value, "font_family": Font.TITLE.value, "font_weight": FontWeight.MEDIUM.value }, - rx.Button: { + rx.button: { "width": "100%", "height": "100%", "padding": Size.SMALL.value, @@ -49,7 +60,8 @@ class Size(Enum): "background_color": Color.SECONDARY.value } }, - rx.Link: { + rx.link: { + "color": TextColor.BODY.value, "text_decoration": "none", "_hover": {} } @@ -70,12 +82,10 @@ class Size(Enum): button_title_style = dict( font_family=Font.TITLE.value, font_weight=FontWeight.MEDIUM.value, - font_size=Size.DEFAULT.value, - color=TextColor.HEADER.value + color=TextColor.HEADER.value, ) button_body_style = dict( font_weight=FontWeight.LIGHT.value, - font_size=Size.MEDIUM.value, color=TextColor.BODY.value ) diff --git a/link_bio/link_bio/utils.py b/link_bio/link_bio/utils.py index 3aa2f14b..9d97f55b 100644 --- a/link_bio/link_bio/utils.py +++ b/link_bio/link_bio/utils.py @@ -1,3 +1,4 @@ +from datetime import date, datetime, timedelta, timezone import reflex as rx # Común @@ -37,3 +38,41 @@ def lang() -> rx.Component: {"name": "og:description", "content": courses_description}, ] courses_meta.extend(_meta) + +# Date + + +def next_date(dates: dict) -> str: + + if len(dates) == 0: + return "" + + now = datetime.now() + current_weekday = now.weekday() + current_time = now.astimezone().timetz() + + for index in range(7): + + day = str((current_weekday + index) % 7) + + if day not in dates or dates[day] == "": + continue + + time_utc = datetime.strptime( + dates[day], + "%H:%M" + ).replace(tzinfo=timezone.utc).timetz() + + time = datetime.combine(now.date(), time_utc).astimezone().timetz() + + if current_time < time or index > 0: + + next_date = now + timedelta(days=index) + + formatted_next_date = next_date.strftime( + "Hoy, %d/%m") if index == 0 else next_date.strftime("%A, %d/%m") + formatted_next_time = time.strftime("%H:%M") + + return f"{formatted_next_date} a las {formatted_next_time} ({dates[day]} UTC)" + + return "" diff --git a/link_bio/link_bio/views/courses_links.py b/link_bio/link_bio/views/courses_links.py index 47822388..1f132309 100644 --- a/link_bio/link_bio/views/courses_links.py +++ b/link_bio/link_bio/views/courses_links.py @@ -2,7 +2,7 @@ import link_bio.constants as const from link_bio.components.link_button import link_button from link_bio.components.title import title -from link_bio.styles.styles import Size, Color +from link_bio.styles.styles import Size, Color, Spacing def courses_links() -> rx.Component: @@ -60,5 +60,5 @@ def courses_links() -> rx.Component: const.YOUTUBE_SECONDARY_URL ), width="100%", - spacing=Size.DEFAULT.value, + spacing=Spacing.DEFAULT.value, ) diff --git a/link_bio/link_bio/views/header.py b/link_bio/link_bio/views/header.py index 9bde6cb2..2c6691a4 100644 --- a/link_bio/link_bio/views/header.py +++ b/link_bio/link_bio/views/header.py @@ -1,48 +1,54 @@ +from turtle import position import reflex as rx import datetime import link_bio.constants as const from link_bio.model.Live import Live -from link_bio.styles.styles import Size +from link_bio.styles.styles import Size, Spacing from link_bio.styles.colors import Color, TextColor from link_bio.components.link_icon import link_icon from link_bio.components.info_text import info_text from link_bio.components.link_button import link_button -def header(details=True, live_status=Live(live=False, title="")) -> rx.Component: +def header(details=True, live_status=Live(live=False, title=""), next_live="") -> rx.Component: return rx.vstack( rx.hstack( - rx.avatar( + rx.box( rx.cond( live_status.live, rx.link( - rx.avatar_badge( - rx.image( - src="/icons/twitch.svg", - height=Size.SMALL.value, - width=Size.SMALL.value - ), - bg=Color.PURPLE.value, - border_color=Color.PURPLE.value, - class_name="blink" + rx.image( + src="/icons/twitch.svg", + height=Size.LARGE.value, + width=Size.LARGE.value ), href=const.TWITCH_URL, - is_external=True + is_external=True, + class_name="blink", + border_radius="50%", + padding=Size.SMALL.value, + bg=Color.PURPLE.value, + position="absolute", + bottom="0", + right="0" ) ), - name="Brais Moure", - size="xl", - src="/avatar.jpg", - color=TextColor.BODY.value, - bg=Color.CONTENT.value, - padding="2px", - border="4px", - border_color=Color.PRIMARY.value + rx.avatar( + name="Brais Moure", + size=Spacing.MEDIUM_BIG.value, + src="/avatar.jpg", + radius="full", + color=TextColor.BODY.value, + bg=Color.CONTENT.value, + padding="2px", + border=f"4px solid {Color.PRIMARY.value}" + ), + position="relative" ), rx.vstack( rx.heading( "Brais Moure", - size="lg" + size=Spacing.BIG.value ), rx.text( "@mouredev", @@ -80,11 +86,14 @@ def header(details=True, live_status=Live(live=False, title="")) -> rx.Component const.LINKEDIN_URL, "LinkedIn" ), - spacing=Size.LARGE.value + spacing=Spacing.LARGE.value, + padding_top=Size.SMALL.value ), + spacing=Spacing.ZERO.value, align_items="start" ), - spacing=Size.DEFAULT.value + align="end", + spacing=Spacing.DEFAULT.value ), rx.cond( details, @@ -112,6 +121,16 @@ def header(details=True, live_status=Live(live=False, title="")) -> rx.Component "/icons/twitch.svg", const.TWITCH_URL, highlight_color=Color.PURPLE.value + ), + rx.cond( + next_live, + link_button( + "Próximo directo", + next_live, + "/icons/twitch.svg", + const.TWITCH_URL, + highlight_color=Color.PURPLE.value + ), ) ), rx.text( @@ -125,12 +144,12 @@ def header(details=True, live_status=Live(live=False, title="")) -> rx.Component color=TextColor.BODY.value ), width="100%", - spacing=Size.BIG.value + spacing=Spacing.BIG.value ) ), width="100%", - spacing=Size.BIG.value, - align_items="start" + spacing=Spacing.BIG.value, + align_items="start", ) diff --git a/link_bio/link_bio/views/index_links.py b/link_bio/link_bio/views/index_links.py index bb29ce24..fdf1fde7 100644 --- a/link_bio/link_bio/views/index_links.py +++ b/link_bio/link_bio/views/index_links.py @@ -5,7 +5,7 @@ from link_bio.routes import Route from link_bio.components.link_button import link_button from link_bio.components.title import title -from link_bio.styles.styles import Size, Color +from link_bio.styles.styles import Size, Color, Spacing def index_links(featured: list[Featured]) -> rx.Component: @@ -48,15 +48,15 @@ def index_links(featured: list[Featured]) -> rx.Component: featured, rx.vstack( title("Destacado"), - rx.responsive_grid( + rx.flex( rx.foreach( featured, featured_link ), - columns=[1, 2], - spacing=Size.DEFAULT.value + flex_direction=["column", "row"], + spacing=Spacing.DEFAULT.value ), - spacing=Size.DEFAULT.value + spacing=Spacing.DEFAULT.value ) ), @@ -106,5 +106,5 @@ def index_links(featured: list[Featured]) -> rx.Component: f"mailto:{const.EMAIL}" ), width="100%", - spacing=Size.DEFAULT.value, + spacing=Spacing.DEFAULT.value, ) diff --git a/link_bio/link_bio/views/sponsors.py b/link_bio/link_bio/views/sponsors.py index d8c4e7cd..161c0272 100644 --- a/link_bio/link_bio/views/sponsors.py +++ b/link_bio/link_bio/views/sponsors.py @@ -1,6 +1,6 @@ import reflex as rx import link_bio.constants as const -from link_bio.styles.styles import Size +from link_bio.styles.styles import Size, Spacing from link_bio.components.title import title from link_bio.components.link_sponsor import link_sponsor @@ -8,7 +8,7 @@ def sponsors() -> rx.Component: return rx.vstack( title("Colaboran"), - rx.responsive_grid( + rx.flex( link_sponsor( "/elgato.png", const.ELGATO_URL, @@ -24,10 +24,10 @@ def sponsors() -> rx.Component: const.GITHUB_STAR_URL, "Logotipo de GitHub Star" ), - spacing=Size.BIG.value, - columns=[1, 3] + spacing=Spacing.BIG.value, + flex_direction=["column", "row"] ), width="100%", align_items="start", - spacing=Size.MEDIUM.value + spacing=Spacing.DEFAULT.value ) diff --git a/link_bio/requirements.txt b/link_bio/requirements.txt index 917567f0..26010b1f 100644 --- a/link_bio/requirements.txt +++ b/link_bio/requirements.txt @@ -1,4 +1,4 @@ -reflex==0.3.10 +reflex==0.4.1 python-dotenv supabase configcat-client \ No newline at end of file