Created
July 9, 2025 07:52
-
-
Save nileshtrivedi/29f0b5b7b660e9cfd6ea0bbe87792475 to your computer and use it in GitHub Desktop.
distributed reactivity example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule ExampleWeb.LiveSigil do | |
use ExampleWeb, :live_view | |
def render(assigns) do | |
~V""" | |
<script> | |
import { onMount } from 'svelte'; | |
// props sent from the server | |
let { shown_word, leaderboard, live } = $props(); | |
// Client-Side State using Runes | |
let name = $state(''); | |
let user_guess = $state(''); // e.g., "ACB" | |
let status_message = $state(''); // Feedback from the server | |
let error_message = $state(''); // Client-side validation errors | |
onMount(() => { | |
live.handleEvent("feedback", (payload) => { | |
// This will be called when the server pushes feedback | |
status_message = payload.message; | |
}); | |
}); | |
function handleSubmit(){ | |
// --- Client-Side Validation --- | |
// Assignments to $state variables automatically trigger updates. | |
error_message = ''; | |
status_message = ''; | |
if (name.trim() === '') { | |
error_message = 'Please enter your name.'; | |
return; | |
} | |
if (user_guess.length != shown_word.length) { | |
error_message = 'Word length is not correct.'; | |
return; | |
} | |
if (user_guess.split('').sort().join('') != shown_word.split('').sort().join('')) { | |
error_message = 'Letters are not correct.'; | |
return; | |
} | |
// `pushEvent` sends the data to the Phoenix LiveView. | |
live.pushEvent("submit_answer", {name: name, attempt: user_guess}); | |
} | |
</script> | |
<h1 class="text-2xl font-bold mb-4">Sorting Guessing Game</h1> | |
<p class="prose">The server has put these letters <b>{shown_word}</b> in some random order which you need to guess. Correct guess earns you 10 points.</p> | |
<!-- Guess Word --> | |
<div class="space-y-4 mb-6"> | |
<div class="flex items-center space-x-4 bg-gray-50 p-3 rounded-lg"> | |
<input | |
id="name" | |
type="text" | |
class="w-48 p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-indigo-500 focus:border-indigo-500" | |
placeholder="Your name" | |
bind:value={name} | |
/> | |
<input | |
type="text" | |
min="1" | |
max="{shown_word.length}" | |
class="w-48 p-2 text-center border border-gray-300 rounded-lg shadow-sm focus:ring-indigo-500 focus:border-indigo-500" | |
placeholder="Your guess" | |
bind:value={user_guess} | |
/> | |
<button | |
class="w-full sm:w-auto px-6 py-3 bg-indigo-600 text-white font-bold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200" | |
onclick={handleSubmit} | |
> | |
Submit Answer | |
</button> | |
</div> | |
</div> | |
<!-- Status/Error Messages --> | |
{#if status_message} | |
<div class="mt-4 p-3 rounded-lg text-center" class:bg-green-100={status_message.includes('Correct')} class:text-green-800={status_message.includes('Correct')} class:bg-red-100={!status_message.includes('Correct')} class:text-red-800={!status_message.includes('Correct')}> | |
{status_message} | |
</div> | |
{/if} | |
{#if error_message} | |
<div class="mt-4 p-3 rounded-lg bg-yellow-100 text-yellow-800 text-center"> | |
{error_message} | |
</div> | |
{/if} | |
<!-- Leaderboard Section --> | |
<div class="bg-white p-6 sm:p-8 rounded-2xl shadow-lg"> | |
<h2 class="text-2xl font-bold text-gray-800 mb-4">Leaderboard</h2> | |
<div class="space-y-3"> | |
{#if Object.keys(leaderboard).length > 0} | |
<!-- Sort leaderboard by score descending --> | |
{#each Object.entries(leaderboard).sort((a, b) => b[1] - a[1]) as [name, score]} | |
<div class="flex justify-between items-center bg-gray-50 p-3 rounded-lg"> | |
<span class="font-medium text-gray-700">{name}</span> | |
<span class="font-bold text-indigo-600">{score} pts</span> | |
</div> | |
{/each} | |
{:else} | |
<p class="text-gray-500">No scores yet. Be the first!</p> | |
{/if} | |
</div> | |
</div> | |
""" | |
end | |
@doc """ | |
Mounts the LiveView, initializing the server-side state. | |
""" | |
def mount(_params, _session, socket) do | |
# On mount, we shuffle the items and set the initial state. | |
new_secret_word = Enum.shuffle(String.split("ABC","", trim: true)) |> Enum.join | |
socket = | |
assign(socket, | |
secret_word: new_secret_word, | |
shown_word: "ABC", | |
leaderboard: %{} | |
) | |
{:ok, socket} | |
end | |
@doc """ | |
Handles the `submit_answer` event pushed from the Svelte client. | |
""" | |
def handle_event("submit_answer", %{"name" => name, "attempt" => attempt}, socket) do | |
# All game logic happens securely on the server. | |
if socket.assigns.secret_word == attempt do | |
# Correct Answer | |
new_leaderboard = Map.update(socket.assigns.leaderboard, name, 10, &(&1 + 10)) | |
# Push a feedback event to the client. | |
socket = push_event(socket, "feedback", %{message: "Correct! You earned 10 points."}) | |
# Reset the game for a new round | |
new_secret_word = Enum.shuffle(String.split("ABC","", trim: true)) |> Enum.join | |
new_socket = | |
assign(socket, | |
secret_word: new_secret_word, | |
shown_word: "ABC", | |
leaderboard: new_leaderboard | |
) | |
{:noreply, new_socket} | |
else | |
# Incorrect Answer | |
socket = push_event(socket, "feedback", %{message: "Not quite, try again!"}) | |
{:noreply, socket} | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment