Enhance url display method(grids)

This commit is contained in:
oiov
2025-05-28 18:20:22 +08:00
parent 026dfb2ffe
commit c619931386
7 changed files with 221 additions and 72 deletions
+174 -24
View File
@@ -12,6 +12,7 @@ import { ShortUrlFormData } from "@/lib/dto/short-urls";
import {
cn,
expirationTime,
extractHostname,
fetcher,
removeUrlSuffix,
timeAgo,
@@ -39,6 +40,7 @@ import {
import { FormType } from "@/components/forms/record-form";
import { UrlForm } from "@/components/forms/url-form";
import ApiReference from "@/components/shared/api-reference";
import BlurImage from "@/components/shared/blur-image";
import { CopyButton } from "@/components/shared/copy-button";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { Icons } from "@/components/shared/icons";
@@ -141,8 +143,18 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
}
};
const renderList = () => (
<div className="rounded-lg border p-4">
const rendeEmpty = () => (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon name="link" />
<EmptyPlaceholder.Title>No urls</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
You don&apos;t have any url yet. Start creating url.
</EmptyPlaceholder.Description>
</EmptyPlaceholder>
);
const rendeHeader = () => (
<>
{pathname === "/dashboard" && (
<h2 className="mb-4 text-lg font-bold">Short URLs</h2>
)}
@@ -221,6 +233,12 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</div>
)}
</div>
</>
);
const rendeList = () => (
<div className="rounded-lg border p-4">
{rendeHeader()}
<Table>
<TableHeader className="bg-gray-100/50 dark:bg-primary-foreground">
@@ -377,13 +395,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</div>
))
) : (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon name="link" />
<EmptyPlaceholder.Title>No urls</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
You don&apos;t have any url yet. Start creating url.
</EmptyPlaceholder.Description>
</EmptyPlaceholder>
rendeEmpty()
)}
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
@@ -399,14 +411,157 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</div>
);
const rendeGrid = () => (
<>
<div className="rounded-lg border p-4">
{rendeHeader()}
<section className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{isLoading ? (
<>
{[1, 2, 3, 4, 5, 6].map((v) => (
<Skeleton key={v} className="h-24 w-full" />
))}
</>
) : data && data.list && data.list.length ? (
data.list.map((short) => (
<div className="h-24 rounded-md border p-3" key={short.id}>
<div className="flex h-full items-start justify-start gap-3">
<BlurImage
src={`https://unavatar.io/${extractHostname(short.target)}?fallback=https://wr.do/logo.png`}
alt="logo"
width={30}
height={30}
/>
<div className="flex size-full flex-col justify-between truncate">
<div className="flex w-full items-center justify-between gap-3">
<div className="flex items-center">
<Link
className="overflow-hidden text-ellipsis whitespace-normal text-sm font-semibold text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-400"
href={`https://${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
target="_blank"
prefetch={false}
title={short.url}
>
{short.prefix}/s/{short.url}
</Link>
<CopyButton
value={`${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
className={cn(
"size-[25px]",
"duration-250 transition-all group-hover:opacity-100",
)}
/>
{short.password && (
<Icons.lock className="size-3 text-neutral-600 dark:text-neutral-400" />
)}
</div>
<div className="ml-auto flex items-center">
<Button
className="size-[25px] p-1.5"
size="sm"
variant="ghost"
onClick={() => {
setCurrentEditUrl(short);
setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm);
}}
>
<PenLine className="size-4" />
</Button>
<Button
className="size-[25px] p-1.5"
size="sm"
variant="ghost"
onClick={() => {
setSelectedUrl(short);
setShowQrcode(!isShowQrcode);
}}
>
<Icons.qrcode className="size-4" />
</Button>
<Button
className="size-[25px] p-1.5"
size="sm"
variant="ghost"
onClick={() => {
setSelectedUrl(short);
if (isShowStats && selectedUrl?.id !== short.id) {
} else {
setShowStats(!isShowStats);
}
}}
>
<Icons.lineChart className="size-4" />
</Button>
</div>
</div>
<div className="flex items-center gap-1 overflow-hidden truncate text-sm text-muted-foreground">
<Icons.forwardArrow className="size-4 shrink-0 text-gray-400" />
<LinkInfoPreviewer
apiKey={user.apiKey ?? ""}
url={short.target}
formatUrl={removeUrlSuffix(short.target)}
/>
</div>
<div className="mt-auto flex items-center justify-end gap-1.5 text-xs text-muted-foreground">
Expiration:{" "}
{expirationTime(short.expiration, short.updatedAt)}
<span>|</span>
Updated at {timeAgo(short.updatedAt as Date)}
</div>
</div>
</div>
</div>
))
) : (
rendeEmpty()
)}
</section>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
pageSize={pageSize}
setPageSize={setPageSize}
/>
)}
</div>
</>
);
const rendLogs = () => (
<>
{action.indexOf("admin") > -1 ? <LiveLog admin={true} /> : <LiveLog />}
<ApiReference
badge="POST /api/v1/short"
target="creating short urls"
link="/docs/short-urls#api-reference"
/>
</>
);
return (
<>
<Tabs defaultValue="Links">
<Tabs defaultValue="List">
{pathname !== "/dashboard" && (
<div className="mb-4 flex items-center justify-between gap-2">
<TabsList>
<TabsTrigger value="Links">Links</TabsTrigger>
<TabsTrigger value="Realtime">Realtime</TabsTrigger>
<TabsTrigger className="flex items-center gap-1" value="List">
<Icons.list className="size-4" />
List
</TabsTrigger>
<TabsTrigger className="flex items-center gap-1" value="Grid">
<Icons.layoutGrid className="size-4" />
Grid
</TabsTrigger>
<TabsTrigger className="flex items-center gap-1" value="Realtime">
<Icons.globe className="size-4" />
Realtime
</TabsTrigger>
</TabsList>
<div className="ml-auto flex items-center justify-end gap-3">
<Button
@@ -437,18 +592,13 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</div>
)}
<TabsContent className="space-y-3" value="Links">
{renderList()}
{action.indexOf("admin") > -1 ? (
<LiveLog admin={true} />
) : (
<LiveLog />
)}
<ApiReference
badge="POST /api/v1/short"
target="creating short urls"
link="/docs/short-urls#api-reference"
/>
<TabsContent className="space-y-3" value="List">
{rendeList()}
{rendLogs()}
</TabsContent>
<TabsContent className="space-y-3" value="Grid">
{rendeGrid()}
{rendLogs()}
</TabsContent>
<TabsContent value="Realtime">
{action.indexOf("admin") > -1 ? <Globe isAdmin={true} /> : <Globe />}
+1 -22
View File
@@ -111,28 +111,7 @@ export default function EmailManagerInnovate() {
</div>
</div>
<div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg
viewBox="0 0 18 18"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 shrink-0 dark:text-gray-400"
>
<path
d="M15.25,9.75H4.75c-1.105,0-2-.895-2-2V3.75"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
></path>
<polyline
fill="none"
points="11 5.5 15.25 9.75 11 14"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
></polyline>
</svg>
<Icons.forwardArrow className="h-4 w-4 shrink-0 text-gray-400" />
{viewMode === "inbox" ? "app@wr.do" : "example@gmail.com"}
</div>
</div>
+1 -24
View File
@@ -78,30 +78,7 @@ export default function UrlShotenerExp() {
</div>
</div>
<div className="flex items-center gap-1 text-sm font-semibold text-gray-400">
<svg
viewBox="0 0 18 18"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 shrink-0 text-gray-400"
>
<g fill="currentColor">
<path
d="M15.25,9.75H4.75c-1.105,0-2-.895-2-2V3.75"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
></path>
<polyline
fill="none"
points="11 5.5 15.25 9.75 11 14"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
></polyline>
</g>
</svg>
<Icons.forwardArrow className="h-4 w-4 shrink-0 text-gray-400" />
wr.do/dashboard
</div>
</div>
+31 -1
View File
@@ -28,9 +28,10 @@ import {
Image,
Inbox,
Laptop,
LayoutGrid,
LayoutPanelLeft,
LineChart,
Link,
List,
ListChecks,
ListFilter,
Loader2,
@@ -84,6 +85,8 @@ export const Icons = {
calendar: Calendar,
crown: Crown,
lock: LockKeyhole,
list: List,
layoutGrid: LayoutGrid,
unLock: LockKeyholeOpen,
listFilter: ListFilter,
botMessageSquare: BotMessageSquare,
@@ -330,4 +333,31 @@ export const Icons = {
</g>
</svg>
),
forwardArrow: ({ ...props }: LucideProps) => (
<svg
viewBox="0 0 18 18"
xmlns="http://www.w3.org/2000/svg"
// className="h-4 w-4 shrink-0 text-gray-400"
{...props}
>
<g fill="currentColor">
<path
d="M15.25,9.75H4.75c-1.105,0-2-.895-2-2V3.75"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
></path>
<polyline
fill="none"
points="11 5.5 15.25 9.75 11 14"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
></polyline>
</g>
</svg>
),
};
+9
View File
@@ -245,6 +245,15 @@ export function removeUrlSuffix(url: string): string {
return url.startsWith("http") ? url.split("//")[1] : url;
}
export function extractHostname(url: string): string {
try {
const urlObject = new URL(url);
return urlObject.hostname;
} catch (error) {
return "";
}
}
export function toCamelCase(str: string) {
return str
.split("-")
+4
View File
@@ -13,6 +13,10 @@ const nextConfig = {
protocol: "https",
hostname: "**",
},
{
protocol: "https",
hostname: "unavatar.io",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
+1 -1
View File
File diff suppressed because one or more lines are too long