Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -139,6 +141,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 @@ -35,6 +35,7 @@ class KanbanLiveView(BaseEventHandler, LiveView[KanbanContext]):

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

@event("task-moved")
async def handle_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>
7 changes: 7 additions & 0 deletions examples/views/recipes/components/ratings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pyview.live_component.live_component import LiveComponent
from pyview.live_component.component_registry import components


@components.register("Ratings")
class RatingsComponent(LiveComponent):
pass
88 changes: 88 additions & 0 deletions examples/views/recipes/components/recipe_card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.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-inline: 0.75em;
padding-bottom: 0.75em;

}

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


.recipe_card-content .recipe_card-title {
font-family: var(--recipe-title-font);
font-weight: 400;
font-style: normal;

font-size: 1.6em;

color: #333;
}

.recipe_card-content .recipe_card-time {
font-size: 0.8em;
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: 0.25em;
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>
8 changes: 8 additions & 0 deletions examples/views/recipes/components/recipe_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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__()
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.
81 changes: 81 additions & 0 deletions examples/views/recipes/recipe_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from dataclasses import dataclass, field
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",
),
),
]
32 changes: 32 additions & 0 deletions examples/views/recipes/recipes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
:root {
--recipe-title-font: "Shadows Into Light", cursive;
}

main {
margin: 0;
padding: 20px;
height: 100vh;
}

.recipes_title {
margin-top: 0;
}

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;
}
12 changes: 12 additions & 0 deletions examples/views/recipes/recipes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Shadows+Into+Light&display=swap" rel="stylesheet">

<main>
<h1 class="recipes_title">Latest Recipes</h1>
<div class="recipes">
{% for recipe in recipes %}
{% live_component "RecipeCard" with id = recipe.id & recipe = recipe %}
{% endfor %}
</div>
</main>
32 changes: 32 additions & 0 deletions examples/views/recipes/recipes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from pyview import LiveView, LiveViewSocket
from dataclasses import dataclass
from .recipe_list 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:
recipe.rating = int(payload["rating"])
Empty file.
Loading