Skip to content

Commit

Permalink
initial preview of components
Browse files Browse the repository at this point in the history
  • Loading branch information
ogrodnek committed Sep 30, 2024
1 parent 33af1c3 commit c191617
Show file tree
Hide file tree
Showing 38 changed files with 715 additions and 20 deletions.
3 changes: 3 additions & 0 deletions examples/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
FileUploadDemoLiveView,
KanbanLiveView,
IncludesLiveView,
RecipesLiveView,
)

app = PyView()
Expand All @@ -35,6 +36,7 @@
("pyview", "static"),
("examples.views.maps", "static"),
("examples.views.kanban", "static"),
("examples.views.recipes", "photos"),
]
),
name="static",
Expand Down Expand Up @@ -94,6 +96,7 @@ def content_wrapper(_context, content: Markup) -> Markup:
("/file_upload", FileUploadDemoLiveView),
("/kanban", KanbanLiveView),
("/includes", IncludesLiveView),
("/recipes", RecipesLiveView),
]


Expand Down
2 changes: 2 additions & 0 deletions examples/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .count_pubsub import CountLiveViewPubSub
from .count import CountLiveView
from .includes import IncludesLiveView
from .recipes import RecipesLiveView

__all__ = [
"CountLiveView",
Expand All @@ -32,4 +33,5 @@
"FileUploadDemoLiveView",
"KanbanLiveView",
"IncludesLiveView",
"RecipesLiveView",
]
1 change: 1 addition & 0 deletions examples/views/kanban/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class KanbanLiveView(LiveView[KanbanContext]):

async def mount(self, socket: LiveViewSocket[KanbanContext], session):
socket.context = KanbanContext()
socket.live_title = "Kanban"

async def handle_event(self, event, payload, socket: LiveViewSocket[KanbanContext]):
if event == "task-moved":
Expand Down
1 change: 1 addition & 0 deletions examples/views/maps/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async def mount(self, socket: LiveViewSocket[MapContext], session):
socket.context = MapContext(
parks=national_parks, selected_park_name=national_parks[0]["name"]
)
socket.live_title = "Maps"

async def handle_event(
self, event, payload, socket: ConnectedLiveViewSocket[MapContext]
Expand Down
10 changes: 10 additions & 0 deletions examples/views/recipes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .recipes import RecipesLiveView
from .components.recipe_card import RecipeCardComponent
from .components.ratings import RatingsComponent


__all__ = [
"RecipesLiveView",
"RecipeCardComponent",
"RatingsComponent",
]
Empty file.
25 changes: 25 additions & 0 deletions examples/views/recipes/components/ratings.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.stars {
display: grid;
grid-template-columns: repeat(5, 1fr);
margin: 0 auto;
}

.star {
background: none;
border: none;
font-size: 1.1rem;
cursor: pointer;
color: #e6e6e6;
transition: color 0.3s;
margin: 0;
padding: 0;
}

.star:hover,
.star.active {
color: #f5a623;
}

.star:focus {
outline: none;
}
8 changes: 8 additions & 0 deletions examples/views/recipes/components/ratings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="recipe-rating">
<div class="stars">
{% for i in range(1, 5) %}
<button class="star {%if rating and i <= rating %}active{%endif%}" phx-click="rate" phx-value-id="{{id}}"
phx-value-rating="{{i}}"></button>
{% endfor %}
</div>
</div>
11 changes: 11 additions & 0 deletions examples/views/recipes/components/ratings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pyview.live_component.live_component import LiveComponent
from pyview.live_component.component_registry import components


@components.register("Ratings")
class RatingsComponent(LiveComponent):
async def mount(self, socket):
print("Ratings mounted")

async def update(self, socket, template_vars):
print("Ratings updated", template_vars)
81 changes: 81 additions & 0 deletions examples/views/recipes/components/recipe_card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.recipe_card {
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 300px;
background-color: white;
}

.recipe_card img {
width: 100%;
height: auto;
display: block;
border: none;
height: 200px;
object-fit: cover
}

.recipe_card-content {
padding: 0.75em;
}

.recipe_card-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}

.recipe_card-content .recipe_card-title {
font-size: 1.1em;
font-weight: 500;
color: #333;
}

.recipe_card-content .recipe_card-time {
font-size: 0.9em;
color: #666;
font-weight: 100;
}

.recipe_card-time::before {
content: "⏱️";
margin-right: 5px;
font-size: 0.9em;
color: #666;
}

.recipe_card-content p {
margin-top: 10px;
font-size: 1em;
color: #666;
}

.recipe_card-actions {
padding-top: .75em;
display: flex;
justify-content: space-between;
}

.recipe_card-bookmark {
filter: opacity(0.4);
}

.recipe_card-bookmark:hover {
filter: opacity(1);
}

.recipe_card-attribution {
font-size: 0.7em;
color: #666;
display: grid;
justify-items: flex-end;
padding: 0.5em;

a {
text-decoration: none;
}
}

.bookmarked {
filter: opacity(1);
}
22 changes: 22 additions & 0 deletions examples/views/recipes/components/recipe_card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div id="{{recipe.id}}" class="recipe_card">
<img src="{{recipe.img}}" alt="{{recipe.name}}">
<div class="recipe_card-attribution">
<span>Photo by <a href="{{recipe.attribution.url}}" target="_blank">{{recipe.attribution.name}}</a></span>
</div>
<div class="recipe_card-content">
<div class="recipe_card-header">
<span class="recipe_card-title">{{recipe.name}}</span>
<span class="recipe_card-time">{{recipe.time}}</span>
</div>

<div class="recipe_card-actions">
{% live_component "Ratings" with id = recipe.id & rating = recipe.rating %}
<a title="{%if recipe.bookmarked %}Remove {%endif%}Bookmark"
class="recipe_card-bookmark {%if recipe.bookmarked %}bookmarked{%endif%}" phx-click="bookmark"
phx-target="{{meta.myself.target}}" phx-value-id="{{recipe.id}}">
🔖
</a>
</div>
</div>
</div>
</div>
14 changes: 14 additions & 0 deletions examples/views/recipes/components/recipe_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pyview.live_component.live_component import LiveComponent
from pyview.live_component.component_registry import components


@components.register("RecipeCard")
class RecipeCardComponent(LiveComponent):
def __init__(self):
super().__init__()

async def mount(self, socket):
print("RecipeCard mounted")

async def update(self, socket, template_vars):
print("RecipeCard updated", template_vars)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions examples/views/recipes/recipe_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from dataclasses import dataclass, field
import random
import uuid
from typing import Optional


@dataclass
class Attribution:
name: str
url: str


@dataclass
class Recipe:
name: str
img: str
time: str

attribution: Attribution

id: str = field(default_factory=lambda: uuid.uuid4().hex)
rating: Optional[int] = None
bookmarked: bool = False


def all_recipes() -> list[Recipe]:
return [
Recipe(
name="Donuts",
img="/static/brooke-lark-V4MBq8kue3U-unsplash.jpg",
time="90 mins",
attribution=Attribution(
"Brooke Lark",
"https://unsplash.com/@brookelark?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash",
),
),
Recipe(
name="Chickpea Salad",
img="/static/deryn-macey-B-DrrO3tSbo-unsplash.jpg",
time="30 mins",
attribution=Attribution(
"Deryn Macey",
"https://unsplash.com/@derynmacey?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash",
),
),
Recipe(
name="Chia Pudding",
img="/static/maryam-sicard-Tz1sAv3xnt0-unsplash.jpg",
time="20 mins",
attribution=Attribution(
"Maryam Sicard",
"https://unsplash.com/@maryamsicard?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash",
),
),
Recipe(
name="Cinnamon Rolls",
img="/static/nick-bratanek-RBwli5VzJXo-unsplash.jpg",
time="45 mins",
attribution=Attribution(
"Nick Bratanek",
"https://unsplash.com/@nickbratanek?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash",
),
),
Recipe(
name="Watermelon Salad",
img="/static/taylor-kiser-EvoIiaIVRzU-unsplash.jpg",
time="15 mins",
attribution=Attribution(
"Taylor Kiser",
"https://unsplash.com/@foodfaithfit?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash",
),
),
Recipe(
name="Curry",
img="static/taylor-kiser-POFG828-GQc-unsplash.jpg",
time="30 mins",
attribution=Attribution(
"Taylor Kiser",
"https://unsplash.com/@foodfaithfit?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash",
),
),
]
24 changes: 24 additions & 0 deletions examples/views/recipes/recipes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
main {
margin: 0;
padding: 20px;
height: 100vh;
}

html {
background-color: #F0F0F0;
}

body {
max-width: 980px;
}

.recipes {
padding-block: 2em;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
}

.attribution {
font-size: 0.7em;
}
8 changes: 8 additions & 0 deletions examples/views/recipes/recipes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<main>
<h1 style="margin-top: 0.75em">Latest Recipes</h1>
<div class="recipes">
{% for recipe in recipes %}
{% live_component "RecipeCard" with id = recipe.id & recipe = recipe %}
{% endfor %}
</div>
</main>
34 changes: 34 additions & 0 deletions examples/views/recipes/recipes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pyview import LiveView, LiveViewSocket
from dataclasses import dataclass, field
from .recipe_db import Recipe, all_recipes


@dataclass
class RecipesContext:
recipes: list[Recipe]


class RecipesLiveView(LiveView[RecipesContext]):
"""
Recipes
This example shows how to use components to encapsulate functionality.
"""

async def mount(self, socket: LiveViewSocket, session):
socket.context = RecipesContext(recipes=all_recipes())
socket.live_title = "Recipes"

async def handle_event(self, event, payload, socket):
if event == "bookmark" and "id" in payload:
id = payload["id"]
recipe = next((r for r in socket.context.recipes if r.id == id), None)
if recipe is not None:
recipe.bookmarked = not recipe.bookmarked
if event == "rate" and "id" in payload and "rating" in payload:
id = payload["id"]
recipe = next((r for r in socket.context.recipes if r.id == id), None)
if recipe is not None:
print("Set rating", recipe.rating, "to", payload["rating"])

recipe.rating = int(payload["rating"])
Empty file.
Loading

0 comments on commit c191617

Please sign in to comment.