258 lines
8.8 kB
1
defmodule BlogWeb.SwearStreamLive do
2
use BlogWeb, :live_view
3
require Logger
4
5
@common_swears [
6
"FUCK", "SHIT", "DAMN", "BITCH", "CUNT", "ASSHOLE", "BASTARD",
7
"PISS", "COCK", "DICK", "ASS", "BULLSHIT", "MOTHERFUCKER",
8
"HELL", "CRAP", "WHORE", "SLUT", "DOUCHE", "JERK", "IDIOT"
9
]
10
11
def mount(_params, _session, socket) do
12
if connected?(socket) do
13
# Subscribe to the general skeet feed
14
Phoenix.PubSub.subscribe(Blog.PubSub, "skeet_feed")
15
end
16
17
{:ok,
18
assign(socket,
19
page_title: "Swear Stream",
20
selected_swear: nil,
21
search_term: "",
22
filtered_swears: [],
23
common_swears: @common_swears,
24
skeets: [],
25
max_skeets: 50,
26
show_suggestions: false
27
)}
28
end
29
30
def handle_event("search", %{"search" => search_term}, socket) do
31
filtered_swears =
32
if String.length(search_term) > 0 do
33
@common_swears
34
|> Enum.filter(fn swear ->
35
String.contains?(String.downcase(swear), String.downcase(search_term))
36
end)
37
|> Enum.sort()
38
else
39
[]
40
end
41
42
{:noreply,
43
assign(socket,
44
search_term: search_term,
45
filtered_swears: filtered_swears,
46
show_suggestions: String.length(search_term) > 0
47
)}
48
end
49
50
def handle_event("select_swear", %{"swear" => swear}, socket) do
51
# Check if the selected swear is in our options or a custom entry
52
if swear in @common_swears or String.length(swear) > 0 do
53
{:noreply,
54
assign(socket,
55
selected_swear: swear,
56
search_term: swear,
57
filtered_swears: [],
58
show_suggestions: false,
59
skeets: [] # Reset skeets when changing filter
60
)}
61
else
62
{:noreply, socket}
63
end
64
end
65
66
def handle_event("use_custom", _, socket) do
67
if String.length(socket.assigns.search_term) > 0 do
68
{:noreply,
69
assign(socket,
70
selected_swear: String.upcase(socket.assigns.search_term),
71
filtered_swears: [],
72
show_suggestions: false,
73
skeets: [] # Reset skeets when changing filter
74
)}
75
else
76
{:noreply, socket}
77
end
78
end
79
80
def handle_info({:new_post, skeet}, socket) do
81
selected_swear = socket.assigns.selected_swear
82
83
# If no swear is selected or the skeet doesn't contain the selected swear, do nothing
84
if is_nil(selected_swear) or not contains_swear?(skeet, selected_swear) do
85
{:noreply, socket}
86
else
87
# Add the new skeet to our collection
88
updated_skeets = [%{text: skeet, timestamp: DateTime.utc_now()} | socket.assigns.skeets]
89
|> Enum.take(socket.assigns.max_skeets) # Keep only the most recent skeets
90
91
{:noreply, assign(socket, skeets: updated_skeets)}
92
end
93
end
94
95
defp contains_swear?(text, swear) do
96
String.contains?(String.downcase(text), String.downcase(swear))
97
end
98
99
# Helper function to highlight the swear word in the text
100
defp highlight_swear(text, swear) do
101
regex = ~r/#{Regex.escape(swear)}/i
102
String.replace(text, regex, fn match ->
103
"<span class=\"text-red-500 font-bold\">#{match}</span>"
104
end)
105
end
106
107
def render(assigns) do
108
~H"""
109
<div class="min-h-screen bg-black text-white p-4 font-mono">
110
<div class="max-w-4xl mx-auto">
111
<div class="mb-8 border-b border-gray-800 pb-4">
112
<h1 class="text-4xl font-bold mb-2 glitch" data-text="SWEAR STREAM">SWEAR STREAM</h1>
113
<p class="text-gray-400">Search for a swear word to see matching skeets in real-time</p>
114
</div>
115
116
<div class="mb-8">
117
<form phx-change="search" class="relative">
118
<div class="flex items-center gap-2">
119
<div class="relative flex-1">
120
<input
121
type="text"
122
name="search"
123
value={@search_term}
124
placeholder="Type a swear word..."
125
class="w-full bg-gray-900 border border-gray-700 rounded px-4 py-2 text-white text-xl focus:outline-none focus:border-blue-500"
126
autocomplete="off"
127
/>
128
<%= if @show_suggestions and not Enum.empty?(@filtered_swears) do %>
129
<div class="absolute z-10 w-full mt-1 bg-gray-900 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto">
130
<%= for swear <- @filtered_swears do %>
131
<button
132
type="button"
133
phx-click="select_swear"
134
phx-value-swear={swear}
135
class="w-full text-left px-4 py-2 hover:bg-gray-800 focus:bg-gray-800 focus:outline-none"
136
>
137
<%= swear %>
138
</button>
139
<% end %>
140
</div>
141
<% end %>
142
</div>
143
<%= if String.length(@search_term) > 0 and is_nil(@selected_swear) do %>
144
<button
145
type="button"
146
phx-click="use_custom"
147
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
148
>
149
Search
150
</button>
151
<% end %>
152
</div>
153
</form>
154
155
<%= if @selected_swear do %>
156
<div class="mt-4 flex items-center">
157
<div class="bg-red-900/50 text-red-300 px-3 py-1 rounded-lg flex items-center">
158
<span>Filtering by: <span class="font-bold"><%= @selected_swear %></span></span>
159
<button
160
phx-click="select_swear"
161
phx-value-swear=""
162
class="ml-2 text-red-300 hover:text-white"
163
>
164
<.icon name="hero-x-mark" class="h-4 w-4" />
165
</button>
166
</div>
167
</div>
168
<% end %>
169
</div>
170
171
<%= if is_nil(@selected_swear) do %>
172
<div class="flex items-center justify-center h-64 border-2 border-dashed border-gray-700 rounded-lg">
173
<p class="text-2xl text-gray-500">Search for a swear word to start seeing skeets</p>
174
</div>
175
<% else %>
176
<div class="space-y-4">
177
<%= if Enum.empty?(@skeets) do %>
178
<div class="flex items-center justify-center h-64 border-2 border-dashed border-gray-700 rounded-lg">
179
<p class="text-xl text-gray-500">Waiting for skeets containing "<%= @selected_swear %>"...</p>
180
</div>
181
<% else %>
182
<%= for {skeet, index} <- Enum.with_index(@skeets) do %>
183
<div
184
id={"skeet-#{index}"}
185
class="border border-gray-800 p-4 rounded-lg hover:border-gray-600 transition-all"
186
phx-mounted={JS.transition("fade-in-slide", time: 500)}
187
>
188
<div class="flex justify-between items-start mb-2">
189
<div class="text-gray-400 text-sm">
190
<%= Calendar.strftime(skeet.timestamp, "%Y-%m-%d %H:%M:%S") %>
191
</div>
192
</div>
193
<p class="whitespace-pre-wrap"><%= raw(highlight_swear(skeet.text, @selected_swear)) %></p>
194
</div>
195
<% end %>
196
<% end %>
197
</div>
198
<% end %>
199
</div>
200
</div>
201
202
<style>
203
.glitch {
204
position: relative;
205
text-shadow: 0.05em 0 0 #00fffc, -0.03em -0.04em 0 #fc00ff,
206
0.025em 0.04em 0 #fffc00;
207
animation: glitch 725ms infinite;
208
}
209
210
@keyframes glitch {
211
0% {
212
text-shadow: 0.05em 0 0 #00fffc, -0.03em -0.04em 0 #fc00ff,
213
0.025em 0.04em 0 #fffc00;
214
}
215
15% {
216
text-shadow: 0.05em 0 0 #00fffc, -0.03em -0.04em 0 #fc00ff,
217
0.025em 0.04em 0 #fffc00;
218
}
219
16% {
220
text-shadow: -0.05em -0.025em 0 #00fffc, 0.025em 0.035em 0 #fc00ff,
221
-0.05em -0.05em 0 #fffc00;
222
}
223
49% {
224
text-shadow: -0.05em -0.025em 0 #00fffc, 0.025em 0.035em 0 #fc00ff,
225
-0.05em -0.05em 0 #fffc00;
226
}
227
50% {
228
text-shadow: 0.05em 0.035em 0 #00fffc, 0.03em 0 0 #fc00ff,
229
0 -0.04em 0 #fffc00;
230
}
231
99% {
232
text-shadow: 0.05em 0.035em 0 #00fffc, 0.03em 0 0 #fc00ff,
233
0 -0.04em 0 #fffc00;
234
}
235
100% {
236
text-shadow: -0.05em 0 0 #00fffc, -0.025em -0.04em 0 #fc00ff,
237
-0.04em -0.025em 0 #fffc00;
238
}
239
}
240
241
.fade-in-slide {
242
animation: fadeInSlide 0.5s ease-out;
243
}
244
245
@keyframes fadeInSlide {
246
from {
247
opacity: 0;
248
transform: translateY(20px);
249
}
250
to {
251
opacity: 1;
252
transform: translateY(0);
253
}
254
}
255
</style>
256
"""
257
end
258
end
259