310 lines
13 kB
1
defmodule BlogWeb.PostLive.Index do
2
use BlogWeb, :live_view
3
alias BlogWeb.Presence
4
alias Blog.Content
5
6
@presence_topic "blog_presence"
7
8
# TODO add meta tags
9
def mount(_params, _session, socket) do
10
reader_id = if connected?(socket) do
11
id = "reader_#{:crypto.strong_rand_bytes(8) |> Base.encode16()}"
12
13
# Generate a random color for this visitor
14
hue = :rand.uniform(360)
15
color = "hsl(#{hue}, 70%, 60%)"
16
17
{:ok, _} =
18
Presence.track(self(), @presence_topic, id, %{
19
page: "index",
20
joined_at: DateTime.utc_now(),
21
cursor_position: nil,
22
color: color,
23
display_name: nil
24
})
25
26
Phoenix.PubSub.subscribe(Blog.PubSub, @presence_topic)
27
id
28
else
29
nil
30
end
31
32
posts = Blog.Content.Post.all()
33
%{tech: tech_posts, non_tech: non_tech_posts} = Content.categorize_posts(posts)
34
35
# Get all current visitors from presence
36
visitor_cursors =
37
Presence.list(@presence_topic)
38
|> Enum.map(fn {id, %{metas: [meta | _]}} -> {id, meta} end)
39
|> Enum.into(%{})
40
41
total_readers = map_size(visitor_cursors)
42
43
{:ok,
44
assign(socket,
45
tech_posts: tech_posts,
46
non_tech_posts: non_tech_posts,
47
total_readers: total_readers,
48
page_title: "Tidbits & Thoughts - A Retro Hacker Blog",
49
cursor_position: nil,
50
reader_id: reader_id,
51
visitor_cursors: visitor_cursors,
52
name_form: %{"name" => ""},
53
name_submitted: false
54
)}
55
end
56
57
def handle_info(%{event: "presence_diff"}, socket) do
58
visitor_cursors =
59
Presence.list(@presence_topic)
60
|> Enum.map(fn {id, %{metas: [meta | _]}} -> {id, meta} end)
61
|> Enum.into(%{})
62
63
total_readers = map_size(visitor_cursors)
64
65
{:noreply, assign(socket, total_readers: total_readers, visitor_cursors: visitor_cursors)}
66
end
67
68
def handle_event("mousemove", %{"x" => x, "y" => y}, socket) do
69
reader_id = socket.assigns.reader_id
70
71
# Update local cursor position
72
cursor_position = %{x: x, y: y}
73
socket = assign(socket, cursor_position: cursor_position)
74
75
if reader_id do
76
# Update the presence with the new cursor position
77
Presence.update(self(), @presence_topic, reader_id, fn meta ->
78
Map.put(meta, :cursor_position, cursor_position)
79
end)
80
end
81
82
{:noreply, socket}
83
end
84
85
def handle_event("save_name", %{"name" => name}, socket) do
86
reader_id = socket.assigns.reader_id
87
trimmed_name = String.trim(name)
88
89
if reader_id && trimmed_name != "" do
90
# Update the presence with the display name
91
Presence.update(self(), @presence_topic, reader_id, fn meta ->
92
Map.put(meta, :display_name, trimmed_name)
93
end)
94
95
{:noreply, assign(socket, name_submitted: true)}
96
else
97
{:noreply, socket}
98
end
99
end
100
101
def handle_event("validate_name", %{"name" => name}, socket) do
102
{:noreply, assign(socket, name_form: %{"name" => name})}
103
end
104
105
def render(assigns) do
106
~H"""
107
<div
108
class="py-12 px-4 sm:px-6 lg:px-8 min-h-screen"
109
id="cursor-tracker-container"
110
phx-hook="CursorTracker"
111
>
112
<!-- Name input form if not yet submitted -->
113
<%= if @reader_id && !@name_submitted do %>
114
<div class="fixed top-4 left-4 z-50">
115
<.form for={%{}} phx-submit="save_name" phx-change="validate_name" class="flex items-center space-x-2">
116
<div class="bg-gradient-to-r from-fuchsia-500 to-cyan-500 p-0.5 rounded-lg shadow-md">
117
<div class="bg-white rounded-md px-3 py-2 flex items-center space-x-2">
118
<input
119
type="text"
120
name="name"
121
value={@name_form["name"]}
122
placeholder="WHAT'S YOUR NAME?"
123
maxlength="20"
124
class="text-sm font-mono text-gray-800 focus:outline-none"
125
/>
126
<button type="submit" class="bg-gradient-to-r from-fuchsia-500 to-cyan-500 text-white text-xs font-bold px-3 py-1 rounded-md">
127
SET
128
</button>
129
</div>
130
</div>
131
</.form>
132
</div>
133
<% end %>
134
135
<%= if @cursor_position do %>
136
<div class="fixed top-4 right-4 bg-gradient-to-r from-fuchsia-500 to-cyan-500 text-white px-3 py-1 rounded-lg shadow-md text-sm font-mono z-50">
137
x: <%= @cursor_position.x %>, y: <%= @cursor_position.y %>
138
</div>
139
140
<!-- Full screen crosshair with gradient and smooth transitions -->
141
<div class="fixed inset-0 pointer-events-none z-40">
142
<!-- Horizontal line across entire screen with gradient -->
143
<div
144
class="absolute w-full h-0.5 opacity-40 transition-all duration-200 ease-out"
145
style={"top: #{@cursor_position.y}px; background: linear-gradient(to right, #d946ef, #0891b2);"}
146
></div>
147
148
<!-- Vertical line across entire screen with gradient -->
149
<div
150
class="absolute h-full w-0.5 opacity-40 transition-all duration-200 ease-out"
151
style={"left: #{@cursor_position.x}px; background: linear-gradient(to bottom, #d946ef, #0891b2);"}
152
></div>
153
</div>
154
<% end %>
155
156
<!-- Show all visitor cursors except our own -->
157
<%= for {visitor_id, visitor} <- @visitor_cursors do %>
158
<%= if visitor_id != @reader_id && visitor.cursor_position do %>
159
<div
160
class="fixed pointer-events-none z-45 transition-all duration-200 ease-out"
161
style={"left: #{visitor.cursor_position.x}px; top: #{visitor.cursor_position.y}px; transform: translate(-50%, -50%);"}
162
>
163
<!-- Visitor cursor indicator -->
164
<div class="flex flex-col items-center">
165
<!-- Cursor icon -->
166
<svg width="16" height="16" viewBox="0 0 16 16" class="transform -rotate-12" style={"filter: drop-shadow(0 0 1px #000); fill: #{visitor.color};"}>
167
<path d="M0 0L5 12L7.5 9.5L14 14L16 0Z" />
168
</svg>
169
170
<!-- Visitor label with name if available -->
171
<div class="mt-1 px-2 py-0.5 rounded text-xs font-mono text-white shadow-sm whitespace-nowrap" style={"background-color: #{visitor.color}; opacity: 0.85;"}>
172
<%= if visitor.display_name, do: visitor.display_name, else: "visitor #{String.slice(visitor_id, -4, 4)}" %>
173
</div>
174
</div>
175
</div>
176
<% end %>
177
<% end %>
178
179
<div class="max-w-7xl mx-auto">
180
<!-- Header with retro styling -->
181
<header class="mb-12 text-center">
182
<div class="inline-block p-1 bg-gradient-to-r from-fuchsia-500 to-cyan-500 rounded-lg shadow-lg mb-6">
183
<h1 class="text-4xl md:text-5xl font-bold bg-white px-6 py-3 rounded-md">
184
<span class="text-transparent bg-clip-text bg-gradient-to-r from-fuchsia-600 to-cyan-600">
185
Thoughts & Tidbits
186
</span>
187
</h1>
188
</div>
189
190
<div class="flex justify-center items-center space-x-2 text-sm text-gray-600 mb-4">
191
<div class="inline-flex items-center px-3 py-1 rounded-full bg-gradient-to-r from-fuchsia-100 to-cyan-100 border border-fuchsia-200">
192
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-fuchsia-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
193
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
194
</svg>
195
<span><%= @total_readers %> <%= if @total_readers == 1, do: "person", else: "people" %> browsing</span>
196
</div>
197
</div>
198
199
<p class="text-gray-600 max-w-2xl mx-auto">
200
A collection of thoughts on technology, life, and weird little things I make.
201
</p>
202
</header>
203
204
<!-- Two column layout for posts -->
205
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
206
<!-- Tech Posts Column -->
207
<div class="bg-gradient-to-br from-fuchsia-50 to-cyan-50 rounded-xl p-6 shadow-lg border border-fuchsia-100">
208
<div class="flex items-center mb-6">
209
<div class="w-3 h-3 rounded-full bg-fuchsia-400 mr-2"></div>
210
<div class="w-3 h-3 rounded-full bg-cyan-400 mr-2"></div>
211
<div class="w-3 h-3 rounded-full bg-indigo-400 mr-4"></div>
212
<h2 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-fuchsia-600 to-cyan-600">
213
Tech & Programming
214
</h2>
215
</div>
216
217
<div class="space-y-4">
218
<%= for post <- @tech_posts do %>
219
<div class="group bg-white rounded-lg p-4 shadow-sm hover:shadow-md transition-all duration-300 border-l-4 border-fuchsia-400">
220
<.link navigate={~p"/post/#{post.slug}"} class="block">
221
<h3 class="text-xl font-bold text-gray-800 group-hover:text-fuchsia-600 transition-colors">
222
<%= post.title %>
223
</h3>
224
<div class="flex flex-wrap gap-2 mt-2">
225
<%= for tag <- post.tags do %>
226
<span class="inline-block px-2 py-1 bg-gradient-to-r from-fuchsia-100 to-cyan-100 rounded-full text-xs font-medium text-gray-700">
227
<%= tag.name %>
228
</span>
229
<% end %>
230
</div>
231
</.link>
232
</div>
233
<% end %>
234
</div>
235
</div>
236
237
<!-- Non-Tech Posts Column -->
238
<div class="bg-gradient-to-br from-cyan-50 to-fuchsia-50 rounded-xl p-6 shadow-lg border border-cyan-100">
239
<div class="flex items-center mb-6">
240
<div class="w-3 h-3 rounded-full bg-cyan-400 mr-2"></div>
241
<div class="w-3 h-3 rounded-full bg-fuchsia-400 mr-2"></div>
242
<div class="w-3 h-3 rounded-full bg-indigo-400 mr-4"></div>
243
<h2 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-600 to-fuchsia-600">
244
Life & Everything Else
245
</h2>
246
</div>
247
248
<div class="space-y-4">
249
<%= for post <- @non_tech_posts do %>
250
<div class="group bg-white rounded-lg p-4 shadow-sm hover:shadow-md transition-all duration-300 border-l-4 border-cyan-400">
251
<.link navigate={~p"/post/#{post.slug}"} class="block">
252
<h3 class="text-xl font-bold text-gray-800 group-hover:text-cyan-600 transition-colors">
253
<%= post.title %>
254
</h3>
255
<div class="flex flex-wrap gap-2 mt-2">
256
<%= for tag <- post.tags do %>
257
<span class="inline-block px-2 py-1 bg-gradient-to-r from-cyan-100 to-fuchsia-100 rounded-full text-xs font-medium text-gray-700">
258
<%= tag.name %>
259
</span>
260
<% end %>
261
</div>
262
</.link>
263
</div>
264
<% end %>
265
</div>
266
</div>
267
268
<!-- Non-Tech Posts Column -->
269
<div class="bg-gradient-to-br from-cyan-50 to-fuchsia-50 rounded-xl p-6 shadow-lg border border-cyan-100">
270
<div class="flex items-center mb-6">
271
<div class="w-3 h-3 rounded-full bg-cyan-400 mr-2"></div>
272
<div class="w-3 h-3 rounded-full bg-fuchsia-400 mr-2"></div>
273
<div class="w-3 h-3 rounded-full bg-indigo-400 mr-4"></div>
274
<h2 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-600 to-fuchsia-600">
275
Tech Demos
276
</h2>
277
</div>
278
279
<div class="space-y-4">
280
<%= for post <- @non_tech_posts do %>
281
<div class="group bg-white rounded-lg p-4 shadow-sm hover:shadow-md transition-all duration-300 border-l-4 border-cyan-400">
282
<.link navigate={~p"/post/#{post.slug}"} class="block">
283
<h3 class="text-xl font-bold text-gray-800 group-hover:text-cyan-600 transition-colors">
284
<%= "Reddit Links Feed" %>
285
</h3>
286
</.link>
287
</div>
288
<% end %>
289
</div>
290
</div>
291
</div>
292
</div>
293
294
<!-- Retro footer -->
295
<footer class="mt-16 text-center">
296
<div class="inline-block px-4 py-2 bg-gradient-to-r from-fuchsia-100 to-cyan-100 rounded-full text-sm text-gray-700">
297
<span class="font-mono">/* Crafted with ♥ and Elixir */</span>
298
</div>
299
</footer>
300
</div>
301
"""
302
end
303
304
# Add a debug function to help troubleshoot
305
def debug_presence(socket) do
306
IO.inspect(socket.assigns.reader_id, label: "Current reader_id")
307
IO.inspect(socket.assigns.visitor_cursors, label: "All visitor cursors")
308
socket
309
end
310
end
311