abstract card design to component, fix text truncation

This commit is contained in:
ppc
2024-08-10 13:39:22 +01:00
parent 431139faef
commit 2e4f3b8140
6 changed files with 90 additions and 71 deletions
+20 -22
View File
@@ -3,29 +3,27 @@ import TimeAgo from "react-timeago";
import {CreditCard, History} from "lucide-react";
import {AimeCard} from "@/lib/database";
import {Card} from "@/components/ui/card";
import {useMediaQuery} from "@/hooks/use-media-query.tsx";
import {AMCard, AMCardActionProps, AMCardDetail} from "@/components/am-card.tsx";
export function AimeCardEntry(props: AMCardActionProps & { card: AimeCard, children?: Readonly<React.ReactNode> }) {
const isDesktop = useMediaQuery("(min-width: 512px)");
export function AimeCardEntry(props: { card: AimeCard, children?: Readonly<React.ReactNode> }) {
return (
<Card>
<div className="flex p-4 items-center gap-3">
<div className="flex-grow flex flex-col gap-4">
<h4 className="text-2xl font-semibold select-none">{props.card.name}</h4>
<div className="text-md flex gap-3 flex-wrap">
{props.card.lastUsed > 0 && (
<div className="flex items-center gap-2 text-gray-500">
<History className="h-5 w-5"/>
<TimeAgo date={props.card.lastUsed}/>
</div>
)}
<div className="flex items-center gap-2 text-gray-500">
<CreditCard className="h-5 w-5"/>
<code>{props.card.id.match(/.{1,4}/g)?.join(" ")}</code>
</div>
</div>
</div>
{props.children}
</div>
</Card>
<AMCard title={props.card.name} actionIcon={props.actionIcon} actionClicked={props.actionClicked} actionDisabled={props.actionDisabled}>
{props.card.lastUsed > 0 && (
<AMCardDetail>
<History className="h-5 w-5"/>
<TimeAgo date={props.card.lastUsed}/>
</AMCardDetail>
)}
<AMCardDetail>
<CreditCard className="h-5 w-5"/>
<code>{isDesktop
? props.card.id.match(/.{1,4}/g)?.join(" ")
: `•••• ${props.card.id.substring(props.card.id.length - 4)}`}
</code>
</AMCardDetail>
</AMCard>
);
}
+40
View File
@@ -0,0 +1,40 @@
import React from "react";
import {ChevronRight} from "lucide-react";
import {Card} from "@/components/ui/card.tsx";
import {Button} from "@/components/ui/button.tsx";
export interface AMCardActionProps {
actionIcon?: Readonly<React.ReactNode>,
actionClicked?: () => void,
actionDisabled?: boolean,
}
export function AMCard(props: AMCardActionProps & { title: string, children: Readonly<React.ReactNode> }) {
return (
<Card>
<div className="grid grid-cols-[1fr_40px] items-center gap-1 p-4 pr-2 sm:p-6">
<div className="max-w-full overflow-hidden space-y-3">
<h4 className="text-2xl font-semibold select-none text-ellipsis whitespace-nowrap overflow-hidden">
{props.title}
</h4>
<div className="text-sm lg:text-md flex flex-auto flex-wrap gap-3">
{props.children}
</div>
</div>
{props.actionClicked && (
<Button className="flex-shrink-0" variant="ghost" size="icon" disabled={props.actionDisabled} onClick={props.actionClicked}>
{props.actionIcon ?? <ChevronRight className="h-4 w-4"/>}
</Button>
)}
</div>
</Card>
)
}
export function AMCardDetail(props: { children?: Readonly<React.ReactNode> }) {
return (
<div className="inline-flex items-center max-w-full gap-2 text-gray-500">
{props.children}
</div>
)
}
@@ -1,4 +1,4 @@
export function Footer() {
export function AMFooter() {
return (<footer>
<span className="text-gray-500 text-sm">AimeNet &copy; ppc {new Date().getUTCFullYear()}</span>
</footer>);
+9 -18
View File
@@ -6,12 +6,11 @@ import {ChevronRight, Circle, CircleCheckBig, Home, Plus, ServerOff, Wallet} fro
import {AimeCard, AimeServer, db} from "@/lib/database";
import {getServerInfo, serverCardIn} from "@/lib/aime-net";
import {Footer} from "@/components/footer";
import {AMFooter} from "@/components/am-footer.tsx";
import {Button} from "@/components/ui/button";
import {useToast} from "@/components/ui/use-toast";
import {Separator} from "@/components/ui/separator";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
import {AimeCardEntry} from "@/components/aime-card";
import {LoadingIndicator} from "@/components/loading-indicator";
@@ -43,6 +42,7 @@ async function performCardInAction(card: AimeCard, server: AimeServer, setCardIn
setTimeout(() => setCardInStatus(false), 1000);
}
}
/* eslint-enable @typescript-eslint/no-explicit-any */
function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected: (card: AimeCard) => void }) {
@@ -62,27 +62,18 @@ function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected:
</Button>
</div>
{props.cards?.length ? props.cards.map(x => <AimeCardEntry card={x} key={x.id}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost" onClick={() => props.cardSelected(x)}>
<Circle className="w-5 h-5"/>
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Select Card</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</AimeCardEntry>) : <NoCardsNotification/>}
{!props.cards?.length
? <NoCardsNotification/>
: props.cards.map(x => <AimeCardEntry key={x.id} card={x}
actionIcon={<Circle className="w-5 h-5"/>}
actionClicked={() => props.cardSelected(x)}/>)}
</>);
}
function ServerCardInPage() {
const {toast} = useToast();
const {serverId} = useParams<{ serverId: string }>();
const navigate = useNavigate();
const cards = useLiveQuery(() => db.cards.toArray());
@@ -182,7 +173,7 @@ function ServerCardInPage() {
}}/>
</div>
)}
<Footer/>
<AMFooter/>
</main>
</>
);
+2 -2
View File
@@ -4,7 +4,7 @@ import {Plus, Server, Trash} from "lucide-react";
import {AimeCard, db} from "@/lib/database";
import {Footer} from "@/components/footer";
import {AMFooter} from "@/components/am-footer.tsx";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
import {
@@ -75,7 +75,7 @@ function CardManagerPage() {
{cards ? <CardList cards={cards}/> : <LoadingIndicator/>}
</div>
<Footer/>
<AMFooter/>
</main>
);
}
+18 -28
View File
@@ -6,19 +6,18 @@ import {useNavigate} from 'react-router-dom';
import {useLiveQuery} from "dexie-react-hooks";
import {zodResolver} from "@hookform/resolvers/zod";
import {
ChevronRight,
Earth,
History,
Plus,
ShieldAlert,
SquareArrowOutUpRight,
Trash, WalletCards
Trash,
WalletCards
} from "lucide-react";
import {AimeServer, db} from "@/lib/database";
import {Card} from "@/components/ui/card";
import {Footer} from "@/components/footer";
import {AMFooter} from "@/components/am-footer.tsx";
import {Input} from "@/components/ui/input";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
@@ -32,6 +31,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger
} from "@/components/ui/context-menu";
import {AMCard, AMCardDetail} from "@/components/am-card.tsx";
const serverAdditionSchema = z.object({
address: z.string().startsWith("http")
@@ -60,28 +60,18 @@ function ServerList(props: { servers: AimeServer[] | undefined }) {
{props.servers.map(s =>
<ContextMenu key={s.id}>
<ContextMenuTrigger>
<Card>
<div className="flex p-6 items-center gap-3">
<div className="flex-grow flex flex-col gap-4">
<h4 className="text-2xl font-semibold select-none">{s.name}</h4>
<div className="text-md flex gap-3 flex-wrap">
{s.lastConnected > 0 && (
<div className="flex items-center gap-2 text-gray-500">
<History className="h-5 w-5"/>
<TimeAgo date={s.lastConnected}/>
</div>
)}
<div className="flex items-center gap-2 text-gray-500">
<Earth className="h-5 w-5"/>
<code>{s.address}</code>
</div>
</div>
</div>
<Button variant="ghost" size="icon" onClick={() => navigate(`/servers/${s.id}`)}>
<ChevronRight className="h-4 w-4"/>
</Button>
</div>
</Card>
<AMCard title={s.name} actionClicked={() => navigate(`/servers/${s.id}`)}>
{s.lastConnected > 0 && (
<AMCardDetail>
<History className="h-5 w-5"/>
<TimeAgo date={s.lastConnected}/>
</AMCardDetail>
)}
<AMCardDetail>
<Earth className="h-5 w-5"/>
<code className="text-ellipsis whitespace-nowrap overflow-hidden">{s.address}</code>
</AMCardDetail>
</AMCard>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem inset onClick={() => navigate(`/servers/${s.id}`)}>
@@ -203,7 +193,7 @@ function ServerListing() {
return (
<main className="flex flex-col gap-5 container min-h-screen py-10">
<div className="flex items-center gap-1">
<h2 className="text-4xl font-bold select-none flex-grow">Servers</h2>
<h2 className="text-4xl font-bold select-none flex-grow">AimeNet</h2>
<Button variant="ghost" size={"default"} onClick={() => navigate('/cards')}>
<WalletCards className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Manage Cards</span>
@@ -215,7 +205,7 @@ function ServerListing() {
<ServerList servers={servers}/>
</div>
<Footer/>
<AMFooter/>
</main>
);
}