mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
Added better navigation between examples projects (#202)
* Added better navigation between examples projects. * Fix json date formatter
This commit is contained in:
parent
1f9ebc49d2
commit
6829effefd
20 changed files with 945 additions and 82 deletions
5
example_project/.gitignore
vendored
5
example_project/.gitignore
vendored
|
|
@ -40,3 +40,8 @@ npm-debug.log
|
|||
|
||||
# Ignore ssr build for svelte.
|
||||
/priv/svelte/
|
||||
|
||||
# SQLite databases
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
|
|
|||
9
example_project/assets/package-lock.json
generated
9
example_project/assets/package-lock.json
generated
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
},
|
||||
"../..": {
|
||||
"version": "0.15.0-rc.6",
|
||||
"version": "0.17.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3",
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"../deps/phoenix": {
|
||||
"version": "1.7.14",
|
||||
"version": "1.7.21",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../deps/phoenix_html": {
|
||||
|
|
@ -581,6 +581,7 @@
|
|||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -735,6 +736,7 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -1198,6 +1200,7 @@
|
|||
"integrity": "sha512-MuzIIVRSbc8XxHH7FjkvWqkIcr1BvoMZoR/oFuAJDlh7VSaNJzrB4uJ38GRQa+mWjLXODAMzeDe0xi9GYbGwnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"css": "^3.0.0",
|
||||
"debug": "~3.1.0",
|
||||
|
|
@ -1221,6 +1224,7 @@
|
|||
"integrity": "sha512-56Vd/nwJrljV0w7RCV1A8sB4/yjSbWW5qrGDTAzp7q42OxwqEWT+6obWzDt41tHjIW+C9Fs2ygtejjJrXR+ZPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
|
|
@ -1316,6 +1320,7 @@
|
|||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
|
|||
239
example_project/assets/svelte/NotesApp.svelte
Normal file
239
example_project/assets/svelte/NotesApp.svelte
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<script>
|
||||
import { flip } from "svelte/animate"
|
||||
import {fly, fade} from "svelte/transition"
|
||||
|
||||
/**
|
||||
* @typedef {Object} Note
|
||||
* @property {string} id
|
||||
* @property {string} title
|
||||
* @property {string|null} content
|
||||
* @property {string} color
|
||||
* @property {string} inserted_at
|
||||
*/
|
||||
|
||||
/** @type {{notes: Note[], encoder: string, info: string, live: any}} */
|
||||
let {notes: propNotes = [], encoder = "OTP", info = "", live} = $props()
|
||||
|
||||
// Use local reactive state for notes - this helps Svelte track changes for transitions
|
||||
let notes = $state([])
|
||||
|
||||
// Sync props to local state using in-place mutations to preserve $state proxy identity.
|
||||
// This is critical for animations - replacing the array would cause all items to re-animate.
|
||||
$effect(() => {
|
||||
const currentIds = new Set(propNotes.map((p) => p.id));
|
||||
|
||||
// 1. Remove deleted items (iterate backwards to avoid index shift issues)
|
||||
for (let i = notes.length - 1; i >= 0; i--) {
|
||||
if (!currentIds.has(notes[i].id)) {
|
||||
notes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update existing items and add new ones in correct order
|
||||
for (let i = 0; i < propNotes.length; i++) {
|
||||
const p = propNotes[i];
|
||||
const existingIndex = notes.findIndex((n) => n.id === p.id);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update existing item in place (no animation triggered)
|
||||
notes[existingIndex].title = p.title;
|
||||
notes[existingIndex].content = p.content;
|
||||
notes[existingIndex].color = p.color;
|
||||
notes[existingIndex].inserted_at = p.inserted_at;
|
||||
|
||||
// Move to correct position if needed (triggers flip animation)
|
||||
if (existingIndex !== i) {
|
||||
const [item] = notes.splice(existingIndex, 1);
|
||||
notes.splice(i, 0, item);
|
||||
}
|
||||
} else {
|
||||
// Insert new item at correct position (triggers enter animation)
|
||||
notes.splice(i, 0, { ...p });
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let title = $state("")
|
||||
let content = $state("")
|
||||
let color = $state("#fef3c7")
|
||||
|
||||
const colors = [
|
||||
{value: "#fef3c7", name: "Amber"},
|
||||
{value: "#dcfce7", name: "Green"},
|
||||
{value: "#dbeafe", name: "Blue"},
|
||||
{value: "#fce7f3", name: "Pink"},
|
||||
{value: "#f3e8ff", name: "Purple"},
|
||||
{value: "#fff", name: "White"},
|
||||
]
|
||||
|
||||
function handleSubmit() {
|
||||
if (!title.trim()) return
|
||||
|
||||
live.pushEvent("create_note", {
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
color,
|
||||
})
|
||||
|
||||
title = ""
|
||||
content = ""
|
||||
color = "#fef3c7"
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function handleDelete(id) {
|
||||
live.pushEvent("delete_note", {id})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} uuid
|
||||
*/
|
||||
function truncateUUID(uuid) {
|
||||
return uuid ? uuid.substring(0, 8) + "..." : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateStr
|
||||
*/
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ""
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Notes ({encoder})</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-4">
|
||||
<!-- Info Banner -->
|
||||
<div class="mb-6 p-4 rounded-lg bg-blue-50 border border-blue-200">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded bg-blue-600 text-white">
|
||||
{encoder} JSON Encoder
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-blue-800">{info}</p>
|
||||
</div>
|
||||
|
||||
<!-- Create Note Form -->
|
||||
<form
|
||||
onsubmit={e => {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}}
|
||||
class="mb-8 p-4 bg-white rounded-lg shadow-sm border"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-4">Create Note</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title *</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
bind:value={title}
|
||||
placeholder="Enter note title"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
||||
<textarea
|
||||
id="content"
|
||||
bind:value={content}
|
||||
placeholder="Enter note content (optional)"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="color" class="block text-sm font-medium text-gray-700 mb-2">Color</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{#each colors as c}
|
||||
<button
|
||||
aria-label={c.name}
|
||||
id="color"
|
||||
type="button"
|
||||
onclick={() => (color = c.value)}
|
||||
class="w-8 h-8 rounded-full border-2 transition-transform hover:scale-110"
|
||||
class:ring-2={color === c.value}
|
||||
class:ring-offset-2={color === c.value}
|
||||
class:ring-blue-500={color === c.value}
|
||||
style="background-color: {c.value}; border-color: {c.value === '#fff' ? '#e5e7eb' : c.value}"
|
||||
title={c.name}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 bg-zinc-900 text-white rounded-md hover:bg-zinc-700 transition-colors">
|
||||
Add Note
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Notes Grid
|
||||
in:fly|global={{ x: -200, duration: 300 }}
|
||||
out:fade|global={{ duration: 200 }}
|
||||
|
||||
-->
|
||||
<ul class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#each notes as note, index (note.id)}
|
||||
<li
|
||||
animate:flip={{ delay: 500 }}
|
||||
role="listitem"
|
||||
id={`note-${note.id}`}
|
||||
aria-label={`Note ${index + 1}`}
|
||||
class="p-4 rounded-lg shadow-sm border transition-shadow hover:shadow-md"
|
||||
style="background-color: {note.color}"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="font-semibold text-gray-900 break-words flex-1 mr-2">{note.title}</h3>
|
||||
<button
|
||||
aria-label="Delete note"
|
||||
onclick={() => handleDelete(note.id)}
|
||||
class="text-gray-500 hover:text-red-600 transition-colors p-1"
|
||||
title="Delete note"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if note.content}
|
||||
<p class="text-sm text-gray-700 mb-3 break-words">{note.content}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between items-center text-xs text-gray-500 pt-2 border-t border-gray-200/50">
|
||||
<span class="font-mono" title={note.id}>ID: {truncateUUID(note.id)}</span>
|
||||
<span>{formatDate(note.inserted_at)}</span>
|
||||
</div>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="col-span-full text-center py-12 text-gray-500">
|
||||
<p class="text-lg">No notes yet</p>
|
||||
<p class="text-sm">Create your first note above!</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<!-- Notes Count -->
|
||||
{#if notes.length > 0}
|
||||
<div class="mt-6 text-center text-sm text-gray-500">
|
||||
{notes.length} note{notes.length === 1 ? "" : "s"}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -2,13 +2,10 @@ import Config
|
|||
|
||||
# Configure your database
|
||||
config :example, Example.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
hostname: "localhost",
|
||||
database: "example_dev",
|
||||
database: Path.expand("../example_dev.db", __DIR__),
|
||||
pool_size: 5,
|
||||
stacktrace: true,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
show_sensitive_data_on_connection_error: true
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ import Config
|
|||
# to provide built-in test partitioning in CI environment.
|
||||
# Run `mix help test` for more information.
|
||||
config :example, Example.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
hostname: "localhost",
|
||||
database: "example_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
database: Path.expand("../example_test#{System.get_env("MIX_TEST_PARTITION")}.db", __DIR__),
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 10
|
||||
pool_size: 5
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ defmodule Example.Application do
|
|||
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]},
|
||||
# Start the Telemetry supervisor
|
||||
ExampleWeb.Telemetry,
|
||||
# Start the Ecto repository (actually not used in this example, so skip it)
|
||||
# Example.Repo,
|
||||
# Start the Ecto repository
|
||||
Example.Repo,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Example.PubSub},
|
||||
# Start Finch
|
||||
|
|
|
|||
28
example_project/lib/example/note.ex
Normal file
28
example_project/lib/example/note.ex
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
defmodule Example.Note do
|
||||
@moduledoc """
|
||||
Ecto schema for notes with UUID primary key.
|
||||
|
||||
This schema demonstrates how Ecto structs work with LiveSvelte's
|
||||
OTP JSON encoder, which automatically converts structs to maps.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
|
||||
schema "notes" do
|
||||
field :title, :string
|
||||
field :content, :string
|
||||
field :color, :string, default: "#fef3c7"
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(note, attrs) do
|
||||
note
|
||||
|> cast(attrs, [:title, :content, :color])
|
||||
|> validate_required([:title])
|
||||
|> validate_length(:title, max: 100)
|
||||
|> validate_length(:content, max: 1000)
|
||||
end
|
||||
end
|
||||
55
example_project/lib/example/notes.ex
Normal file
55
example_project/lib/example/notes.ex
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
defmodule Example.Notes do
|
||||
@moduledoc """
|
||||
Context module for managing notes.
|
||||
"""
|
||||
import Ecto.Query
|
||||
alias Example.Repo
|
||||
alias Example.Note
|
||||
|
||||
@doc """
|
||||
Returns all notes ordered by creation date (newest first).
|
||||
"""
|
||||
def list_notes do
|
||||
Note
|
||||
|> order_by(desc: :inserted_at)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single note by ID.
|
||||
Raises `Ecto.NoResultsError` if the note does not exist.
|
||||
"""
|
||||
def get_note!(id), do: Repo.get!(Note, id)
|
||||
|
||||
@doc """
|
||||
Creates a new note.
|
||||
"""
|
||||
def create_note(attrs \\ %{}) do
|
||||
%Note{}
|
||||
|> Note.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates an existing note.
|
||||
"""
|
||||
def update_note(%Note{} = note, attrs) do
|
||||
note
|
||||
|> Note.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a note.
|
||||
"""
|
||||
def delete_note(%Note{} = note) do
|
||||
Repo.delete(note)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a changeset for tracking note changes.
|
||||
"""
|
||||
def change_note(%Note{} = note, attrs \\ %{}) do
|
||||
Note.changeset(note, attrs)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule Example.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :example,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
adapter: Ecto.Adapters.SQLite3
|
||||
end
|
||||
|
|
|
|||
|
|
@ -686,4 +686,139 @@ defmodule ExampleWeb.CoreComponents do
|
|||
def translate_errors(errors, field) when is_list(errors) do
|
||||
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a navigation link with active state styling.
|
||||
|
||||
## Examples
|
||||
|
||||
<.nav_link href={~p"/home"} current_path={@current_path}>Home</.nav_link>
|
||||
"""
|
||||
attr :href, :string, required: true
|
||||
attr :current_path, :string, required: true
|
||||
attr :class, :string, default: nil
|
||||
slot :inner_block, required: true
|
||||
|
||||
def nav_link(assigns) do
|
||||
~H"""
|
||||
<a
|
||||
href={@href}
|
||||
class={[
|
||||
"block px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
if(@current_path == @href,
|
||||
do: "bg-zinc-900 text-white",
|
||||
else: "text-zinc-700 hover:bg-zinc-100 hover:text-zinc-900"
|
||||
),
|
||||
@class
|
||||
]}
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a mobile navigation panel with slide-out sidebar.
|
||||
|
||||
## Examples
|
||||
|
||||
<.mobile_nav id="mobile-nav" current_path={@current_path}>
|
||||
<:section title="Basics">
|
||||
<.nav_link href={~p"/hello"} current_path={@current_path}>Hello</.nav_link>
|
||||
</:section>
|
||||
</.mobile_nav>
|
||||
"""
|
||||
attr :id, :string, required: true
|
||||
attr :current_path, :string, required: true
|
||||
|
||||
slot :section, required: true do
|
||||
attr :title, :string, required: true
|
||||
end
|
||||
|
||||
def mobile_nav(assigns) do
|
||||
~H"""
|
||||
<div id={@id} class="relative z-50 lg:hidden" role="dialog" aria-modal="true">
|
||||
<div
|
||||
id={"#{@id}-backdrop"}
|
||||
class="fixed inset-0 bg-zinc-900/80 opacity-0 transition-opacity duration-300 ease-linear hidden"
|
||||
phx-click={hide_mobile_nav(@id)}
|
||||
/>
|
||||
|
||||
<div
|
||||
id={"#{@id}-container"}
|
||||
class="fixed inset-0 flex -translate-x-full transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<div class="relative mr-16 flex w-full max-w-xs flex-1">
|
||||
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 p-2.5 text-white"
|
||||
phx-click={hide_mobile_nav(@id)}
|
||||
>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<.icon name="hero-x-mark" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
|
||||
<div class="flex h-16 shrink-0 items-center border-b border-zinc-200">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-semibold">LiveSvelte</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-1 flex-col">
|
||||
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||
<%= for section <- @section do %>
|
||||
<li>
|
||||
<div class="text-xs font-semibold leading-6 text-zinc-400 uppercase tracking-wider">
|
||||
<%= section.title %>
|
||||
</div>
|
||||
<ul role="list" class="-mx-2 mt-2 space-y-1">
|
||||
<%= render_slot(section) %>
|
||||
</ul>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def show_mobile_nav(js \\ %JS{}, id) do
|
||||
js
|
||||
|> JS.show(to: "##{id}")
|
||||
|> JS.show(
|
||||
to: "##{id}-backdrop",
|
||||
transition: {"transition-opacity ease-linear duration-300", "opacity-0", "opacity-100"}
|
||||
)
|
||||
|> JS.remove_class("-translate-x-full",
|
||||
to: "##{id}-container",
|
||||
transition: {"transition ease-in-out duration-300 transform", "-translate-x-full", "translate-x-0"}
|
||||
)
|
||||
|> JS.add_class("overflow-hidden", to: "body")
|
||||
end
|
||||
|
||||
def hide_mobile_nav(js \\ %JS{}, id) do
|
||||
js
|
||||
|> JS.add_class("-translate-x-full",
|
||||
to: "##{id}-container",
|
||||
transition: {"transition ease-in-out duration-300 transform", "translate-x-0", "-translate-x-full"}
|
||||
)
|
||||
|> JS.hide(
|
||||
to: "##{id}-backdrop",
|
||||
transition: {"transition-opacity ease-linear duration-300", "opacity-100", "opacity-0"}
|
||||
)
|
||||
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|
||||
|> JS.remove_class("overflow-hidden", to: "body")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,4 +2,101 @@ defmodule ExampleWeb.Layouts do
|
|||
use ExampleWeb, :html
|
||||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
# Single source of truth for main nav (sidebar + desktop). Layout has route helpers (~p).
|
||||
def nav_groups do
|
||||
[
|
||||
%{
|
||||
label: "Basics",
|
||||
links: [
|
||||
%{label: "Hello World", to: ~p"/hello-world"},
|
||||
%{label: "Lodash", to: ~p"/lodash"},
|
||||
%{label: "Struct Props", to: ~p"/live-struct"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
label: "Interactive",
|
||||
links: [
|
||||
%{label: "Counter", to: ~p"/live-simple-counter"},
|
||||
%{label: "Lights", to: ~p"/live-lights"},
|
||||
%{label: "Sigil", to: ~p"/live-sigil"},
|
||||
%{label: "Plus/Minus (Static)", to: ~p"/plus-minus-svelte"},
|
||||
%{label: "Plus/Minus (Live)", to: ~p"/live-plus-minus"},
|
||||
%{label: "Hybrid Counter", to: ~p"/live-plus-minus-hybrid"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
label: "Data",
|
||||
links: [
|
||||
%{label: "Log List", to: ~p"/live-log-list"},
|
||||
%{label: "Breaking News", to: ~p"/live-breaking-news"},
|
||||
%{label: "Chat", to: ~p"/live-chat"},
|
||||
%{label: "LiveJSON", to: ~p"/live-json"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
label: "Slots",
|
||||
links: [
|
||||
%{label: "Simple Slots", to: ~p"/live-slots-simple"},
|
||||
%{label: "Dynamic Slots", to: ~p"/live-slots-dynamic"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
label: "Advanced",
|
||||
links: [
|
||||
%{label: "Client Loading", to: ~p"/live-client-side-loading"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
label: "Ecto",
|
||||
links: [
|
||||
%{label: "Notes (OTP)", to: ~p"/live-notes-otp"}
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def nav_sidebar_items(assigns) do
|
||||
assigns = assign(assigns, :nav_groups, nav_groups())
|
||||
|
||||
~H"""
|
||||
<nav class="flex flex-1 flex-col">
|
||||
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||
<li :for={group <- @nav_groups}>
|
||||
<div class="text-xs font-semibold leading-6 text-zinc-400 uppercase tracking-wider">
|
||||
<%= group.label %>
|
||||
</div>
|
||||
<ul role="list" class="-mx-2 mt-2 space-y-1">
|
||||
<li :for={link <- group.links}>
|
||||
<a href={link.to} class="block px-3 py-2 rounded-md text-sm font-medium text-zinc-700 hover:bg-zinc-100">
|
||||
<%= link.label %>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
|
||||
def nav_desktop_dropdowns(assigns) do
|
||||
assigns = assign(assigns, :nav_groups, nav_groups())
|
||||
|
||||
~H"""
|
||||
<nav class="hidden lg:flex lg:items-center lg:gap-1">
|
||||
<div :for={group <- @nav_groups} class="relative group">
|
||||
<button type="button" class="px-3 py-2 text-sm font-medium text-zinc-700 hover:text-zinc-900 rounded-md hover:bg-zinc-100">
|
||||
<%= group.label %>
|
||||
</button>
|
||||
<div class="absolute left-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-zinc-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-150 z-50">
|
||||
<div class="py-1">
|
||||
<a :for={link <- group.links} href={link.to} class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100">
|
||||
<%= link.label %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,57 +1,92 @@
|
|||
<header class="px-4 sm:px-6 lg:px-8 h-[70px]">
|
||||
<div class="flex items-center justify-between border-b border-zinc-100 py-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/">
|
||||
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<%!-- Mobile Navigation Sidebar --%>
|
||||
<div id="mobile-nav" class="relative z-50 lg:hidden hidden" role="dialog" aria-modal="true">
|
||||
<div
|
||||
id="mobile-nav-backdrop"
|
||||
class="fixed inset-0 bg-zinc-900/80 opacity-0 transition-opacity duration-300 ease-linear"
|
||||
phx-click={ExampleWeb.CoreComponents.hide_mobile_nav("mobile-nav")}
|
||||
/>
|
||||
|
||||
<p class="rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand">
|
||||
LiveSvelte Examples
|
||||
</p>
|
||||
<div
|
||||
id="mobile-nav-container"
|
||||
class="fixed inset-0 flex -translate-x-full transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<div class="relative mr-16 flex w-full max-w-xs flex-1">
|
||||
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 p-2.5 text-white"
|
||||
phx-click={ExampleWeb.CoreComponents.hide_mobile_nav("mobile-nav")}
|
||||
>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<.icon name="hero-x-mark" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
|
||||
<div class="flex h-16 shrink-0 items-center border-b border-zinc-200">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-semibold">LiveSvelte</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<.nav_sidebar_items />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<%= for {path, text} <- [
|
||||
{~p"/hello-world", "1"},
|
||||
{~p"/lodash", "2"},
|
||||
{~p"/live-struct", "3"},
|
||||
{~p"/live-simple-counter", "4"},
|
||||
{~p"/live-lights", "5"},
|
||||
{~p"/live-sigil", "6"},
|
||||
{~p"/plus-minus-svelte", "7"},
|
||||
{~p"/live-plus-minus", "8"},
|
||||
{~p"/live-plus-minus-hybrid", "9"},
|
||||
{~p"/live-log-list", "10"},
|
||||
{~p"/live-breaking-news", "11"},
|
||||
{~p"/live-chat", "12"},
|
||||
{~p"/live-json", "13"},
|
||||
{~p"/live-slots-simple", "14"},
|
||||
{~p"/live-slots-dynamic", "15"},
|
||||
{~p"/live-client-side-loading", "16"}
|
||||
] do %>
|
||||
<a href={path} class="font-semibold leading-6 text-zinc-900 hover:text-zinc-700 hover:underline p-2">
|
||||
<%= text %>
|
||||
<header class="sticky top-0 z-40 bg-white border-b border-zinc-200">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<%!-- Logo and mobile menu button --%>
|
||||
<div class="flex items-center gap-4">
|
||||
<%!-- Mobile menu button --%>
|
||||
<button
|
||||
type="button"
|
||||
class="lg:hidden -m-2.5 p-2.5 text-zinc-700"
|
||||
phx-click={ExampleWeb.CoreComponents.show_mobile_nav("mobile-nav")}
|
||||
>
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<.icon name="hero-bars-3" class="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="https://github.com/woutdp/live_svelte"
|
||||
target="_blank"
|
||||
class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<span class="hidden sm:block rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand">
|
||||
LiveSvelte Examples
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<%!-- Desktop Navigation --%>
|
||||
<.nav_desktop_dropdowns />
|
||||
|
||||
<%!-- GitHub Link --%>
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="https://github.com/woutdp/live_svelte"
|
||||
target="_blank"
|
||||
class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="p-0 sm:py-20 sm:px-6 lg:px-8 h-[calc(100vh_-_70px)]">
|
||||
<main class="p-4 sm:py-8 sm:px-6 lg:px-8 min-h-[calc(100vh-65px)]">
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1 +1,149 @@
|
|||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-bold text-zinc-900 mb-4">LiveSvelte Examples</h1>
|
||||
<p class="text-lg text-zinc-600">
|
||||
Explore these examples to learn how to integrate Svelte components with Phoenix LiveView.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-8 md:grid-cols-2">
|
||||
<%!-- Basics --%>
|
||||
<div class="bg-white rounded-lg border border-zinc-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
|
||||
<span class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm">1</span>
|
||||
Basics
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href={~p"/hello-world"} class="text-blue-600 hover:text-blue-800 hover:underline">Hello World</a>
|
||||
<span class="text-zinc-500 text-sm">- Simple component rendering</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/lodash"} class="text-blue-600 hover:text-blue-800 hover:underline">Lodash</a>
|
||||
<span class="text-zinc-500 text-sm">- Using npm packages</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-struct"} class="text-blue-600 hover:text-blue-800 hover:underline">Struct Props</a>
|
||||
<span class="text-zinc-500 text-sm">- Passing Elixir structs as props</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%!-- Interactive --%>
|
||||
<div class="bg-white rounded-lg border border-zinc-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
|
||||
<span class="w-8 h-8 rounded-full bg-green-100 text-green-600 flex items-center justify-center text-sm">2</span>
|
||||
Interactive
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href={~p"/live-simple-counter"} class="text-blue-600 hover:text-blue-800 hover:underline">Counter</a>
|
||||
<span class="text-zinc-500 text-sm">- Server + client state</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-lights"} class="text-blue-600 hover:text-blue-800 hover:underline">Lights</a>
|
||||
<span class="text-zinc-500 text-sm">- Multiple components sharing state</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-sigil"} class="text-blue-600 hover:text-blue-800 hover:underline">Sigil</a>
|
||||
<span class="text-zinc-500 text-sm">- Inline Svelte with ~V sigil</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/plus-minus-svelte"} class="text-blue-600 hover:text-blue-800 hover:underline">Plus/Minus (Static)</a>
|
||||
<span class="text-zinc-500 text-sm">- Dead view integration</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-plus-minus"} class="text-blue-600 hover:text-blue-800 hover:underline">Plus/Minus (Live)</a>
|
||||
<span class="text-zinc-500 text-sm">- LiveView integration</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-plus-minus-hybrid"} class="text-blue-600 hover:text-blue-800 hover:underline">Hybrid Counter</a>
|
||||
<span class="text-zinc-500 text-sm">- Client + server events</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%!-- Data --%>
|
||||
<div class="bg-white rounded-lg border border-zinc-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
|
||||
<span class="w-8 h-8 rounded-full bg-purple-100 text-purple-600 flex items-center justify-center text-sm">3</span>
|
||||
Data
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href={~p"/live-log-list"} class="text-blue-600 hover:text-blue-800 hover:underline">Log List</a>
|
||||
<span class="text-zinc-500 text-sm">- Dynamic list updates</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-breaking-news"} class="text-blue-600 hover:text-blue-800 hover:underline">Breaking News</a>
|
||||
<span class="text-zinc-500 text-sm">- Real-time updates with ~V sigil</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-chat"} class="text-blue-600 hover:text-blue-800 hover:underline">Chat</a>
|
||||
<span class="text-zinc-500 text-sm">- PubSub + pushEvent</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-json"} class="text-blue-600 hover:text-blue-800 hover:underline">LiveJSON</a>
|
||||
<span class="text-zinc-500 text-sm">- Efficient JSON diffing</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%!-- Slots --%>
|
||||
<div class="bg-white rounded-lg border border-zinc-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
|
||||
<span class="w-8 h-8 rounded-full bg-orange-100 text-orange-600 flex items-center justify-center text-sm">4</span>
|
||||
Slots
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href={~p"/live-slots-simple"} class="text-blue-600 hover:text-blue-800 hover:underline">Simple Slots</a>
|
||||
<span class="text-zinc-500 text-sm">- Basic slot usage</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-slots-dynamic"} class="text-blue-600 hover:text-blue-800 hover:underline">Dynamic Slots</a>
|
||||
<span class="text-zinc-500 text-sm">- Named slots with dynamic content</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%!-- Advanced --%>
|
||||
<div class="bg-white rounded-lg border border-zinc-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
|
||||
<span class="w-8 h-8 rounded-full bg-red-100 text-red-600 flex items-center justify-center text-sm">5</span>
|
||||
Advanced
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href={~p"/live-client-side-loading"} class="text-blue-600 hover:text-blue-800 hover:underline">Client Loading</a>
|
||||
<span class="text-zinc-500 text-sm">- Loading states and SSR</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%!-- Ecto --%>
|
||||
<div class="bg-white rounded-lg border border-zinc-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
|
||||
<span class="w-8 h-8 rounded-full bg-teal-100 text-teal-600 flex items-center justify-center text-sm">6</span>
|
||||
Ecto
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href={~p"/live-notes-otp"} class="text-blue-600 hover:text-blue-800 hover:underline">Notes (OTP JSON)</a>
|
||||
<span class="text-zinc-500 text-sm">- Default OTP encoder with SQLite</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-notes-jason"} class="text-blue-600 hover:text-blue-800 hover:underline">Notes (Jason)</a>
|
||||
<span class="text-zinc-500 text-sm">- Jason-compatible approach</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 text-center text-zinc-500 text-sm">
|
||||
<p>
|
||||
View the source code on
|
||||
<a href="https://github.com/woutdp/live_svelte" target="_blank" class="text-blue-600 hover:underline">GitHub</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
58
example_project/lib/example_web/live/live_notes_otp.ex
Normal file
58
example_project/lib/example_web/live/live_notes_otp.ex
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
defmodule ExampleWeb.LiveNotesOtp do
|
||||
@moduledoc """
|
||||
LiveView demonstrating Ecto structs with OTP JSON encoder.
|
||||
|
||||
The OTP JSON encoder (LiveSvelte.JSON) automatically converts Ecto structs
|
||||
to maps, stripping the __struct__ key. This means you can pass Ecto schemas
|
||||
directly to Svelte components without any additional configuration.
|
||||
|
||||
This is the default encoder for LiveSvelte since v0.17.0.
|
||||
"""
|
||||
use ExampleWeb, :live_view
|
||||
alias Example.Notes
|
||||
|
||||
@info """
|
||||
Using OTP JSON encoder (default since v0.17.0). Ecto structs are automatically
|
||||
converted to maps.
|
||||
"""
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte
|
||||
name="NotesApp"
|
||||
props={%{
|
||||
notes: @notes,
|
||||
encoder: "OTP",
|
||||
info: @info
|
||||
}}
|
||||
socket={@socket}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, assign(socket, notes: Notes.list_notes(), info: @info)}
|
||||
end
|
||||
|
||||
def handle_event("create_note", params, socket) do
|
||||
case Notes.create_note(params) do
|
||||
{:ok, _note} ->
|
||||
{:noreply, assign(socket, :notes, Notes.list_notes())}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to create note")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("delete_note", %{"id" => id}, socket) do
|
||||
note = Notes.get_note!(id)
|
||||
|
||||
case Notes.delete_note(note) do
|
||||
{:ok, _note} ->
|
||||
{:noreply, assign(socket, :notes, Notes.list_notes())}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to delete note")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -35,6 +35,8 @@ defmodule ExampleWeb.Router do
|
|||
live "/live-slots-simple", LiveSlotsSimple
|
||||
live "/live-slots-dynamic", LiveSlotsDynamic
|
||||
live "/live-client-side-loading", LiveClientSideLoading
|
||||
# Ecto Examples
|
||||
live "/live-notes-otp", LiveNotesOtp
|
||||
# not referenced in app.html.heex:
|
||||
live "/live-composition", LiveComposition
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,10 +32,9 @@ defmodule Example.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:ecto_sql, "~> 3.12"},
|
||||
{:finch, "~> 0.13"},
|
||||
{:gettext, "~> 0.20"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:json_diff_ex, "~> 0.6", override: true},
|
||||
{:live_json, "~> 0.4.5"},
|
||||
{:live_svelte, path: ".."},
|
||||
|
|
@ -45,7 +44,7 @@ defmodule Example.MixProject do
|
|||
{:phoenix_live_dashboard, "~> 0.8"},
|
||||
{:phoenix_live_view, "~> 0.19"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:ecto_sqlite3, "~> 0.17"},
|
||||
{:swoosh, "~> 1.3"},
|
||||
{:telemetry_metrics, "~> 0.6"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
%{
|
||||
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
|
||||
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
|
||||
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
|
||||
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
|
||||
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
|
||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"},
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"expo": {:hex, :expo, "0.4.0", "bbe4bf455e2eb2ebd2f1e7d83530ce50fb9990eb88fc47855c515bfdf1c6626f", [:mix], [], "hexpm", "a8ed1683ec8b7c7fa53fd7a41b2c6935f539168a6bb0616d7fd6b58a36f3abf2"},
|
||||
"exqlite": {:hex, :exqlite, "0.34.0", "ebca3570eb4c4eb4345d76c8e44ce31a62de7b24a54fd118164480f2954bd540", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "bcdc58879a0db5e08cd5f6fbe07a0692ceffaaaa617eab46b506137edf0a2742"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
|
||||
"floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
|
||||
|
|
@ -19,7 +21,7 @@
|
|||
"json_diff_ex": {:hex, :json_diff_ex, "0.6.7", "679eb6df8fb59b061434798ecf641f510e8dc7ae9d6ca22e593457bfe3d51a5e", [:mix], [], "hexpm", "7da6360cfb8aea96513d67c1a6401e1bb1cf2c988e2e81cfb67224fd05187043"},
|
||||
"jsonpatch": {:hex, :jsonpatch, "0.13.1", "fd32eae78e2a7d9a2f40ee2468d22b75676b5d49bb5f6f929204123b15e3f254", [:make, :mix], [], "hexpm", "b3a29d2a3d56149e50fd5a8e19f5dd82c7f85491871b5598405cc7fb97de8f0b"},
|
||||
"live_json": {:hex, :live_json, "0.4.5", "7a97932a8bb944d546a2e0af231aa90889eeaa0ab3f77afc50748636d9202581", [:mix], [{:jason, ">= 1.3.0", [hex: :jason, repo: "hexpm", optional: true]}, {:json_diff_ex, "~> 0.5.0", [hex: :json_diff_ex, repo: "hexpm", optional: false]}, {:jsonpatch, "~> 0.13.1", [hex: :jsonpatch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.1.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "43f941c94ef3a917f0940552316ae23128b8da274511d1d356494441a69edd71"},
|
||||
"live_svelte": {:hex, :live_svelte, "0.3.1", "81c7c632c1425772b498a445cb37a4f664116c031d6a0a4d0602100187452be6", [:mix], [{:esbuild, "~> 0.5", [hex: :esbuild, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:nodejs, "~> 2.0", [hex: :nodejs, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7e0353e57af92bcbaa142e38a26012a8aaa818cd53f5d33f627283fe7ba825d6"},
|
||||
"live_svelte": {:hex, :live_svelte, "0.17.0", "953603f1358cceb8e893ad41c1e5243dba5bd95c57b5a8162164f593bcf499f8", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:nodejs, "~> 3.1", [hex: :nodejs, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "c60055b3acc9ee56258589b98b3b0a5e2a3b5bec4298af5f3936384ff4c4a44d"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
|
||||
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
|
||||
|
|
@ -31,13 +33,12 @@
|
|||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
|
||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.5", "261f21b67aea8162239b2d6d3b4c31efde4daa22a20d80b19c2c0f21b34b270e", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20884bf58a90ff5a5663420f5d2c368e9e15ed1ad5e911daf0916ea3c57f77ac"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
|
||||
|
|
@ -46,5 +47,5 @@
|
|||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
defmodule Example.Repo.Migrations.CreateNotes do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:notes, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :title, :string, null: false
|
||||
add :content, :text
|
||||
add :color, :string, default: "#fef3c7"
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,6 +11,8 @@ defmodule LiveSvelte.JSON do
|
|||
- Automatically converts structs to maps
|
||||
- Converts all map keys to strings (matching Jason behavior)
|
||||
- Handles nested data structures
|
||||
- Converts DateTime/NaiveDateTime/Date/Time to ISO 8601 strings
|
||||
- Strips Ecto schema metadata (`__meta__` field) automatically
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -22,9 +24,9 @@ defmodule LiveSvelte.JSON do
|
|||
## SSR Compatibility Note
|
||||
|
||||
When using server-side rendering, the NodeJS worker uses Jason internally
|
||||
to serialize data to the Node.js process. This module is designed to produce
|
||||
Jason-compatible output, ensuring consistency between SSR and client-side
|
||||
hydration.
|
||||
to serialize data to the Node.js process. The `prepare/1` function is used
|
||||
to convert Elixir terms to JSON-compatible values before passing to NodeJS,
|
||||
ensuring consistency between SSR and client-side hydration.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -51,6 +53,31 @@ defmodule LiveSvelte.JSON do
|
|||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepares an Elixir term for JSON serialization.
|
||||
|
||||
This function recursively converts Elixir terms to JSON-compatible values:
|
||||
- Structs become maps (with `__struct__` key stripped)
|
||||
- Ecto schemas have `__meta__` field stripped
|
||||
- DateTime/NaiveDateTime/Date/Time become ISO 8601 strings
|
||||
- Atoms become strings
|
||||
- nil becomes :null (for Erlang's :json module)
|
||||
|
||||
This is useful for preparing data before passing to external JSON encoders
|
||||
(like the NodeJS worker which uses Jason internally).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> LiveSvelte.JSON.prepare(%DateTime{} = dt)
|
||||
"2026-01-31T20:31:10Z"
|
||||
|
||||
iex> LiveSvelte.JSON.prepare(%{notes: [%MyApp.Note{title: "Hello"}]})
|
||||
%{"notes" => [%{"title" => "Hello"}]}
|
||||
|
||||
"""
|
||||
@spec prepare(term()) :: term()
|
||||
def prepare(term), do: prepare_term(term)
|
||||
|
||||
# Recursively prepare terms for JSON encoding.
|
||||
# Converts structs to maps, nil to null, and handles nested structures.
|
||||
|
||||
|
|
@ -66,6 +93,22 @@ defmodule LiveSvelte.JSON do
|
|||
Atom.to_string(atom)
|
||||
end
|
||||
|
||||
# DateTime/NaiveDateTime/Date/Time become ISO 8601 strings
|
||||
# These must come before the generic struct handler
|
||||
defp prepare_term(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
|
||||
defp prepare_term(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
|
||||
defp prepare_term(%Date{} = d), do: Date.to_iso8601(d)
|
||||
defp prepare_term(%Time{} = t), do: Time.to_iso8601(t)
|
||||
|
||||
# Ecto schema structs - strip __meta__ field
|
||||
# Must come before the generic struct handler
|
||||
defp prepare_term(%{__struct__: _, __meta__: _} = struct) do
|
||||
struct
|
||||
|> Map.from_struct()
|
||||
|> Map.delete(:__meta__)
|
||||
|> prepare_term()
|
||||
end
|
||||
|
||||
# Structs become maps (strip __struct__ key)
|
||||
defp prepare_term(%_{} = struct) do
|
||||
struct
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@ defmodule LiveSvelte.SSR.NodeJS do
|
|||
@behaviour LiveSvelte.SSR
|
||||
|
||||
def render(name, props, slots) do
|
||||
# Prepare props and slots for JSON serialization before passing to NodeJS.
|
||||
# This converts structs to maps, DateTime to ISO 8601 strings, and strips
|
||||
# Ecto metadata (__meta__). Required because NodeJS.call! uses Jason internally.
|
||||
prepared_props = LiveSvelte.JSON.prepare(props)
|
||||
prepared_slots = LiveSvelte.JSON.prepare(slots)
|
||||
|
||||
try do
|
||||
NodeJS.call!({"server", "render"}, [name, props, slots], binary: true)
|
||||
NodeJS.call!({"server", "render"}, [name, prepared_props, prepared_slots], binary: true)
|
||||
catch
|
||||
:exit, {:noproc, _} ->
|
||||
message = """
|
||||
|
|
|
|||
Loading…
Reference in a new issue