diff --git a/reflex-twitter-clone/.gitignore b/reflex-twitter-clone/.gitignore new file mode 100644 index 0000000..932441e --- /dev/null +++ b/reflex-twitter-clone/.gitignore @@ -0,0 +1,6 @@ +.states +*.db +.web +*.py[cod] +assets/external/ +__pycache__/ diff --git a/reflex-twitter-clone/README.md b/reflex-twitter-clone/README.md new file mode 100644 index 0000000..2da310c --- /dev/null +++ b/reflex-twitter-clone/README.md @@ -0,0 +1,40 @@ +# Reflex Twitter Clone Template + +This template is a small social feed built with Reflex. It includes a single-page timeline, local sign-in state, post composer, like/repost counters, profile cards, trending topics, and a Codesphere `ci.yml` pipeline. + +## Features + +- Reflex full-stack Python app +- Local demo sign-in and profile switching +- Compose posts from the browser +- Like and repost interactions +- Responsive three-column social layout +- Codesphere-ready CI pipeline + +## Run locally + +```bash +pip install -r requirements.txt +reflex init +reflex run +``` + +Open the Reflex frontend URL printed by the command. By default Reflex serves the app on port `3000`. + +## Deploy on Codesphere + +1. Create a Codesphere workspace from this template. +2. Open the CI pipeline view. +3. Run the prepare step to install dependencies and initialize Reflex. +4. Run the app service. The pipeline binds Reflex to `0.0.0.0` and exposes the frontend on port `3000`. + +The template uses only local demo state, so no external API keys or databases are required. + +## Blog article outline + +Use this outline for the requested article: + +1. Why Reflex is useful for Python-first web prototypes. +2. How the template models a social timeline with a single `rx.State` class. +3. How Codesphere runs the app from `ci.yml`. +4. Ideas for extending the clone with persistence, authentication, and media uploads. diff --git a/reflex-twitter-clone/ci.yml b/reflex-twitter-clone/ci.yml new file mode 100644 index 0000000..990f723 --- /dev/null +++ b/reflex-twitter-clone/ci.yml @@ -0,0 +1,20 @@ +schemaVersion: v0.2 +prepare: + steps: + - name: Install Python dependencies + command: pip install -r requirements.txt + - name: Initialize Reflex project + command: reflex init +test: + steps: + - name: Export Reflex app + command: reflex export --frontend-only --no-zip +run: + app: + steps: + - name: Start Reflex Twitter clone + command: reflex run --env prod --backend-host 0.0.0.0 --frontend-port 3000 --backend-port 8000 + plan: 8 + replicas: 1 + network: + path: / diff --git a/reflex-twitter-clone/metadata.json b/reflex-twitter-clone/metadata.json new file mode 100644 index 0000000..219891b --- /dev/null +++ b/reflex-twitter-clone/metadata.json @@ -0,0 +1,10 @@ +{ + "Workspace": "Micro", + "Links": { + "Reflex": "https://reflex.dev/", + "Codesphere CI Pipelines": "https://docs.codesphere.com/getting-started/ci-pipelines" + }, + "Categories": ["Python", "Reflex", "Social", "Template"], + "Contributors": ["kenproxx"], + "Title": "Reflex Twitter Clone" +} diff --git a/reflex-twitter-clone/reflex-twitter-clone.webp b/reflex-twitter-clone/reflex-twitter-clone.webp new file mode 100644 index 0000000..a92469e Binary files /dev/null and b/reflex-twitter-clone/reflex-twitter-clone.webp differ diff --git a/reflex-twitter-clone/requirements.txt b/reflex-twitter-clone/requirements.txt new file mode 100644 index 0000000..8101997 --- /dev/null +++ b/reflex-twitter-clone/requirements.txt @@ -0,0 +1 @@ +reflex==0.9.2.post1 diff --git a/reflex-twitter-clone/rxconfig.py b/reflex-twitter-clone/rxconfig.py new file mode 100644 index 0000000..f2216c7 --- /dev/null +++ b/reflex-twitter-clone/rxconfig.py @@ -0,0 +1,10 @@ +import reflex as rx +from reflex_base.plugins import SitemapPlugin +from reflex_components_radix.plugin import RadixThemesPlugin + + +config = rx.Config( + app_name="twitter_clone", + plugins=[RadixThemesPlugin()], + disable_plugins=[SitemapPlugin], +) diff --git a/reflex-twitter-clone/twitter_clone/__init__.py b/reflex-twitter-clone/twitter_clone/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/reflex-twitter-clone/twitter_clone/__init__.py @@ -0,0 +1 @@ + diff --git a/reflex-twitter-clone/twitter_clone/twitter_clone.py b/reflex-twitter-clone/twitter_clone/twitter_clone.py new file mode 100644 index 0000000..eaa0b8a --- /dev/null +++ b/reflex-twitter-clone/twitter_clone/twitter_clone.py @@ -0,0 +1,257 @@ +import reflex as rx + + +class State(rx.State): + current_user: str = "Ada" + current_handle: str = "@ada.codes" + draft: str = "" + signed_in: bool = False + posts: list[dict[str, str | int | bool]] = [ + { + "id": 1, + "author": "Mira", + "handle": "@mira.builds", + "avatar": "M", + "body": "Reflex makes this whole social feed feel surprisingly Python-native.", + "timestamp": "8m", + "likes": 18, + "reposts": 4, + "liked": False, + "reposted": False, + }, + { + "id": 2, + "author": "Jon", + "handle": "@jon.codes", + "avatar": "J", + "body": "Shipping a template is easier when the deploy script is part of the repo, not hidden in a wiki.", + "timestamp": "22m", + "likes": 31, + "reposts": 7, + "liked": False, + "reposted": False, + }, + { + "id": 3, + "author": "Leah", + "handle": "@leah.ops", + "avatar": "L", + "body": "Next step: wire this to SQLite and turn demo posts into real conversations.", + "timestamp": "1h", + "likes": 12, + "reposts": 2, + "liked": False, + "reposted": False, + }, + ] + + @rx.event + def update_draft(self, value: str): + self.draft = value + + @rx.event + def sign_in(self): + self.signed_in = True + + @rx.event + def sign_out(self): + self.signed_in = False + + @rx.event + def publish(self): + text = self.draft.strip() + if not text or not self.signed_in: + return + self.posts.insert( + 0, + { + "id": max([int(post["id"]) for post in self.posts], default=0) + 1, + "author": self.current_user, + "handle": self.current_handle, + "avatar": self.current_user[0], + "body": text, + "timestamp": "now", + "likes": 0, + "reposts": 0, + "liked": False, + "reposted": False, + }, + ) + self.draft = "" + + @rx.event + def toggle_like(self, post_id: int): + for post in self.posts: + if post["id"] == post_id: + post["liked"] = not post["liked"] + post["likes"] += 1 if post["liked"] else -1 + break + + @rx.event + def toggle_repost(self, post_id: int): + for post in self.posts: + if post["id"] == post_id: + post["reposted"] = not post["reposted"] + post["reposts"] += 1 if post["reposted"] else -1 + break + + +def avatar(letter: str) -> rx.Component: + return rx.center( + rx.text(letter, font_weight="700", color="white"), + width="44px", + height="44px", + border_radius="999px", + background="#2563eb", + flex_shrink="0", + ) + + +def sidebar() -> rx.Component: + return rx.vstack( + rx.heading("Reflex Social", size="6"), + rx.button("Home", width="100%", variant="soft"), + rx.button("Explore", width="100%", variant="ghost"), + rx.button("Messages", width="100%", variant="ghost"), + rx.button("Profile", width="100%", variant="ghost"), + rx.spacer(), + rx.cond( + State.signed_in, + rx.button("Sign out", on_click=State.sign_out, width="100%"), + rx.button("Sign in demo", on_click=State.sign_in, width="100%"), + ), + align="stretch", + min_width="190px", + height="calc(100vh - 48px)", + padding="24px 18px", + border_right="1px solid #e5e7eb", + position="sticky", + top="0", + ) + + +def composer() -> rx.Component: + return rx.vstack( + rx.hstack( + avatar("A"), + rx.text_area( + placeholder="What are you building?", + value=State.draft, + on_change=State.update_draft, + min_height="96px", + resize="vertical", + ), + width="100%", + align="start", + ), + rx.hstack( + rx.cond( + State.signed_in, + rx.text("Posting as @ada.codes", color="#64748b"), + rx.text("Sign in to publish", color="#64748b"), + ), + rx.spacer(), + rx.button("Post", on_click=State.publish, is_disabled=~State.signed_in), + width="100%", + ), + padding="20px", + border_bottom="1px solid #e5e7eb", + align="stretch", + ) + + +def post_card(post: dict[str, str | int | bool]) -> rx.Component: + return rx.hstack( + avatar(post["avatar"]), + rx.vstack( + rx.hstack( + rx.text(post["author"], font_weight="700"), + rx.text(post["handle"], color="#64748b"), + rx.text("ยท", color="#94a3b8"), + rx.text(post["timestamp"], color="#64748b"), + spacing="2", + ), + rx.text(post["body"], line_height="1.6"), + rx.hstack( + rx.button( + rx.cond(post["liked"], "Liked", "Like"), + rx.text(post["likes"]), + on_click=State.toggle_like(post["id"]), + variant=rx.cond(post["liked"], "solid", "soft"), + ), + rx.button( + rx.cond(post["reposted"], "Reposted", "Repost"), + rx.text(post["reposts"]), + on_click=State.toggle_repost(post["id"]), + variant=rx.cond(post["reposted"], "solid", "soft"), + ), + spacing="3", + padding_top="8px", + ), + align="stretch", + width="100%", + spacing="2", + ), + align="start", + padding="20px", + border_bottom="1px solid #e5e7eb", + width="100%", + ) + + +def trends() -> rx.Component: + topics = ["#Reflex", "#PythonWeb", "#Codesphere", "#OpenSource"] + return rx.vstack( + rx.heading("Trends", size="4"), + rx.foreach( + topics, + lambda topic: rx.box( + rx.text(topic, font_weight="600"), + rx.text("Template builders are talking", color="#64748b", font_size="14px"), + padding="12px 0", + border_bottom="1px solid #e5e7eb", + width="100%", + ), + ), + rx.box( + rx.text("Demo profile", font_weight="700"), + rx.text("@ada.codes", color="#64748b"), + rx.text("Python-first product builder", margin_top="8px"), + padding="16px", + border="1px solid #e5e7eb", + border_radius="8px", + width="100%", + ), + align="stretch", + min_width="240px", + padding="24px 18px", + position="sticky", + top="0", + height="calc(100vh - 48px)", + ) + + +def index() -> rx.Component: + return rx.hstack( + sidebar(), + rx.vstack( + rx.heading("Home", size="5", padding="20px", border_bottom="1px solid #e5e7eb", width="100%"), + composer(), + rx.foreach(State.posts, post_card), + align="stretch", + width="100%", + max_width="680px", + min_height="100vh", + ), + trends(), + align="start", + justify="center", + width="100%", + min_height="100vh", + background="#ffffff", + color="#0f172a", + ) + + +app = rx.App() +app.add_page(index, title="Reflex Twitter Clone")