From f82bc223bbd8a27aa46c49270cea6a88ae54bf26 Mon Sep 17 00:00:00 2001 From: qdust41 Date: Thu, 2 Apr 2026 03:34:56 -0400 Subject: [PATCH] added right click context menu and did a static deployment test --- assets/css/app.css | 44 ++++++ assets/js/index.tsx | 134 ++++++++++++++++++ ...vicon-91f37b602a111216f1eef3aa337ad763.ico | Bin 0 -> 152 bytes .../logo-06a11be1f2cdde2c851763d00bdd2e80.svg | 6 + ...go-06a11be1f2cdde2c851763d00bdd2e80.svg.gz | Bin 0 -> 1564 bytes priv/static/images/logo.svg.gz | Bin 0 -> 1564 bytes ...obots-9e2c81b0855bbff2baa8371bc4a78186.txt | 5 + ...ts-9e2c81b0855bbff2baa8371bc4a78186.txt.gz | Bin 0 -> 163 bytes priv/static/robots.txt.gz | Bin 0 -> 163 bytes 9 files changed, 189 insertions(+) create mode 100644 priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico create mode 100644 priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg create mode 100644 priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz create mode 100644 priv/static/images/logo.svg.gz create mode 100644 priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt create mode 100644 priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz create mode 100644 priv/static/robots.txt.gz diff --git a/assets/css/app.css b/assets/css/app.css index 6e4a931..a8e1812 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -734,3 +734,47 @@ html, body { border-radius: var(--mx-radius-sm); display: block; } + +/* ── Context Menu ── */ +.mx-context-menu { + position: fixed; + z-index: 300; + min-width: 160px; + background: var(--mx-surface2); + border: 1px solid var(--mx-border2); + border-radius: var(--mx-radius-sm); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.2); + overflow: hidden; + padding: 4px 0; + animation: mx-ctx-in 0.1s ease; +} + +@keyframes mx-ctx-in { + from { opacity: 0; transform: scale(0.96) translateY(-4px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.mx-context-menu-item { + display: flex; + align-items: center; + width: 100%; + padding: 0.45rem 0.875rem; + background: none; + border: none; + color: var(--mx-fg); + font-size: 0.875rem; + font-family: inherit; + cursor: pointer; + text-align: left; + transition: background 0.1s; +} + +.mx-context-menu-item:hover { + background: color-mix(in oklch, var(--mx-accent) 14%, transparent); +} + +.mx-context-menu-separator { + height: 1px; + background: var(--mx-border); + margin: 4px 0; +} diff --git a/assets/js/index.tsx b/assets/js/index.tsx index f7ed4e3..312405b 100644 --- a/assets/js/index.tsx +++ b/assets/js/index.tsx @@ -55,6 +55,75 @@ function getAssetHost(): string { return appEl?.dataset.assetHost ?? "http://localhost:9000"; } +// ── Context menu ────────────────────────────────────────────────────────────── + +type ContextMenuItem = + | { type: "item"; label: string; onClick: () => void } + | { type: "separator" }; + +function ContextMenu({ + x, + y, + items, + onClose, +}: { + x: number; + y: number; + items: ContextMenuItem[]; + onClose: () => void; +}) { + const ref = useRef(null); + + useEffect(() => { + function handleMouseDown(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) onClose(); + } + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + document.addEventListener("mousedown", handleMouseDown); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("mousedown", handleMouseDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [onClose]); + + const itemCount = items.filter((i) => i.type === "item").length; + const sepCount = items.filter((i) => i.type === "separator").length; + const menuH = itemCount * 34 + sepCount * 9 + 8; + const menuW = 180; + const left = Math.min(x, window.innerWidth - menuW - 8); + const top = Math.min(y, window.innerHeight - menuH - 8); + + return createPortal( +
e.preventDefault()} + > + {items.map((item, i) => + item.type === "separator" ? ( +
+ ) : ( + + ) + )} +
, + document.body + ); +} + // ── Components ───────────────────────────────────────────────────────────────── function Spinner() { @@ -319,8 +388,43 @@ function TweetCard({ tweet }: { tweet: Tweet }) { const [editText, setEditText] = useState(tweet.content); const [error, setError] = useState(null); const [confirmDelete, setConfirmDelete] = useState(false); + const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null); const qc = useQueryClient(); + const tweetUrl = `${window.location.origin}/feed/${tweet.id}`; + + const ctxItems: ContextMenuItem[] = canModify + ? [ + { + type: "item", + label: "Edit", + onClick: () => { + setEditText(tweet.content); + setEditing(true); + setConfirmDelete(false); + }, + }, + { type: "separator" }, + { + type: "item", + label: "Share", + onClick: () => navigator.clipboard.writeText(tweetUrl), + }, + ] + : [ + { + type: "item", + label: "View", + onClick: () => { window.location.href = tweetUrl; }, + }, + { type: "separator" }, + { + type: "item", + label: "Share", + onClick: () => navigator.clipboard.writeText(tweetUrl), + }, + ]; + const deleteMutation = useMutation({ mutationFn: async () => { const res = await destroyTweet({ @@ -379,6 +483,7 @@ function TweetCard({ tweet }: { tweet: Tweet }) { className="mx-tweet" style={{ cursor: "pointer" }} onClick={() => { window.location.href = `/feed/${tweet.id}`; }} + onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ x: e.clientX, y: e.clientY }); }} >
M @@ -492,6 +597,14 @@ function TweetCard({ tweet }: { tweet: Tweet }) { {error && !editing &&

{error}

}
+ {ctxMenu && ( + setCtxMenu(null)} + /> + )} ); } @@ -775,11 +888,24 @@ function RefreshButton() { } function UserCard({ user }: { user: User }) { + const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null); + + const userUrl = `${window.location.origin}/users/${user.id}`; + + const ctxItems: ContextMenuItem[] = [ + { + type: "item", + label: "Share", + onClick: () => navigator.clipboard.writeText(userUrl), + }, + ]; + return (
{ window.location.href = `/users/${user.id}`; }} + onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ x: e.clientX, y: e.clientY }); }} >
M @@ -789,6 +915,14 @@ function UserCard({ user }: { user: User }) { {user.email}
+ {ctxMenu && ( + setCtxMenu(null)} + /> + )} ); } diff --git a/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico b/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico new file mode 100644 index 0000000000000000000000000000000000000000..7f372bfc21cdd8cb47585339d5fa4d9dd424402f GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=@t!V@Ar*{oFEH`~d50E!_s``s q?{G*w(7?#d#v@^nKnY_HKaYb01EZMZjMqTJ89ZJ6T-G@yGywoKK_h|y literal 0 HcmV?d00001 diff --git a/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg new file mode 100644 index 0000000..9f26bab --- /dev/null +++ b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg @@ -0,0 +1,6 @@ + diff --git a/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz new file mode 100644 index 0000000000000000000000000000000000000000..566997d77dd27ffac89932ede081275f30edf7b9 GIT binary patch literal 1564 zcmV+%2IKi3iwFP!0000015H-TZX7uhy!$H%?U~_xC%Lc;?9O6t`vZLw+8*10WDjJG zHJ`r-whntxbybN)W=2Lt=7*oZzKp-09>4#*zx(>~^5;L_zkhvw#jE4T^OyG~BJY3x z`f@jZef#|S?;n5P-^q~CI%3_8KfXOa-rrsSdVc=={pGhGk3XL8#>4Zshxzr}r%#{1 z-`~AF|MmIq=t);yZq9YcA}HX~<>M5UZx5L}$QtPe(`1gc8G2ooL}| zSMFGZ_?E!gy|80J-HAyQvW6jwk6MXbj@aiZcoj`9bRy07hDE$`6j}+IcMcU;u?n?j zVuekJ)!bOhidsx{xUAW5^{Mb;3b`5PS)p~nU{!nNG}K;`b!K9;4l6KjbepMN1fvXvWD`6y5Tp7Q7~w$#!4ec zH&ap1(a<(z=HWNbKfRQtXOs@ z%Wa*7I&srXP8b*RfRyB2oZZ_<#fe_1?US%L&(;-ZAr4Qu^}$_H6TY|=E4rOWt|J_& z8_NjGH0(om6H;v7C|?vDcM@ebwSwr7kJ_8OJEMdtHSm$=wXH&)h#_N z5SoqA0MTzUYKAoe(K^)7$D7*!Rg_kYw;x)s4(}0y)jVReYNv=r&!i!BvZVX+G&eM+ z3q115;u7yBD8ftC#8GF_7(3{F&aL*M^~qkNP>Y<`?IYlMV|R~*ZpKZPIBYu(2a`oG zl3LtZTjoW&IegKQ96Je=`tfw{;}AX@AxbMGx~_)@B#5&PW!Nzj;XFP&rwU>v6*e^? z$2l_n@?=d;4o!sR6)g*r%r3N`#*ozlx=1G0KZG`zd`x9DX(Vb$&~SuBrkNH=w1t5x z3$vSK>eEq{ajhQ>a`6^oRT7Z!qj<@HYD22duag3FvJEkkSEBib6))Da37a-md;FAD zL}TrVMJ?8epJX!H2?eBRtw|FnMvF4hB>gJ5R{HWZ7pXA=)oeRU?N~*^PF+Jbn{Ck2 zcMydxZl>gV-AeDY^lrX(N~VMK;kgu@o>SSUw5bDiXx5dM1!;B#XdcXALKtYkXfd_} z!qD)2sJ95QQWV<|9rVUj7HkQNXhx=mpWc4%TL_xm8j2Sz4dM$aezxnZ<8I~wF>bnH zNe;0X)c%{CJ0|y}MaZt%hhB=+c6lQ4B=+1y{i=vgNa`D4(4bA-c(zeqL_j$nyJd#Ca@bt~)gWP!FG+Gtl&g_R{7oVj*#?C|O{nXQTRGg0{> z*XeY@A&E$+klbzSL4Bwn_mab?GYv^y6{M1Fnu{JAPh&{CphuL8ouIA$|JKjAV?_7z zG>urVp#`byX*%TV={b9GDzYFfn^4Po*r@7pyWZT?u=_N-eFgi+QCOa~!I&tLV|B@% zavOb`j+V|T!<(+n?n*bDC^p;B)3$ug(@i?qC)je<$&f+)}N%LCM! ze{Ybn+Y#66@eQG?`8w}&aR<9?%METTtj>TU8kf5>bl(Q`fBgQR_+5mr-hX`efph)G OcmD&r06^vd3;+OY0t9XV literal 0 HcmV?d00001 diff --git a/priv/static/images/logo.svg.gz b/priv/static/images/logo.svg.gz new file mode 100644 index 0000000000000000000000000000000000000000..566997d77dd27ffac89932ede081275f30edf7b9 GIT binary patch literal 1564 zcmV+%2IKi3iwFP!0000015H-TZX7uhy!$H%?U~_xC%Lc;?9O6t`vZLw+8*10WDjJG zHJ`r-whntxbybN)W=2Lt=7*oZzKp-09>4#*zx(>~^5;L_zkhvw#jE4T^OyG~BJY3x z`f@jZef#|S?;n5P-^q~CI%3_8KfXOa-rrsSdVc=={pGhGk3XL8#>4Zshxzr}r%#{1 z-`~AF|MmIq=t);yZq9YcA}HX~<>M5UZx5L}$QtPe(`1gc8G2ooL}| zSMFGZ_?E!gy|80J-HAyQvW6jwk6MXbj@aiZcoj`9bRy07hDE$`6j}+IcMcU;u?n?j zVuekJ)!bOhidsx{xUAW5^{Mb;3b`5PS)p~nU{!nNG}K;`b!K9;4l6KjbepMN1fvXvWD`6y5Tp7Q7~w$#!4ec zH&ap1(a<(z=HWNbKfRQtXOs@ z%Wa*7I&srXP8b*RfRyB2oZZ_<#fe_1?US%L&(;-ZAr4Qu^}$_H6TY|=E4rOWt|J_& z8_NjGH0(om6H;v7C|?vDcM@ebwSwr7kJ_8OJEMdtHSm$=wXH&)h#_N z5SoqA0MTzUYKAoe(K^)7$D7*!Rg_kYw;x)s4(}0y)jVReYNv=r&!i!BvZVX+G&eM+ z3q115;u7yBD8ftC#8GF_7(3{F&aL*M^~qkNP>Y<`?IYlMV|R~*ZpKZPIBYu(2a`oG zl3LtZTjoW&IegKQ96Je=`tfw{;}AX@AxbMGx~_)@B#5&PW!Nzj;XFP&rwU>v6*e^? z$2l_n@?=d;4o!sR6)g*r%r3N`#*ozlx=1G0KZG`zd`x9DX(Vb$&~SuBrkNH=w1t5x z3$vSK>eEq{ajhQ>a`6^oRT7Z!qj<@HYD22duag3FvJEkkSEBib6))Da37a-md;FAD zL}TrVMJ?8epJX!H2?eBRtw|FnMvF4hB>gJ5R{HWZ7pXA=)oeRU?N~*^PF+Jbn{Ck2 zcMydxZl>gV-AeDY^lrX(N~VMK;kgu@o>SSUw5bDiXx5dM1!;B#XdcXALKtYkXfd_} z!qD)2sJ95QQWV<|9rVUj7HkQNXhx=mpWc4%TL_xm8j2Sz4dM$aezxnZ<8I~wF>bnH zNe;0X)c%{CJ0|y}MaZt%hhB=+c6lQ4B=+1y{i=vgNa`D4(4bA-c(zeqL_j$nyJd#Ca@bt~)gWP!FG+Gtl&g_R{7oVj*#?C|O{nXQTRGg0{> z*XeY@A&E$+klbzSL4Bwn_mab?GYv^y6{M1Fnu{JAPh&{CphuL8ouIA$|JKjAV?_7z zG>urVp#`byX*%TV={b9GDzYFfn^4Po*r@7pyWZT?u=_N-eFgi+QCOa~!I&tLV|B@% zavOb`j+V|T!<(+n?n*bDC^p;B)3$ug(@i?qC)je<$&f+)}N%LCM! ze{Ybn+Y#66@eQG?`8w}&aR<9?%METTtj>TU8kf5>bl(Q`fBgQR_+5mr-hX`efph)G OcmD&r06^vd3;+OY0t9XV literal 0 HcmV?d00001 diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt new file mode 100644 index 0000000..26e06b5 --- /dev/null +++ b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..18b919cd30f409b2badc4fed867cc2a929a09afa GIT binary patch literal 163 zcmV;U09^kciwFP!0000014WE64#F@H1be??C3i?9y=#5|Jiw3(vEPvh8oADTUk&Ygp`*Z5xx*NWL1CVOtHC^L&GOtDx84Vw$N2!6@HotR6N z5JSYx@W_f>EBsw1X^y$sdibI&P2h!sO`o=Y&kv%q6YPdsR6K=vXz R*J?ds3?K4Ua0tr)008+=M^OL( literal 0 HcmV?d00001 diff --git a/priv/static/robots.txt.gz b/priv/static/robots.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..18b919cd30f409b2badc4fed867cc2a929a09afa GIT binary patch literal 163 zcmV;U09^kciwFP!0000014WE64#F@H1be??C3i?9y=#5|Jiw3(vEPvh8oADTUk&Ygp`*Z5xx*NWL1CVOtHC^L&GOtDx84Vw$N2!6@HotR6N z5JSYx@W_f>EBsw1X^y$sdibI&P2h!sO`o=Y&kv%q6YPdsR6K=vXz R*J?ds3?K4Ua0tr)008+=M^OL( literal 0 HcmV?d00001