-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement link display drawer for mobile and UX
- Loading branch information
Showing
11 changed files
with
396 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Markdown } from '@/components/Markdown' | ||
import { Skeleton } from '@/components/ui/skeleton' | ||
import { cn } from '@/lib/utils' | ||
|
||
interface LinkDisplayProps { | ||
summary: string | ||
cleaned: string | ||
} | ||
|
||
export function LinkDisplay({ summary, cleaned }: LinkDisplayProps) { | ||
return ( | ||
<section className="flex flex-col relative lg:grid lg:grid-cols-12 "> | ||
<Markdown>{cleaned}</Markdown> | ||
|
||
<div className="order-first flex flex-col gap-4 relative lg:order-last lg:col-span-4 lg:px-2 lg:pt-6"> | ||
<div className="lg:pr-4"> | ||
{summary ? <Summary summary={summary} /> : <Skeleton />} | ||
</div> | ||
</div> | ||
</section> | ||
) | ||
} | ||
|
||
const Summary = ({ summary }: { summary: string }) => { | ||
const lines = summary.split('\n\n') | ||
return ( | ||
<div className="rounded-sm border border-muted bg-muted/80 text-foreground/80 p-2 text-sm"> | ||
<h4 className="text-sm font-semibold mb-1">Summary:</h4> | ||
{lines.map((line: string, index: number) => ( | ||
<p | ||
key={line} | ||
className={cn( | ||
'text-sm leading-5 subpixel-antialiased', | ||
index < lines.length - 1 ? 'mb-2' : '', | ||
)} | ||
> | ||
{line} | ||
</p> | ||
))} | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './LinkDisplay' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
'use client' | ||
|
||
import { | ||
DrawerClose, | ||
DrawerDescription, | ||
DrawerFooter, | ||
DrawerHeader, | ||
DrawerTitle, | ||
} from '@/components/ui/drawer' | ||
import { Button } from '@/components/ui/button' | ||
import { client } from '@/lib/api/client' | ||
import { useEffect, useState } from 'react' | ||
import { LinkDisplay } from '@/components/LinkDisplay/LinkDisplay' | ||
import { formatRelative } from 'date-fns' | ||
import Link from 'next/link' | ||
import { Expand, X } from 'lucide-react' | ||
|
||
interface LinkDrawerProps { | ||
id: string | null | ||
close: () => void | ||
} | ||
|
||
type TLink = { | ||
id: string | ||
summary: string | ||
cleaned: string | ||
title: string | ||
url: string | ||
updatedAt: string | ||
} | ||
|
||
export function LinkDrawer({ id, close }: LinkDrawerProps) { | ||
if (!id) return null | ||
|
||
const [link, setLink] = useState<TLink | null>(null) | ||
useEffect(() => { | ||
async function run() { | ||
if (!id) return | ||
|
||
const res = await client.api.v1.links[':id'].$get({ param: { id: id } }) | ||
const { link } = (await res.json()) as { | ||
link: TLink | ||
} | ||
|
||
setLink(link) | ||
} | ||
|
||
run().catch(console.error) | ||
}, [id]) | ||
|
||
return link ? ( | ||
<> | ||
<DrawerHeader> | ||
<DrawerTitle> | ||
<div className="flex lg:flex-row"> | ||
<h3 className="text-left">{link?.title}</h3> | ||
<div className="ml-auto flex flex-row gap-2"> | ||
<Button size="icon" variant="outline" asChild className="size-7"> | ||
<Link href={`/bookmarks/${link.id}`}> | ||
<Expand className="h-4 w-4" /> | ||
</Link> | ||
</Button> | ||
<DrawerClose | ||
onClick={() => { | ||
close() | ||
}} | ||
asChild | ||
> | ||
<Button size="icon" variant="outline" className="size-7"> | ||
<X className="h-4 w-4" /> | ||
</Button> | ||
</DrawerClose> | ||
</div> | ||
</div> | ||
</DrawerTitle> | ||
<DrawerDescription className="text-left"> | ||
<a | ||
href={link?.url} | ||
target="_blank" | ||
rel="noreferrer" | ||
className="text-muted-foreground text-xs" | ||
> | ||
{link?.url} | ||
</a> | ||
{link?.updatedAt && ( | ||
<p className="text-xs mt-1 text-foreground/50"> | ||
Last updated{' '} | ||
{formatRelative(new Date(link?.updatedAt), new Date())}. | ||
</p> | ||
)} | ||
</DrawerDescription> | ||
</DrawerHeader> | ||
<div className="py-4 px-10 max-h-96 overflow-y-scroll"> | ||
<LinkDisplay summary={link.summary} cleaned={link.cleaned} /> | ||
</div> | ||
<DrawerFooter> | ||
<DrawerClose | ||
onClick={() => { | ||
close() | ||
}} | ||
> | ||
<Button variant="outline">Close</Button> | ||
</DrawerClose> | ||
</DrawerFooter> | ||
</> | ||
) : null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
'use client' | ||
|
||
import { LinkDrawer } from '@/components/LinkList/LinkDrawer' | ||
import { StatusIcon, type LinkStatus } from '@/components/StatusIcon' | ||
import { AspectRatio } from '@/components/ui/aspect-ratio' | ||
import { DrawerContent, DrawerTrigger } from '@/components/ui/drawer' | ||
import { Drawer } from '@/components/ui/drawer' | ||
import { cn } from '@/lib/utils' | ||
import { formatRelative } from 'date-fns' | ||
import Image from 'next/image' | ||
import { useRouter, useSearchParams } from 'next/navigation' | ||
import { useState } from 'react' | ||
|
||
interface LinkItemsProps { | ||
links: { | ||
id: string | ||
href: string | ||
prefetch?: boolean | ||
title: string | ||
url: string | ||
imageUrl?: string | ||
summary: string | ||
status: LinkStatus | ||
tags?: { key: string; label: string }[] | ||
createdAt: string | ||
}[] | ||
} | ||
|
||
export function LinkItems({ links }: LinkItemsProps) { | ||
const [selectedLink, setLink] = useState<string | null>(null) | ||
const searchParams = useSearchParams() | ||
const router = useRouter() | ||
|
||
const page = searchParams.get('page') | ||
const pageSize = searchParams.get('pageSize') | ||
const sort = searchParams.get('sort') | ||
const direction = searchParams.get('direction') | ||
const search = searchParams.get('search') | ||
|
||
return ( | ||
<Drawer> | ||
{links.map((link) => { | ||
return ( | ||
<DrawerTrigger | ||
key={link.id} | ||
className={cn( | ||
'grid grid-cols-12 w-full text-left gap-5 p-2 hover:shadow-md transition-shadow rounded-lg', | ||
'dark:hover:bg-primary/2- dark:transition-colors', | ||
'ease-in-out duration-75 group', | ||
)} | ||
onClick={() => { | ||
setLink(link.id) | ||
router.replace( | ||
`/bookmarks?${new URLSearchParams({ | ||
page: page || '1', | ||
pageSize: pageSize || '10', | ||
sort: sort || 'createdAt', | ||
direction: direction || 'desc', | ||
search: search || '', | ||
selected: link?.id || '', | ||
}).toString()}`, | ||
) | ||
}} | ||
> | ||
<section className="col-span-3 flex rounded-md overflow-hidden shadow-md"> | ||
<AspectRatio ratio={16 / 9} className="w-full"> | ||
<Image | ||
src={ | ||
link.imageUrl ?? | ||
'https://placehold.co/250x140/png?text=No+Image' | ||
} | ||
fill | ||
className="object-cover" | ||
alt={link.title ?? link.url} | ||
/> | ||
</AspectRatio> | ||
</section> | ||
<section className="col-span-8"> | ||
<div className="flex flex-row gap-1 items-baseline"> | ||
<h3 className="font-semibold text-lg truncate"> | ||
{link.title ?? link.url} | ||
</h3> | ||
<StatusIcon status={link.status} /> | ||
</div> | ||
<div className="text-sm text-muted-foreground"> | ||
{link.title ? link.url : null} | ||
</div> | ||
<div className="text-sm text-foreground"> | ||
Added {formatRelative(link.createdAt, new Date())} | ||
</div> | ||
</section> | ||
</DrawerTrigger> | ||
) | ||
})} | ||
|
||
<DrawerContent> | ||
<LinkDrawer | ||
id={selectedLink} | ||
close={() => { | ||
router.replace( | ||
`/bookmarks?${new URLSearchParams({ | ||
page: page || '1', | ||
pageSize: pageSize || '10', | ||
sort: sort || 'createdAt', | ||
direction: direction || 'desc', | ||
search: search || '', | ||
}).toString()}`, | ||
) | ||
}} | ||
/> | ||
</DrawerContent> | ||
</Drawer> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.