[Embed] Don't reuse DOM when changing embed (#3530)
* Don't reuse DOM when changing embed
* add skeleton loading state 💀
* autoselect text
---------
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
zio/stable
parent
1390b1dc9e
commit
9fb20915e8
|
@ -8,7 +8,7 @@ export function Container({
|
||||||
href,
|
href,
|
||||||
}: {
|
}: {
|
||||||
children: ComponentChildren
|
children: ComponentChildren
|
||||||
href: string
|
href?: string
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
const prevHeight = useRef(0)
|
const prevHeight = useRef(0)
|
||||||
|
@ -39,7 +39,7 @@ export function Container({
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
|
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (ref.current) {
|
if (ref.current && href) {
|
||||||
// forwardRef requires preact/compat - let's keep it simple
|
// forwardRef requires preact/compat - let's keep it simple
|
||||||
// to keep the bundle size down
|
// to keep the bundle size down
|
||||||
const anchor = ref.current.querySelector('a')
|
const anchor = ref.current.querySelector('a')
|
||||||
|
@ -48,7 +48,7 @@ export function Container({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Link href={href} />
|
{href && <Link href={href} />}
|
||||||
<div className="flex-1 px-4 pt-3 pb-2.5">{children}</div>
|
<div className="flex-1 px-4 pt-3 pb-2.5">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import '../index.css'
|
import '../index.css'
|
||||||
|
|
||||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
|
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
|
||||||
import {Fragment, h, render} from 'preact'
|
import {h, render} from 'preact'
|
||||||
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
||||||
|
|
||||||
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
|
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
|
||||||
|
@ -30,6 +30,7 @@ render(<LandingPage />, root)
|
||||||
function LandingPage() {
|
function LandingPage() {
|
||||||
const [uri, setUri] = useState('')
|
const [uri, setUri] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
|
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
@ -37,6 +38,8 @@ function LandingPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
setThread(null)
|
||||||
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
let atUri = DEFAULT_URI
|
let atUri = DEFAULT_URI
|
||||||
|
|
||||||
|
@ -98,6 +101,8 @@ function LandingPage() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
setError(err instanceof Error ? err.message : 'Invalid Bluesky URL')
|
setError(err instanceof Error ? err.message : 'Invalid Bluesky URL')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [uri])
|
}, [uri])
|
||||||
|
@ -122,19 +127,42 @@ function LandingPage() {
|
||||||
|
|
||||||
<img src={arrowBottom as string} className="w-6" />
|
<img src={arrowBottom as string} className="w-6" />
|
||||||
|
|
||||||
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
{loading ? (
|
||||||
{uri && !error && thread && <Snippet thread={thread} />}
|
<Skeleton />
|
||||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
) : (
|
||||||
{error && (
|
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
||||||
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
|
{!error && thread && uri && <Snippet thread={thread} />}
|
||||||
<p className="text-red-500 text-center">{error}</p>
|
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||||
</div>
|
{error && (
|
||||||
)}
|
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
|
||||||
</div>
|
<p className="text-red-500 text-center">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Skeleton() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className="flex-1 flex-col flex gap-2 pb-8">
|
||||||
|
<div className="flex gap-2.5 items-center">
|
||||||
|
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" />
|
||||||
|
<div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" />
|
||||||
|
<div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" />
|
||||||
|
<div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||||
const ref = useRef<HTMLInputElement>(null)
|
const ref = useRef<HTMLInputElement>(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
@ -195,6 +223,9 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||||
className="border rounded-lg py-3 w-full px-4"
|
className="border rounded-lg py-3 w-full px-4"
|
||||||
readOnly
|
readOnly
|
||||||
autoFocus
|
autoFocus
|
||||||
|
onFocus={() => {
|
||||||
|
ref.current?.select()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
|
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
|
||||||
|
|
Loading…
Reference in New Issue