├── .gitignore ├── README.md ├── assets ├── favicon.ico └── logo.jpg ├── full_stack_python ├── __init__.py ├── articles │ ├── __init__.py │ ├── detail.py │ ├── list.py │ └── state.py ├── auth │ ├── __init__.py │ ├── forms.py │ ├── pages.py │ └── state.py ├── blog │ ├── __init__.py │ ├── add.py │ ├── detail.py │ ├── edit.py │ ├── forms.py │ ├── list.py │ ├── notfound.py │ └── state.py ├── contact │ ├── __init__.py │ ├── form.py │ ├── page.py │ └── state.py ├── full_stack_python.py ├── models.py ├── navigation │ ├── __init__.py │ ├── routes.py │ └── state.py ├── pages │ ├── __init__.py │ ├── about.py │ ├── dashboard.py │ ├── landing.py │ ├── pricing.py │ └── protected.py ├── ui │ ├── __init__.py │ ├── base.py │ ├── dashboard.py │ ├── nav.py │ └── sidebar.py └── utils │ ├── __init__.py │ └── timing.py ├── requirements.txt └── rxconfig.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.py[cod] 3 | .web 4 | __pycache__/ 5 | venv/ 6 | alembic/ 7 | alembic.ini -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pure Web Apps with Python 2 | 3 | This repo is a reference to our recent tutorial on using [Reflex](https://reflex.dev) to create full stack web applications using purely Python. 4 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/full-stack-python/4d89865a97ddae320dd1d20f48bd878f61ab22a9/assets/favicon.ico -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/full-stack-python/4d89865a97ddae320dd1d20f48bd878f61ab22a9/assets/logo.jpg -------------------------------------------------------------------------------- /full_stack_python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/full-stack-python/4d89865a97ddae320dd1d20f48bd878f61ab22a9/full_stack_python/__init__.py -------------------------------------------------------------------------------- /full_stack_python/articles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/full-stack-python/4d89865a97ddae320dd1d20f48bd878f61ab22a9/full_stack_python/articles/__init__.py -------------------------------------------------------------------------------- /full_stack_python/articles/detail.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..ui.base import base_page 4 | 5 | from . import state 6 | 7 | from ..blog.notfound import blog_post_not_found 8 | 9 | 10 | def article_detail_page() -> rx.Component: 11 | my_child = rx.cond(state.ArticlePublicState.post, rx.vstack( 12 | rx.hstack( 13 | rx.heading(state.ArticlePublicState.post.title, size="9"), 14 | align='end' 15 | ), 16 | rx.text("By ", state.ArticlePublicState.post.userinfo.user.username), 17 | rx.text(state.ArticlePublicState.post.publish_date), 18 | rx.text( 19 | state.ArticlePublicState.post.content, 20 | white_space='pre-wrap' 21 | ), 22 | spacing="5", 23 | align="center", 24 | min_height="85vh" 25 | ), 26 | blog_post_not_found() 27 | ) 28 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/articles/list.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from .. import navigation 4 | from ..ui.base import base_page 5 | from ..models import BlogPostModel 6 | from . import state 7 | 8 | def article_card_link(post: BlogPostModel): 9 | post_id = post.id 10 | if post_id is None: 11 | return rx.fragment("Not found") 12 | root_path = navigation.routes.ARTICLE_LIST_ROUTE 13 | post_detail_url = f"{root_path}/{post_id}" 14 | return rx.card( 15 | rx.link( 16 | rx.flex( 17 | rx.box( 18 | rx.heading(post.title), 19 | ), 20 | spacing="2", 21 | ), 22 | href=post_detail_url 23 | ), 24 | as_child=True, 25 | ) 26 | 27 | def article_public_list_component(columns:int=3, spacing:int=5, limit:int=100) -> rx.Component: 28 | return rx.grid( 29 | rx.foreach(state.ArticlePublicState.posts, article_card_link), 30 | columns=f'{columns}', 31 | spacing=f'{spacing}', 32 | on_mount=lambda: state.ArticlePublicState.set_limit_and_reload(limit) 33 | ) 34 | 35 | def article_public_list_page() ->rx.Component: 36 | return base_page( 37 | rx.box( 38 | rx.heading("Published Articles", size="5"), 39 | article_public_list_component(), 40 | min_height="85vh", 41 | ) 42 | ) 43 | -------------------------------------------------------------------------------- /full_stack_python/articles/state.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional, List 3 | import reflex as rx 4 | 5 | import sqlalchemy 6 | from sqlmodel import select 7 | 8 | from .. import navigation 9 | from ..auth.state import SessionState 10 | from ..models import BlogPostModel, UserInfo 11 | 12 | ARTICLE_LIST_ROUTE = navigation.routes.ARTICLE_LIST_ROUTE 13 | if ARTICLE_LIST_ROUTE.endswith("/"): 14 | ARTICLE_LIST_ROUTE = ARTICLE_LIST_ROUTE[:-1] 15 | 16 | class ArticlePublicState(SessionState): 17 | posts: List['BlogPostModel'] = [] 18 | post: Optional['BlogPostModel'] = None 19 | post_content: str = "" 20 | post_publish_active: bool = False 21 | limit: int = 20 22 | 23 | @rx.var 24 | def post_id(self): 25 | return self.router.page.params.get("post_id", "") 26 | 27 | @rx.var 28 | def post_url(self): 29 | if not self.post: 30 | return f"{ARTICLE_LIST_ROUTE}" 31 | return f"{ARTICLE_LIST_ROUTE}/{self.post.id}" 32 | 33 | def get_post_detail(self): 34 | lookups = ( 35 | (BlogPostModel.publish_active == True) & 36 | (BlogPostModel.publish_date < datetime.now()) & 37 | (BlogPostModel.id == self.post_id) 38 | ) 39 | with rx.session() as session: 40 | if self.post_id == "": 41 | self.post = None 42 | self.post_content = "" 43 | self.post_publish_active = False 44 | return 45 | sql_statement = select(BlogPostModel).options( 46 | sqlalchemy.orm.joinedload(BlogPostModel.userinfo).joinedload(UserInfo.user) 47 | ).where(lookups) 48 | result = session.exec(sql_statement).one_or_none() 49 | self.post = result 50 | if result is None: 51 | self.post_content = "" 52 | return 53 | self.post_content = self.post.content 54 | self.post_publish_active = self.post.publish_active 55 | # return 56 | 57 | def set_limit_and_reload(self, new_limit: int=5): 58 | self.limit = new_limit 59 | self.load_posts() 60 | yield 61 | 62 | def load_posts(self, *args, **kwargs): 63 | lookup_args = ( 64 | (BlogPostModel.publish_active == True) & 65 | (BlogPostModel.publish_date < datetime.now()) 66 | ) 67 | with rx.session() as session: 68 | result = session.exec( 69 | select(BlogPostModel).options( 70 | sqlalchemy.orm.joinedload(BlogPostModel.userinfo) 71 | ).where(lookup_args).limit(self.limit) 72 | ).all() 73 | self.posts = result 74 | 75 | def to_post(self): 76 | if not self.post: 77 | return rx.redirect(ARTICLE_LIST_ROUTE) 78 | return rx.redirect(f"{self.post_url}") -------------------------------------------------------------------------------- /full_stack_python/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/full-stack-python/4d89865a97ddae320dd1d20f48bd878f61ab22a9/full_stack_python/auth/__init__.py -------------------------------------------------------------------------------- /full_stack_python/auth/forms.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | from reflex_local_auth.pages.components import input_100w, MIN_WIDTH 4 | 5 | from .state import MyRegisterState 6 | 7 | def register_error() -> rx.Component: 8 | """Render the registration error message.""" 9 | return rx.cond( 10 | reflex_local_auth.RegistrationState.error_message != "", 11 | rx.callout( 12 | reflex_local_auth.RegistrationState.error_message, 13 | icon="triangle_alert", 14 | color_scheme="red", 15 | role="alert", 16 | width="100%", 17 | ), 18 | ) 19 | 20 | 21 | def my_register_form() -> rx.Component: 22 | """Render the registration form.""" 23 | return rx.form( 24 | rx.vstack( 25 | rx.heading("Create an account", size="7"), 26 | register_error(), 27 | rx.text("Username"), 28 | input_100w("username"), 29 | rx.text("Email"), 30 | input_100w("email", type='email'), 31 | rx.text("Password"), 32 | input_100w("password", type="password"), 33 | rx.text("Confirm Password"), 34 | input_100w("confirm_password", type="password"), 35 | rx.button("Sign up", width="100%"), 36 | rx.center( 37 | rx.link("Login", on_click=lambda: rx.redirect(reflex_local_auth.routes.LOGIN_ROUTE)), 38 | width="100%", 39 | ), 40 | min_width=MIN_WIDTH, 41 | ), 42 | on_submit=MyRegisterState.handle_registration_email, 43 | ) -------------------------------------------------------------------------------- /full_stack_python/auth/pages.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from reflex_local_auth.pages.login import LoginState, login_form 4 | from reflex_local_auth.pages.registration import RegistrationState, register_form 5 | 6 | from .. import navigation 7 | from ..ui.base import base_page 8 | 9 | from .forms import my_register_form 10 | from .state import SessionState 11 | 12 | def my_login_page()->rx.Component: 13 | return base_page( 14 | rx.center( 15 | rx.cond( 16 | LoginState.is_hydrated, # type: ignore 17 | rx.card(login_form()), 18 | ), 19 | min_height="85vh", 20 | ), 21 | 22 | ) 23 | 24 | def my_register_page()->rx.Component: 25 | return base_page( 26 | rx.center( 27 | rx.cond( 28 | RegistrationState.success, 29 | rx.vstack( 30 | rx.text("Registration successful!"), 31 | ), 32 | rx.card(my_register_form()), 33 | 34 | ), 35 | min_height="85vh", 36 | ) 37 | 38 | ) 39 | 40 | 41 | def my_logout_page() -> rx.Component: 42 | # Welcome Page (Index) 43 | my_child = rx.vstack( 44 | rx.heading("Are you sure you want logout?", size="7"), 45 | rx.link( 46 | rx.button("No", color_scheme="gray"), 47 | href=navigation.routes.HOME_ROUTE 48 | ), 49 | rx.button("Yes, please logout", on_click=SessionState.perform_logout), 50 | spacing="5", 51 | justify="center", 52 | align="center", 53 | # text_align="center", 54 | min_height="85vh", 55 | id='my-child' 56 | ) 57 | return base_page(my_child) 58 | 59 | -------------------------------------------------------------------------------- /full_stack_python/auth/state.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | 4 | import sqlmodel 5 | 6 | from ..models import UserInfo 7 | 8 | 9 | 10 | 11 | class SessionState(reflex_local_auth.LocalAuthState): 12 | @rx.cached_var 13 | def my_userinfo_id(self) -> str | None: 14 | if self.authenticated_user_info is None: 15 | return None 16 | return self.authenticated_user_info.id 17 | 18 | @rx.cached_var 19 | def my_user_id(self) -> str | None: 20 | if self.authenticated_user.id < 0: 21 | return None 22 | return self.authenticated_user.id 23 | 24 | @rx.cached_var 25 | def authenticated_username(self) -> str | None: 26 | if self.authenticated_user.id < 0: 27 | return None 28 | return self.authenticated_user.username 29 | 30 | @rx.cached_var 31 | def authenticated_user_info(self) -> UserInfo | None: 32 | if self.authenticated_user.id < 0: 33 | return None 34 | with rx.session() as session: 35 | result = session.exec( 36 | sqlmodel.select(UserInfo).where( 37 | UserInfo.user_id == self.authenticated_user.id 38 | ), 39 | ).one_or_none() 40 | if result is None: 41 | return None 42 | # database lookup 43 | # result.user 44 | # user_obj = result.user 45 | # print(result.user) 46 | return result 47 | 48 | def on_load(self): 49 | if not self.is_authenticated: 50 | return reflex_local_auth.LoginState.redir 51 | print(self.is_authenticated) 52 | print(self.authenticated_user_info) 53 | 54 | def perform_logout(self): 55 | self.do_logout() 56 | return rx.redirect("/") 57 | 58 | 59 | 60 | class MyRegisterState(reflex_local_auth.RegistrationState): 61 | def handle_registration( 62 | self, form_data 63 | ) -> rx.event.EventSpec | list[rx.event.EventSpec]: 64 | """Handle registration form on_submit. 65 | 66 | Set error_message appropriately based on validation results. 67 | 68 | Args: 69 | form_data: A dict of form fields and values. 70 | """ 71 | username = form_data["username"] 72 | password = form_data["password"] 73 | validation_errors = self._validate_fields( 74 | username, password, form_data["confirm_password"] 75 | ) 76 | if validation_errors: 77 | self.new_user_id = -1 78 | return validation_errors 79 | self._register_user(username, password) 80 | return self.new_user_id 81 | 82 | def handle_registration_email(self, form_data): 83 | new_user_id = self.handle_registration(form_data) 84 | if new_user_id >= 0: 85 | with rx.session() as session: 86 | session.add( 87 | UserInfo( 88 | email=form_data["email"], 89 | user_id=self.new_user_id, 90 | ) 91 | ) 92 | session.commit() 93 | return type(self).successful_registration -------------------------------------------------------------------------------- /full_stack_python/blog/__init__.py: -------------------------------------------------------------------------------- 1 | from .add import blog_post_add_page 2 | from .detail import blog_post_detail_page 3 | from .edit import blog_post_edit_page 4 | from .list import blog_post_list_page 5 | from .state import BlogPostState 6 | 7 | __all__= [ 8 | 'blog_post_add_page', 9 | 'blog_post_detail_page', 10 | 'blog_post_edit_page', 11 | 'blog_post_list_page', 12 | 'BlogPostState' 13 | ] -------------------------------------------------------------------------------- /full_stack_python/blog/add.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | from ..ui.base import base_page 4 | from . import forms 5 | 6 | @reflex_local_auth.require_login 7 | def blog_post_add_page() -> rx.Component: 8 | my_form = forms.blog_post_add_form() 9 | my_child = rx.vstack( 10 | rx.heading("New Blog Post", size="9"), 11 | rx.desktop_only( 12 | rx.box( 13 | my_form, 14 | width='50vw' 15 | ) 16 | ), 17 | rx.tablet_only( 18 | rx.box( 19 | my_form, 20 | width='75vw' 21 | ) 22 | ), 23 | rx.mobile_only( 24 | rx.box( 25 | my_form, 26 | width='95vw' 27 | ) 28 | ), 29 | spacing="5", 30 | align="center", 31 | min_height="95vh", 32 | ) 33 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/blog/detail.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..ui.base import base_page 4 | 5 | from . import state 6 | from .notfound import blog_post_not_found 7 | # @rx.page(route='/about') 8 | def blog_post_detail_page() -> rx.Component: 9 | can_edit = True 10 | edit_link = rx.link("Edit", href=f"{state.BlogPostState.blog_post_edit_url}") 11 | edit_link_el = rx.cond( 12 | can_edit, 13 | edit_link, 14 | rx.fragment("") 15 | ) 16 | my_child = rx.cond(state.BlogPostState.post, rx.vstack( 17 | rx.hstack( 18 | rx.heading(state.BlogPostState.post.title, size="9"), 19 | edit_link_el, 20 | align='end' 21 | ), 22 | rx.text("User info id ", state.BlogPostState.post.userinfo_id), 23 | rx.text("User info: ", state.BlogPostState.post.userinfo.to_string()), 24 | rx.text("User: ", state.BlogPostState.post.userinfo.user.to_string()), 25 | rx.text(state.BlogPostState.post.publish_date), 26 | rx.text( 27 | state.BlogPostState.post.content, 28 | white_space='pre-wrap' 29 | ), 30 | spacing="5", 31 | align="center", 32 | min_height="85vh" 33 | ), 34 | blog_post_not_found() 35 | ) 36 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/blog/edit.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | from ..ui.base import base_page 4 | 5 | 6 | from . import forms 7 | 8 | from .state import BlogEditFormState 9 | from .notfound import blog_post_not_found 10 | 11 | @reflex_local_auth.require_login 12 | def blog_post_edit_page() -> rx.Component: 13 | my_form = forms.blog_post_edit_form() 14 | post = BlogEditFormState.post 15 | my_child = rx.cond(post, 16 | rx.vstack( 17 | rx.heading("Editing ", post.title, size="9"), 18 | rx.desktop_only( 19 | rx.box( 20 | my_form, 21 | width='50vw' 22 | ) 23 | ), 24 | rx.tablet_only( 25 | rx.box( 26 | my_form, 27 | width='75vw' 28 | ) 29 | ), 30 | rx.mobile_only( 31 | rx.box( 32 | my_form, 33 | width='95vw' 34 | ) 35 | ), 36 | spacing="5", 37 | align="center", 38 | min_height="95vh", 39 | ), 40 | blog_post_not_found() 41 | ) 42 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/blog/forms.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | 4 | from .state import ( 5 | BlogAddPostFormState, 6 | BlogEditFormState 7 | ) 8 | 9 | 10 | def blog_post_add_form() -> rx.Component: 11 | return rx.form( 12 | rx.vstack( 13 | rx.hstack( 14 | rx.input( 15 | name="title", 16 | placeholder="Title", 17 | required=True, 18 | type='text', 19 | width='100%', 20 | ), 21 | width='100%' 22 | ), 23 | rx.text_area( 24 | name='content', 25 | placeholder="Your message", 26 | required=True, 27 | height='50vh', 28 | width='100%', 29 | ), 30 | rx.button("Submit", type="submit"), 31 | ), 32 | on_submit=BlogAddPostFormState.handle_submit, 33 | reset_on_submit=True, 34 | ) 35 | 36 | 37 | 38 | 39 | def blog_post_edit_form() -> rx.Component: 40 | post = BlogEditFormState.post 41 | title = post.title 42 | publish_active = post.publish_active 43 | post_content = BlogEditFormState.post_content 44 | return rx.form( 45 | rx.box( 46 | rx.input( 47 | type='hidden', 48 | name='post_id', 49 | value=post.id 50 | ), 51 | display='none' 52 | ), 53 | rx.vstack( 54 | rx.hstack( 55 | rx.input( 56 | default_value=title, 57 | name="title", 58 | placeholder="Title", 59 | required=True, 60 | type='text', 61 | width='100%', 62 | ), 63 | width='100%' 64 | ), 65 | rx.text_area( 66 | value = post_content, 67 | on_change = BlogEditFormState.set_post_content, 68 | name='content', 69 | placeholder="Your message", 70 | required=True, 71 | height='50vh', 72 | width='100%', 73 | ), 74 | rx.flex( 75 | rx.switch( 76 | default_checked=BlogEditFormState.post_publish_active, 77 | on_change=BlogEditFormState.set_post_publish_active, 78 | name='publish_active', 79 | ), 80 | rx.text("Publish Active"), 81 | spacing="2", 82 | ), 83 | rx.cond( 84 | BlogEditFormState.post_publish_active, 85 | rx.box( 86 | rx.hstack( 87 | rx.input( 88 | default_value=BlogEditFormState.publish_display_date, 89 | type='date', 90 | name='publish_date', 91 | width='100%' 92 | ), 93 | rx.input( 94 | default_value=BlogEditFormState.publish_display_time, 95 | type='time', 96 | name='publish_time', 97 | width='100%' 98 | ), 99 | width='100%' 100 | ), 101 | width='100%' 102 | ) 103 | ), 104 | rx.button("Submit", type="submit"), 105 | ), 106 | on_submit=BlogEditFormState.handle_submit, 107 | ) -------------------------------------------------------------------------------- /full_stack_python/blog/list.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | from .. import navigation 4 | from ..ui.base import base_page 5 | from ..models import BlogPostModel 6 | from . import state 7 | 8 | def blog_post_detail_link(child: rx.Component, post: BlogPostModel): 9 | if post is None: 10 | return rx.fragment(child) 11 | post_id = post.id 12 | if post_id is None: 13 | return rx.fragment(child) 14 | root_path = navigation.routes.BLOG_POSTS_ROUTE 15 | post_detail_url = f"{root_path}/{post_id}" 16 | return rx.link( 17 | child, 18 | rx.heading("by ", post.userinfo.email), 19 | href=post_detail_url 20 | ) 21 | 22 | def blog_post_list_item(post: BlogPostModel): 23 | return rx.box( 24 | blog_post_detail_link( 25 | rx.heading(post.title), 26 | 27 | post 28 | ), 29 | padding='1em' 30 | ) 31 | 32 | # def foreach_callback(text): 33 | # return rx.box(rx.text(text)) 34 | 35 | @reflex_local_auth.require_login 36 | def blog_post_list_page() ->rx.Component: 37 | return base_page( 38 | rx.vstack( 39 | rx.heading("Blog Posts", size="5"), 40 | rx.link( 41 | rx.button("New Post"), 42 | href=navigation.routes.BLOG_POST_ADD_ROUTE 43 | ), 44 | # rx.foreach(["abc", "abc", "cde"], foreach_callback), 45 | rx.foreach(state.BlogPostState.posts, blog_post_list_item), 46 | spacing="5", 47 | align="center", 48 | min_height="85vh", 49 | ) 50 | ) 51 | -------------------------------------------------------------------------------- /full_stack_python/blog/notfound.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | def blog_post_not_found() -> rx.Component: 4 | return rx.hstack( 5 | rx.heading("Blog Post Not Found"),spacing="5", 6 | align="center", 7 | min_height="85vh") -------------------------------------------------------------------------------- /full_stack_python/blog/state.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional, List 3 | import reflex as rx 4 | 5 | import sqlalchemy 6 | from sqlmodel import select 7 | 8 | from .. import navigation 9 | from ..auth.state import SessionState 10 | from ..models import BlogPostModel, UserInfo 11 | 12 | BLOG_POSTS_ROUTE = navigation.routes.BLOG_POSTS_ROUTE 13 | if BLOG_POSTS_ROUTE.endswith("/"): 14 | BLOG_POSTS_ROUTE = BLOG_POSTS_ROUTE[:-1] 15 | 16 | class BlogPostState(SessionState): 17 | posts: List['BlogPostModel'] = [] 18 | post: Optional['BlogPostModel'] = None 19 | post_content: str = "" 20 | post_publish_active: bool = False 21 | 22 | @rx.var 23 | def blog_post_id(self): 24 | return self.router.page.params.get("blog_id", "") 25 | 26 | @rx.var 27 | def blog_post_url(self): 28 | if not self.post: 29 | return f"{BLOG_POSTS_ROUTE}" 30 | return f"{BLOG_POSTS_ROUTE}/{self.post.id}" 31 | 32 | @rx.var 33 | def blog_post_edit_url(self): 34 | if not self.post: 35 | return f"{BLOG_POSTS_ROUTE}" 36 | return f"{BLOG_POSTS_ROUTE}/{self.post.id}/edit" 37 | 38 | def get_post_detail(self): 39 | if self.my_userinfo_id is None: 40 | self.post = None 41 | self.post_content = "" 42 | self.post_publish_active = False 43 | return 44 | lookups = ( 45 | (BlogPostModel.userinfo_id == self.my_userinfo_id) & 46 | (BlogPostModel.id == self.blog_post_id) 47 | ) 48 | with rx.session() as session: 49 | if self.blog_post_id == "": 50 | self.post = None 51 | return 52 | sql_statement = select(BlogPostModel).options( 53 | sqlalchemy.orm.joinedload(BlogPostModel.userinfo).joinedload(UserInfo.user) 54 | ).where(lookups) 55 | result = session.exec(sql_statement).one_or_none() 56 | # if result.userinfo: # db lookup 57 | # print('working') 58 | # result.userinfo.user # db lookup 59 | self.post = result 60 | if result is None: 61 | self.post_content = "" 62 | return 63 | self.post_content = self.post.content 64 | self.post_publish_active = self.post.publish_active 65 | # return 66 | 67 | 68 | def load_posts(self, *args, **kwargs): 69 | # if published_only: 70 | # lookup_args = ( 71 | # (BlogPostModel.publish_active == True) & 72 | # (BlogPostModel.publish_date < datetime.now()) 73 | # ) 74 | with rx.session() as session: 75 | result = session.exec( 76 | select(BlogPostModel).options( 77 | sqlalchemy.orm.joinedload(BlogPostModel.userinfo) 78 | ).where(BlogPostModel.userinfo_id == self.my_userinfo_id) 79 | ).all() 80 | self.posts = result 81 | # return 82 | 83 | def add_post(self, form_data:dict): 84 | with rx.session() as session: 85 | post = BlogPostModel(**form_data) 86 | # print("adding", post) 87 | session.add(post) 88 | session.commit() 89 | session.refresh(post) # post.id 90 | # print("added", post) 91 | self.post = post 92 | 93 | def save_post_edits(self, post_id:int, updated_data:dict): 94 | with rx.session() as session: 95 | post = session.exec( 96 | select(BlogPostModel).where( 97 | BlogPostModel.id == post_id 98 | ) 99 | ).one_or_none() 100 | if post is None: 101 | return 102 | for key, value in updated_data.items(): 103 | setattr(post, key, value) 104 | session.add(post) 105 | session.commit() 106 | session.refresh(post) 107 | self.post = post 108 | 109 | def to_blog_post(self, edit_page=False): 110 | if not self.post: 111 | return rx.redirect(BLOG_POSTS_ROUTE) 112 | if edit_page: 113 | return rx.redirect(f"{self.blog_post_edit_url}") 114 | return rx.redirect(f"{self.blog_post_url}") 115 | 116 | 117 | class BlogAddPostFormState(BlogPostState): 118 | form_data: dict = {} 119 | 120 | def handle_submit(self, form_data): 121 | data = form_data.copy() 122 | if self.my_userinfo_id is not None: 123 | data['userinfo_id'] = self.my_userinfo_id 124 | self.form_data = data 125 | self.add_post(data) 126 | return self.to_blog_post(edit_page=True) 127 | 128 | 129 | class BlogEditFormState(BlogPostState): 130 | form_data: dict = {} 131 | # post_content: str = "" 132 | 133 | @rx.var 134 | def publish_display_date(self) -> str: 135 | # return "2023-12-01" # YYYY-MM-DD 136 | if not self.post: 137 | return datetime.now().strftime("%Y-%m-%d") 138 | if not self.post.publish_date: 139 | return datetime.now().strftime("%Y-%m-%d") 140 | return self.post.publish_date.strftime("%Y-%m-%d") 141 | 142 | @rx.var 143 | def publish_display_time(self) -> str: 144 | if not self.post: 145 | return datetime.now().strftime("%H:%M:%S") 146 | if not self.post.publish_date: 147 | return datetime.now().strftime("%H:%M:%S") 148 | return self.post.publish_date.strftime("%H:%M:%S") 149 | 150 | def handle_submit(self, form_data): 151 | self.form_data = form_data 152 | post_id = form_data.pop('post_id') 153 | publish_date = None 154 | if 'publish_date' in form_data: 155 | publish_date = form_data.pop('publish_date') 156 | publish_time = None 157 | if 'publish_time' in form_data: 158 | publish_time = form_data.pop('publish_time') 159 | publish_input_string = f"{publish_date} {publish_time}" 160 | try: 161 | final_publish_date = datetime.strptime(publish_input_string, '%Y-%m-%d %H:%M:%S') 162 | except: 163 | final_publish_date = None 164 | publish_active = False 165 | if 'publish_active' in form_data: 166 | publish_active = form_data.pop('publish_active') == "on" 167 | updated_data = {**form_data} 168 | updated_data['publish_active'] = publish_active 169 | updated_data['publish_date'] = final_publish_date 170 | self.save_post_edits(post_id, updated_data) 171 | return self.to_blog_post() -------------------------------------------------------------------------------- /full_stack_python/contact/__init__.py: -------------------------------------------------------------------------------- 1 | from .form import contact_form 2 | from .state import ContactState 3 | from .page import contact_page, contact_entries_list_page 4 | 5 | __all__ = [ 6 | 'contact_form', 7 | 'ContactState', 8 | 'contact_page', 9 | 'contact_entries_list_page' 10 | ] -------------------------------------------------------------------------------- /full_stack_python/contact/form.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..auth.state import SessionState 4 | from .state import ContactState 5 | 6 | def contact_form() -> rx.Component: 7 | return rx.form( 8 | # rx.cond( 9 | # SessionState.my_user_id, 10 | # rx.box( 11 | # rx.input( 12 | # type='hidden', 13 | # name='user_id', 14 | # value=SessionState.my_user_id 15 | # ), 16 | # display='none' 17 | # ), 18 | # rx.fragment('') 19 | # ), 20 | rx.vstack( 21 | rx.hstack( 22 | rx.input( 23 | name="first_name", 24 | placeholder="First Name", 25 | required=True, 26 | type='text', 27 | width='100%', 28 | ), 29 | rx.input( 30 | name="last_name", 31 | placeholder="Last Name", 32 | type='text', 33 | width='100%', 34 | ), 35 | width='100%' 36 | ), 37 | rx.input( 38 | name='email', 39 | placeholder='Your email', 40 | type='email', 41 | width='100%', 42 | ), 43 | rx.text_area( 44 | name='message', 45 | placeholder="Your message", 46 | required=True, 47 | width='100%', 48 | ), 49 | rx.button("Submit", type="submit"), 50 | ), 51 | on_submit=ContactState.handle_submit, 52 | reset_on_submit=True, 53 | ) -------------------------------------------------------------------------------- /full_stack_python/contact/page.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | from ..ui.base import base_page 3 | from ..models import ContactEntryModel 4 | from . import form, state 5 | 6 | def contact_entry_list_item(contact: ContactEntryModel): 7 | return rx.box( 8 | rx.heading(contact.first_name), 9 | rx.text("Message:", contact.message), 10 | rx.cond(contact.user_id, 11 | rx.text("User Id:", f"{contact.user_id}",), 12 | rx.fragment("")), 13 | padding='1em' 14 | ) 15 | 16 | # def foreach_callback(text): 17 | # return rx.box(rx.text(text)) 18 | 19 | def contact_entries_list_page() ->rx.Component: 20 | return base_page( 21 | rx.vstack( 22 | rx.heading("Contact Entries", size="5"), 23 | # rx.foreach(["abc", "abc", "cde"], foreach_callback), 24 | rx.foreach(state.ContactState.entries, contact_entry_list_item), 25 | spacing="5", 26 | align="center", 27 | min_height="85vh", 28 | ) 29 | ) 30 | 31 | def contact_page() -> rx.Component: 32 | 33 | my_child = rx.vstack( 34 | rx.heading("Contact Us", size="9"), 35 | rx.cond(state.ContactState.did_submit, state.ContactState.thank_you, ""), 36 | rx.desktop_only( 37 | rx.box( 38 | form.contact_form(), 39 | width='50vw' 40 | ) 41 | ), 42 | rx.tablet_only( 43 | rx.box( 44 | form.contact_form(), 45 | width='75vw' 46 | ) 47 | ), 48 | rx.mobile_only( 49 | rx.box( 50 | form.contact_form(), 51 | width='95vw' 52 | ) 53 | ), 54 | spacing="5", 55 | justify="center", 56 | align="center", 57 | min_height="85vh", 58 | id='my-child' 59 | ) 60 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/contact/state.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import asyncio 3 | import reflex as rx 4 | 5 | from sqlmodel import select 6 | 7 | 8 | from ..auth.state import SessionState 9 | from ..models import ContactEntryModel 10 | 11 | class ContactState(SessionState): 12 | form_data: dict = {} 13 | entries: List['ContactEntryModel'] = [] 14 | did_submit: bool = False 15 | 16 | @rx.var 17 | def thank_you(self): 18 | first_name = self.form_data.get("first_name") or "" 19 | return f"Thank you {first_name}".strip() + "!" 20 | 21 | async def handle_submit(self, form_data: dict): 22 | """Handle the form submit.""" 23 | # print(form_data) 24 | self.form_data = form_data 25 | data = {} 26 | for k,v in form_data.items(): 27 | if v == "" or v is None: 28 | continue 29 | data[k] = v 30 | if self.my_user_id is not None: 31 | data['user_id'] = self.my_user_id 32 | if self.my_userinfo_id is not None: 33 | data['userinfo_id'] = self.my_userinfo_id 34 | with rx.session() as session: 35 | db_entry = ContactEntryModel( 36 | **data 37 | ) 38 | session.add(db_entry) 39 | session.commit() 40 | self.did_submit = True 41 | yield 42 | await asyncio.sleep(2) 43 | self.did_submit = False 44 | yield 45 | 46 | def list_entries(self): 47 | with rx.session() as session: 48 | entries = session.exec( 49 | select(ContactEntryModel) 50 | ).all() 51 | self.entries = entries -------------------------------------------------------------------------------- /full_stack_python/full_stack_python.py: -------------------------------------------------------------------------------- 1 | """Welcome to Reflex! This file outlines the steps to create a basic app.""" 2 | 3 | import reflex as rx 4 | import reflex_local_auth 5 | 6 | from rxconfig import config 7 | from .ui.base import base_page 8 | 9 | from .auth.pages import ( 10 | my_login_page, 11 | my_register_page, 12 | my_logout_page 13 | ) 14 | from .auth.state import SessionState 15 | 16 | 17 | from .articles.detail import article_detail_page 18 | from .articles.list import article_public_list_page, article_public_list_component 19 | from .articles.state import ArticlePublicState 20 | 21 | from . import blog, contact, navigation, pages 22 | 23 | def index() -> rx.Component: 24 | return base_page( 25 | rx.cond(SessionState.is_authenticated, 26 | pages.dashboard_component(), 27 | pages.landing_component(), 28 | ) 29 | ) 30 | 31 | 32 | 33 | app = rx.App( 34 | theme=rx.theme( 35 | appearance="dark", 36 | has_background=True, 37 | panel_background="solid", 38 | scaling="90%", 39 | radius="medium", 40 | accent_color="sky" 41 | ) 42 | 43 | ) 44 | app.add_page(index, 45 | on_load=ArticlePublicState.load_posts 46 | ) 47 | # reflex_local_auth pages 48 | app.add_page( 49 | my_login_page, 50 | route=reflex_local_auth.routes.LOGIN_ROUTE, 51 | title="Login", 52 | ) 53 | app.add_page( 54 | my_register_page, 55 | route=reflex_local_auth.routes.REGISTER_ROUTE, 56 | title="Register", 57 | ) 58 | 59 | app.add_page( 60 | my_logout_page, 61 | route=navigation.routes.LOGOUT_ROUTE, 62 | title="Logout", 63 | ) 64 | 65 | # my pages 66 | app.add_page(pages.about_page, 67 | route=navigation.routes.ABOUT_US_ROUTE) 68 | 69 | app.add_page( 70 | pages.protected_page, 71 | route="/protected/", 72 | on_load=SessionState.on_load 73 | ) 74 | 75 | 76 | app.add_page( 77 | article_public_list_page, 78 | route=navigation.routes.ARTICLE_LIST_ROUTE, 79 | on_load=ArticlePublicState.load_posts 80 | ) 81 | 82 | app.add_page( 83 | article_detail_page, 84 | route=f"{navigation.routes.ARTICLE_LIST_ROUTE}/[post_id]", 85 | on_load=ArticlePublicState.get_post_detail 86 | ) 87 | 88 | 89 | app.add_page( 90 | blog.blog_post_list_page, 91 | route=navigation.routes.BLOG_POSTS_ROUTE, 92 | on_load=blog.BlogPostState.load_posts 93 | 94 | ) 95 | 96 | app.add_page( 97 | blog.blog_post_add_page, 98 | route=navigation.routes.BLOG_POST_ADD_ROUTE 99 | ) 100 | 101 | app.add_page( 102 | blog.blog_post_detail_page, 103 | route="/blog/[blog_id]", 104 | on_load=blog.BlogPostState.get_post_detail 105 | ) 106 | 107 | app.add_page( 108 | blog.blog_post_edit_page, 109 | route="/blog/[blog_id]/edit", 110 | on_load=blog.BlogPostState.get_post_detail 111 | ) 112 | 113 | app.add_page(contact.contact_page, 114 | route=navigation.routes.CONTACT_US_ROUTE) 115 | app.add_page( 116 | contact.contact_entries_list_page, 117 | route=navigation.routes.CONTACT_ENTRIES_ROUTE, 118 | on_load=contact.ContactState.list_entries 119 | ) 120 | app.add_page(pages.pricing_page, 121 | route=navigation.routes.PRICING_ROUTE) 122 | -------------------------------------------------------------------------------- /full_stack_python/models.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from datetime import datetime 3 | import reflex as rx 4 | from reflex_local_auth.user import LocalUser 5 | 6 | import sqlalchemy 7 | from sqlmodel import Field, Relationship 8 | 9 | from . import utils 10 | 11 | class UserInfo(rx.Model, table=True): 12 | email: str 13 | user_id: int = Field(foreign_key='localuser.id') 14 | user: LocalUser | None = Relationship() # LocalUser instance 15 | posts: List['BlogPostModel'] = Relationship( 16 | back_populates='userinfo' 17 | ) 18 | contact_entries: List['ContactEntryModel'] = Relationship( 19 | back_populates='userinfo' 20 | ) 21 | created_at: datetime = Field( 22 | default_factory=utils.timing.get_utc_now, 23 | sa_type=sqlalchemy.DateTime(timezone=True), 24 | sa_column_kwargs={ 25 | 'server_default': sqlalchemy.func.now() 26 | }, 27 | nullable=False 28 | ) 29 | updated_at: datetime = Field( 30 | default_factory=utils.timing.get_utc_now, 31 | sa_type=sqlalchemy.DateTime(timezone=True), 32 | sa_column_kwargs={ 33 | 'onupdate': sqlalchemy.func.now(), 34 | 'server_default': sqlalchemy.func.now() 35 | }, 36 | nullable=False 37 | ) 38 | 39 | 40 | class BlogPostModel(rx.Model, table=True): 41 | # user 42 | # id: int -> primary key 43 | userinfo_id: int = Field(default=None, foreign_key="userinfo.id") 44 | userinfo: Optional['UserInfo'] = Relationship(back_populates="posts") 45 | title: str 46 | content: str 47 | created_at: datetime = Field( 48 | default_factory=utils.timing.get_utc_now, 49 | sa_type=sqlalchemy.DateTime(timezone=True), 50 | sa_column_kwargs={ 51 | 'server_default': sqlalchemy.func.now() 52 | }, 53 | nullable=False 54 | ) 55 | updated_at: datetime = Field( 56 | default_factory=utils.timing.get_utc_now, 57 | sa_type=sqlalchemy.DateTime(timezone=True), 58 | sa_column_kwargs={ 59 | 'onupdate': sqlalchemy.func.now(), 60 | 'server_default': sqlalchemy.func.now() 61 | }, 62 | nullable=False 63 | ) 64 | publish_active: bool = False 65 | publish_date: datetime = Field( 66 | default=None, 67 | sa_type=sqlalchemy.DateTime(timezone=True), 68 | sa_column_kwargs={}, 69 | nullable=True 70 | ) 71 | 72 | 73 | 74 | class ContactEntryModel(rx.Model, table=True): 75 | user_id: int | None = None 76 | userinfo_id: int = Field(default=None, foreign_key="userinfo.id") 77 | userinfo: Optional['UserInfo'] = Relationship(back_populates="contact_entries") 78 | first_name: str 79 | last_name: str | None = None 80 | email: str | None = None # = Field(nullable=True) 81 | message: str 82 | created_at: datetime = Field( 83 | default_factory=utils.timing.get_utc_now, 84 | sa_type=sqlalchemy.DateTime(timezone=True), 85 | sa_column_kwargs={ 86 | 'server_default': sqlalchemy.func.now() 87 | }, 88 | nullable=False 89 | ) -------------------------------------------------------------------------------- /full_stack_python/navigation/__init__.py: -------------------------------------------------------------------------------- 1 | from . import routes 2 | from .state import NavState 3 | 4 | 5 | __all__ = [ 6 | 'routes', 7 | 'NavState' 8 | ] -------------------------------------------------------------------------------- /full_stack_python/navigation/routes.py: -------------------------------------------------------------------------------- 1 | HOME_ROUTE="/" 2 | ABOUT_US_ROUTE="/about" 3 | ARTICLE_LIST_ROUTE="/articles" 4 | BLOG_POSTS_ROUTE="/blog" 5 | BLOG_POST_ADD_ROUTE="/blog/add" 6 | CONTACT_US_ROUTE="/contact" 7 | CONTACT_ENTRIES_ROUTE="/contact/entries" 8 | LOGOUT_ROUTE="/logout" 9 | PRICING_ROUTE="/pricing" -------------------------------------------------------------------------------- /full_stack_python/navigation/state.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | from . import routes 4 | 5 | class NavState(rx.State): 6 | def to_home(self): 7 | return rx.redirect(routes.HOME_ROUTE) 8 | 9 | def to_register(self): 10 | return rx.redirect(reflex_local_auth.routes.REGISTER_ROUTE) 11 | 12 | def to_login(self): 13 | return rx.redirect(reflex_local_auth.routes.LOGIN_ROUTE) 14 | 15 | def to_logout(self): 16 | return rx.redirect(routes.LOGOUT_ROUTE) 17 | 18 | def to_about_us(self): 19 | return rx.redirect(routes.ABOUT_US_ROUTE) 20 | 21 | def to_articles(self): 22 | return rx.redirect(routes.ARTICLE_LIST_ROUTE) 23 | 24 | def to_blog(self): 25 | return rx.redirect(routes.BLOG_POSTS_ROUTE) 26 | def to_blog_add(self): 27 | return rx.redirect(routes.BLOG_POST_ADD_ROUTE) 28 | def to_blog_create(self): 29 | return self.to_blog_add() 30 | def to_contact(self): 31 | return rx.redirect(routes.CONTACT_US_ROUTE) 32 | def to_pricing(self): 33 | return rx.redirect(routes.PRICING_ROUTE) 34 | -------------------------------------------------------------------------------- /full_stack_python/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .about import about_page 2 | from .dashboard import dashboard_component 3 | from .landing import landing_component 4 | from .pricing import pricing_page 5 | from .protected import protected_page 6 | # from .contact import contact_page 7 | 8 | __all__ = [ 9 | 'about_page', 10 | # 'contact_page', 11 | 'dashboard_component', 12 | 'landing_component', 13 | 'pricing_page', 14 | 'protected_page' 15 | ] -------------------------------------------------------------------------------- /full_stack_python/pages/about.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..ui.base import base_page 4 | 5 | # @rx.page(route='/about') 6 | def about_page() -> rx.Component: 7 | my_child = rx.vstack( 8 | rx.heading("About Us", size="9"), 9 | rx.text( 10 | "Something cool about us.", 11 | ), 12 | spacing="5", 13 | justify="center", 14 | align="center", 15 | min_height="85vh", 16 | id='my-child' 17 | ) 18 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/pages/dashboard.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..articles.list import article_public_list_component 4 | 5 | def dashboard_component() -> rx.Component: 6 | return rx.box( 7 | rx.heading("Welcome back", size='2'), 8 | rx.divider(margin_top='1em', margin_bottom='1em'), 9 | article_public_list_component(columns=3, limit=20), 10 | min_height="85vh", 11 | ) -------------------------------------------------------------------------------- /full_stack_python/pages/landing.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from .. import navigation 4 | from ..articles.list import article_public_list_component 5 | 6 | 7 | def landing_component() -> rx.Component: 8 | return rx.vstack( 9 | # rx.theme_panel(default_open=True), 10 | rx.heading("Welcome to SaaS", size="9"), 11 | rx.link( 12 | rx.button("About us", color_scheme='gray'), 13 | href=navigation.routes.ABOUT_US_ROUTE 14 | ), 15 | rx.divider(), 16 | rx.heading("Recent Articles", size="5"), 17 | article_public_list_component(columns=1, limit=1), 18 | spacing="5", 19 | justify="center", 20 | align="center", 21 | # text_align="center", 22 | min_height="85vh", 23 | id='my-child' 24 | ) -------------------------------------------------------------------------------- /full_stack_python/pages/pricing.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..ui.base import base_page 4 | 5 | def feature_item(feature: str) -> rx.Component: 6 | return rx.hstack( 7 | rx.icon( 8 | "check", color=rx.color("blue", 9), size=21 9 | ), 10 | rx.text(feature, size="4", weight="regular"), 11 | ) 12 | 13 | 14 | def standard_features() -> rx.Component: 15 | return rx.vstack( 16 | feature_item("40 credits for image generation"), 17 | feature_item("Credits never expire"), 18 | feature_item("High quality images"), 19 | feature_item("Commercial license"), 20 | spacing="3", 21 | width="100%", 22 | align_items="start", 23 | ) 24 | 25 | 26 | def popular_features() -> rx.Component: 27 | return rx.vstack( 28 | feature_item("250 credits for image generation"), 29 | feature_item("+30% Extra free credits"), 30 | feature_item("Credits never expire"), 31 | feature_item("High quality images"), 32 | feature_item("Commercial license"), 33 | spacing="3", 34 | width="100%", 35 | align_items="start", 36 | ) 37 | 38 | 39 | def pricing_card_standard() -> rx.Component: 40 | return rx.vstack( 41 | rx.hstack( 42 | rx.hstack( 43 | rx.text( 44 | "$14.99", 45 | trim="both", 46 | as_="s", 47 | size="3", 48 | weight="regular", 49 | opacity=0.8, 50 | ), 51 | rx.text( 52 | "$3.99", 53 | trim="both", 54 | size="6", 55 | weight="regular", 56 | ), 57 | width="100%", 58 | spacing="2", 59 | align_items="end", 60 | ), 61 | height="35px", 62 | align_items="center", 63 | justify="between", 64 | width="100%", 65 | ), 66 | rx.text( 67 | "40 Image Credits", 68 | weight="bold", 69 | size="7", 70 | width="100%", 71 | text_align="left", 72 | ), 73 | standard_features(), 74 | rx.spacer(), 75 | rx.button( 76 | "Purchase", 77 | size="3", 78 | variant="outline", 79 | width="100%", 80 | color_scheme="blue", 81 | ), 82 | spacing="6", 83 | border=f"1.5px solid {rx.color('gray', 5)}", 84 | background=rx.color("gray", 1), 85 | padding="28px", 86 | width="100%", 87 | max_width="400px", 88 | min_height="475px", 89 | border_radius="0.5rem", 90 | ) 91 | 92 | 93 | def pricing_card_popular() -> rx.Component: 94 | return rx.vstack( 95 | rx.hstack( 96 | rx.hstack( 97 | rx.text( 98 | "$69.99", 99 | trim="both", 100 | as_="s", 101 | size="3", 102 | weight="regular", 103 | opacity=0.8, 104 | ), 105 | rx.text( 106 | "$18.99", 107 | trim="both", 108 | size="6", 109 | weight="regular", 110 | ), 111 | width="100%", 112 | spacing="2", 113 | align_items="end", 114 | ), 115 | rx.badge( 116 | "POPULAR", 117 | size="2", 118 | radius="full", 119 | variant="soft", 120 | color_scheme="blue", 121 | ), 122 | align_items="center", 123 | justify="between", 124 | height="35px", 125 | width="100%", 126 | ), 127 | rx.text( 128 | "250 Image Credits", 129 | weight="bold", 130 | size="7", 131 | width="100%", 132 | text_align="left", 133 | ), 134 | popular_features(), 135 | rx.spacer(), 136 | rx.button( 137 | "Purchase", 138 | size="3", 139 | width="100%", 140 | color_scheme="blue", 141 | ), 142 | spacing="6", 143 | border=f"1.5px solid {rx.color('blue', 6)}", 144 | background=rx.color("blue", 1), 145 | padding="28px", 146 | width="100%", 147 | max_width="400px", 148 | min_height="475px", 149 | border_radius="0.5rem", 150 | ) 151 | 152 | 153 | def pricing_cards() -> rx.Component: 154 | return rx.flex( 155 | pricing_card_standard(), 156 | pricing_card_popular(), 157 | spacing="4", 158 | flex_direction=["column", "column", "row"], 159 | width="100%", 160 | align_items="center", 161 | ) 162 | 163 | def pricing_page() -> rx.Component: 164 | my_child = rx.vstack( 165 | rx.heading("Pricing", size="9"), 166 | pricing_cards(), 167 | spacing="5", 168 | justify="center", 169 | align="center", 170 | min_height="85vh", 171 | id='my-child' 172 | ) 173 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/pages/protected.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | 4 | from ..ui.base import base_page 5 | 6 | @reflex_local_auth.require_login 7 | def protected_page() -> rx.Component: 8 | my_child = rx.vstack( 9 | rx.heading("Protect Page", size="9"), 10 | rx.text( 11 | "Something cool about us.", 12 | ), 13 | spacing="5", 14 | justify="center", 15 | align="center", 16 | min_height="85vh", 17 | id='my-child' 18 | ) 19 | return base_page(my_child) -------------------------------------------------------------------------------- /full_stack_python/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/full-stack-python/4d89865a97ddae320dd1d20f48bd878f61ab22a9/full_stack_python/ui/__init__.py -------------------------------------------------------------------------------- /full_stack_python/ui/base.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from ..auth.state import SessionState 4 | from .nav import navbar 5 | from .dashboard import base_dashboard_page 6 | 7 | def base_layout_component(child, *args, **kwargs) -> rx.Component: 8 | return rx.fragment( # renders nada 9 | navbar(), 10 | rx.box( 11 | child, 12 | # bg=rx.color("accent", 3), 13 | padding="1em", 14 | width="100%", 15 | id="my-content-area-el" 16 | ), 17 | rx.logo(), 18 | rx.color_mode.button(position="bottom-left"), 19 | # padding='10em', 20 | # id="my-base-container" 21 | ) 22 | 23 | def base_page(child: rx.Component, *args, **kwargs) -> rx.Component: 24 | if not isinstance(child,rx. Component): 25 | child = rx.heading("this is not a valid child element") 26 | return rx.cond( 27 | SessionState.is_authenticated, 28 | base_dashboard_page(child, *args, **kwargs), 29 | base_layout_component(child, *args, **kwargs ), 30 | ) -------------------------------------------------------------------------------- /full_stack_python/ui/dashboard.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from .sidebar import sidebar 4 | 5 | def base_dashboard_page(child: rx.Component, *args, **kwargs) -> rx.Component: 6 | # print([type(x) for x in args]) 7 | if not isinstance(child,rx. Component): 8 | child = rx.heading("this is not a valid child element") 9 | return rx.fragment( 10 | rx.hstack( 11 | sidebar(), 12 | rx.box( 13 | child, 14 | rx.logo(), 15 | # bg=rx.color("accent", 3), 16 | padding="1em", 17 | width="100%", 18 | id="my-content-area-el" 19 | ), 20 | 21 | ), 22 | # rx.color_mode.button(position="bottom-left"), 23 | # padding='10em', 24 | # id="my-base-container" 25 | ) -------------------------------------------------------------------------------- /full_stack_python/ui/nav.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | import reflex_local_auth 3 | 4 | 5 | from .. import navigation 6 | 7 | def navbar_link(text: str, url: str) -> rx.Component: 8 | return rx.link( 9 | rx.text(text, size="4", weight="medium"), href=url 10 | ) 11 | 12 | 13 | def navbar() -> rx.Component: 14 | return rx.box( 15 | rx.desktop_only( 16 | rx.hstack( 17 | rx.hstack( 18 | rx.link( 19 | rx.image( 20 | src="/logo.jpg", 21 | width="2.25em", 22 | height="auto", 23 | border_radius="25%", 24 | ), 25 | href=navigation.routes.HOME_ROUTE 26 | ), 27 | rx.link( 28 | rx.heading( 29 | "Reflex", size="7", weight="bold" 30 | ), 31 | href=navigation.routes.HOME_ROUTE 32 | ), 33 | align_items="center", 34 | ), 35 | rx.hstack( 36 | navbar_link("Home", navigation.routes.HOME_ROUTE), 37 | navbar_link("About", navigation.routes.ABOUT_US_ROUTE), 38 | navbar_link("Articles", navigation.routes.ARTICLE_LIST_ROUTE), 39 | navbar_link("Pricing", navigation.routes.PRICING_ROUTE), 40 | navbar_link("Contact", navigation.routes.CONTACT_US_ROUTE), 41 | spacing="5", 42 | ), 43 | rx.hstack( 44 | rx.link( 45 | rx.button( 46 | "Register", 47 | size="3", 48 | variant="outline", 49 | ), 50 | href=reflex_local_auth.routes.REGISTER_ROUTE 51 | ), 52 | rx.link( 53 | rx.button( 54 | "Login", 55 | size="3", 56 | variant="outline", 57 | ), 58 | href=reflex_local_auth.routes.LOGIN_ROUTE 59 | ), 60 | spacing="4", 61 | justify="end", 62 | ), 63 | justify="between", 64 | align_items="center", 65 | id='my-navbar-hstack-desktop', 66 | ), 67 | ), 68 | rx.mobile_and_tablet( 69 | rx.hstack( 70 | rx.hstack( 71 | rx.image( 72 | src="/logo.jpg", 73 | width="2em", 74 | height="auto", 75 | border_radius="25%", 76 | ), 77 | rx.heading( 78 | "Reflex", size="6", weight="bold" 79 | ), 80 | align_items="center", 81 | ), 82 | rx.menu.root( 83 | rx.menu.trigger( 84 | rx.icon("menu", size=30) 85 | ), 86 | rx.menu.content( 87 | rx.menu.item("Home", 88 | on_click=navigation.NavState.to_home), 89 | rx.menu.item("About", 90 | on_click=navigation.NavState.to_about_us), 91 | rx.menu.item("Articles", 92 | on_click=navigation.NavState.to_articles), 93 | rx.menu.item("Pricing", 94 | on_click=navigation.NavState.to_pricing), 95 | rx.menu.item("Contact", 96 | on_click=navigation.NavState.to_contact), 97 | rx.menu.separator(), 98 | rx.menu.item("Log in", 99 | on_click=navigation.NavState.to_login), 100 | rx.menu.item("Register", 101 | on_click=navigation.NavState.to_register), 102 | ), 103 | justify="end", 104 | ), 105 | justify="between", 106 | align_items="center", 107 | ), 108 | ), 109 | bg=rx.color("accent", 3), 110 | padding="1em", 111 | # position="fixed", 112 | # top="0px", 113 | # z_index="5", 114 | width="100%", 115 | id='my-main-nav', 116 | ) 117 | 118 | -------------------------------------------------------------------------------- /full_stack_python/ui/sidebar.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | from reflex.style import toggle_color_mode 3 | 4 | from ..auth.state import SessionState 5 | from .. import navigation 6 | 7 | def sidebar_user_item() -> rx.Component: 8 | user_info_obj = SessionState.authenticated_user_info 9 | username_via_user_obj = rx.cond(SessionState.authenticated_username, SessionState.authenticated_username, "Account") 10 | return rx.cond( 11 | user_info_obj, 12 | rx.hstack( 13 | rx.icon_button( 14 | rx.icon("user"), 15 | size="3", 16 | radius="full", 17 | ), 18 | rx.vstack( 19 | rx.box( 20 | rx.text( 21 | username_via_user_obj, 22 | size="3", 23 | weight="bold", 24 | ), 25 | rx.text( 26 | f"{user_info_obj.email}", 27 | size="2", 28 | weight="medium", 29 | ), 30 | width="100%", 31 | ), 32 | spacing="0", 33 | align="start", 34 | justify="start", 35 | width="100%", 36 | ), 37 | padding_x="0.5rem", 38 | align="center", 39 | justify="start", 40 | width="100%", 41 | ), 42 | rx.fragment("") 43 | ) 44 | 45 | def sidebar_logout_item() -> rx.Component: 46 | return rx.box( 47 | rx.hstack( 48 | rx.icon("log-out"), 49 | rx.text("Logout", size="4"), 50 | width="100%", 51 | padding_x="0.5rem", 52 | padding_y="0.75rem", 53 | align="center", 54 | style={ 55 | "_hover": { 56 | "cursor": "pointer", # css 57 | "bg": rx.color("accent", 4), 58 | "color": rx.color("accent", 11), 59 | }, 60 | "color": rx.color("accent", 11), 61 | "border-radius": "0.5em", 62 | }, 63 | ), 64 | on_click=navigation.NavState.to_logout, 65 | as_='button', 66 | underline="none", 67 | weight="medium", 68 | width="100%", 69 | ) 70 | 71 | def sidebar_dark_mode_toggle_item() -> rx.Component: 72 | return rx.box( 73 | rx.hstack( 74 | rx.color_mode_cond( 75 | light=rx.icon("moon"), 76 | dark=rx.icon("sun"), 77 | ), 78 | rx.text(rx.color_mode_cond( 79 | light="Turn dark mode on", 80 | dark="Turn light mode on", 81 | ), size="4"), 82 | width="100%", 83 | padding_x="0.5rem", 84 | padding_y="0.75rem", 85 | align="center", 86 | style={ 87 | "_hover": { 88 | "cursor": "pointer", # css 89 | "bg": rx.color("accent", 4), 90 | "color": rx.color("accent", 11), 91 | }, 92 | "color": rx.color("accent", 11), 93 | "border-radius": "0.5em", 94 | }, 95 | ), 96 | on_click=toggle_color_mode, 97 | as_='button', 98 | underline="none", 99 | weight="medium", 100 | width="100%", 101 | ) 102 | 103 | 104 | def sidebar_item( 105 | text: str, icon: str, href: str 106 | ) -> rx.Component: 107 | return rx.link( 108 | rx.hstack( 109 | rx.icon(icon), 110 | rx.text(text, size="4"), 111 | width="100%", 112 | padding_x="0.5rem", 113 | padding_y="0.75rem", 114 | align="center", 115 | style={ 116 | "_hover": { 117 | "bg": rx.color("accent", 4), 118 | "color": rx.color("accent", 11), 119 | }, 120 | "border-radius": "0.5em", 121 | }, 122 | ), 123 | href=href, 124 | underline="none", 125 | weight="medium", 126 | width="100%", 127 | ) 128 | 129 | 130 | def sidebar_items() -> rx.Component: 131 | return rx.vstack( 132 | sidebar_item("Dashboard", "layout-dashboard", navigation.routes.HOME_ROUTE), 133 | sidebar_item("Articles", "globe", navigation.routes.ARTICLE_LIST_ROUTE), 134 | sidebar_item("Blog", "newspaper", navigation.routes.BLOG_POSTS_ROUTE), 135 | sidebar_item("Create post", "notebook-pen", navigation.routes.BLOG_POST_ADD_ROUTE), 136 | sidebar_item("Contact", "mail", navigation.routes.CONTACT_US_ROUTE), 137 | sidebar_item("Contact History", "mailbox", navigation.routes.CONTACT_ENTRIES_ROUTE), 138 | spacing="1", 139 | width="100%", 140 | ) 141 | 142 | 143 | def sidebar() -> rx.Component: 144 | return rx.box( 145 | rx.desktop_only( 146 | rx.vstack( 147 | rx.hstack( 148 | rx.image( 149 | src="/logo.jpg", 150 | width="2.25em", 151 | height="auto", 152 | border_radius="25%", 153 | ), 154 | rx.heading( 155 | "Reflex", size="7", weight="bold" 156 | ), 157 | align="center", 158 | justify="start", 159 | padding_x="0.5rem", 160 | width="100%", 161 | ), 162 | sidebar_items(), 163 | rx.spacer(), 164 | rx.vstack( 165 | rx.vstack( 166 | sidebar_dark_mode_toggle_item(), 167 | sidebar_logout_item(), 168 | spacing="1", 169 | width="100%", 170 | ), 171 | rx.divider(), 172 | sidebar_user_item(), 173 | width="100%", 174 | spacing="5", 175 | ), 176 | spacing="5", 177 | # position="fixed", 178 | # left="0px", 179 | # top="0px", 180 | # z_index="5", 181 | padding_x="1em", 182 | padding_y="1.5em", 183 | bg=rx.color("accent", 3), 184 | align="start", 185 | height="100vh", 186 | # height="650px", 187 | width="16em", 188 | ), 189 | ), 190 | rx.mobile_and_tablet( 191 | rx.drawer.root( 192 | rx.drawer.trigger( 193 | rx.icon("align-justify", size=30) 194 | ), 195 | rx.drawer.overlay(z_index="5"), 196 | rx.drawer.portal( 197 | rx.drawer.content( 198 | rx.vstack( 199 | rx.box( 200 | rx.drawer.close( 201 | rx.icon("x", size=30) 202 | ), 203 | width="100%", 204 | ), 205 | sidebar_items(), 206 | rx.spacer(), 207 | rx.vstack( 208 | rx.vstack( 209 | sidebar_dark_mode_toggle_item(), 210 | sidebar_logout_item(), 211 | width="100%", 212 | spacing="1", 213 | ), 214 | rx.divider(margin="0"), 215 | sidebar_user_item(), 216 | width="100%", 217 | spacing="5", 218 | ), 219 | spacing="5", 220 | width="100%", 221 | ), 222 | top="auto", 223 | right="auto", 224 | height="100%", 225 | width="20em", 226 | padding="1.5em", 227 | bg=rx.color("accent", 2), 228 | ), 229 | width="100%", 230 | ), 231 | direction="left", 232 | ), 233 | padding="1em", 234 | ), 235 | ) -------------------------------------------------------------------------------- /full_stack_python/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import timing 2 | 3 | __all__ = ['timing'] -------------------------------------------------------------------------------- /full_stack_python/utils/timing.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | def get_utc_now() -> datetime: 4 | return datetime.now(timezone.utc) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | reflex==0.5.3 2 | reflex-local-auth -------------------------------------------------------------------------------- /rxconfig.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | config = rx.Config( 4 | app_name="full_stack_python", 5 | db_url="sqlite:///reflex.db", 6 | ) --------------------------------------------------------------------------------