add card-in page

This commit is contained in:
ppc
2024-08-07 22:58:18 +01:00
parent 26c61f3438
commit 84fa0947e1
5 changed files with 199 additions and 9 deletions
+2 -5
View File
@@ -18,6 +18,7 @@ import {
} from "@/components/ui/context-menu";
import {AimeCardEntry} from "@/components/aime-card";
import {IncognitoStorageWarning} from "@/components/incognito-warning";
import {NoCardsNotification} from "@/components/no-cards-notification";
function CardAddButton() {
return (<>
@@ -34,11 +35,7 @@ function CardList(props: { cards: AimeCard[] | undefined }) {
if (!props.cards?.length) {
return (<>
<CardAddButton/>
<div className="flex flex-col gap-2 items-center text-gray-500 pt-6 pb-5">
<CreditCard className="h-8 w-8"/>
<span className="text-center">No cards found</span>
</div>
<NoCardsNotification/>
</>);
}
+1
View File
@@ -3,6 +3,7 @@ import {Inter as FontSans} from "next/font/google";
import {cn} from "@/lib/utils";
import "./globals.css";
import {ThemeProvider} from "@/components/theme-provider";
import {Toaster} from "@/components/ui/toaster";
const fontSans = FontSans({
subsets: ["latin"],
+174
View File
@@ -0,0 +1,174 @@
"use client";
import {useLiveQuery} from "dexie-react-hooks";
import React, {useEffect, useState} from "react";
import {useParams, useRouter} from "next/navigation";
import {ChevronRight, Circle, CircleCheckBig, Home, Plus, ServerOff} from "lucide-react";
import {AimeCard, AimeServer, db} from "@/lib/database";
import {getServerInfo, serverCardIn} from "@/lib/aime-net";
import {Button} from "@/components/ui/button";
import {Footer} from "@/components/footer";
import {Toaster} from "@/components/ui/toaster";
import {useToast} from "@/components/ui/use-toast";
import {Separator} from "@/components/ui/separator";
import {AimeCardEntry} from "@/components/aime-card";
import {LoadingIndicator} from "@/components/loading-indicator";
import {CardCreationDialog} from "@/components/card-creation-form";
import {NoCardsNotification} from "@/components/no-cards-notification";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected: (card: AimeCard) => void }) {
return (<>
<h4 className="text-2xl font-bold">{props.cards?.length ? "Cards" : "Additional Cards"}</h4>
<CardCreationDialog>
<Button className="bg-blue-500">
<Plus className="mr-2 h-4 w-4"/> Add Card
</Button>
</CardCreationDialog>
{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/>}
</>);
}
async function performCardInAction(card: AimeCard, server: AimeServer, setCardInStatus: (status: boolean) => void, toaster: any) {
setCardInStatus(true);
try {
await serverCardIn(server.address, card.id);
await db.cards.update(card.id, {lastUsed: Date.now()});
setTimeout(() => setCardInStatus(false), 1000);
toaster({
title: "Card In Success",
description: "Card in request has been sent successfully.",
duration: 5000
});
} catch {
toaster({
variant: "destructive",
title: "Card in failed",
description: "Failed to perform card in action. Please try again.",
duration: 5000
});
setTimeout(() => setCardInStatus(false), 1000);
}
}
export default function ServerPage() {
const {serverId} = useParams<{ serverId: string }>();
const {toast} = useToast();
const router = useRouter();
const cards = useLiveQuery(() => db.cards.toArray());
const [server, setServer] = useState<AimeServer | null>(null);
const [connectionError, setConnectionError] = useState(false);
const [cardInStatus, setCardInStatus] = useState(false);
const [selectedCard, setSelectedCard] = useState<AimeCard | undefined | null>(null);
// select newest card
useEffect(() => setSelectedCard(cards?.sort(x => x.lastUsed)[0]), [cards]);
useEffect(() => {
const loadServer = async () => {
const sid = parseInt(serverId);
if (isNaN(sid)) {
router.push('/');
return;
}
const server = await db.servers.get(sid);
if (!server) {
router.push('/');
return;
}
try {
const info = await getServerInfo(server.address);
server.name = info.name;
server.lastConnected = Date.now();
setConnectionError(false);
await db.servers.put(server);
} catch {
setConnectionError(true);
}
setServer(server);
};
loadServer().catch(console.error);
}, [serverId]);
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">{server?.name ?? "Server"}</h2>
<Button variant="ghost" onClick={() => router.push('/')}>
<Home className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Servers</span>
</Button>
</div>
<Separator/>
{!server && (<LoadingIndicator/>)}
{(server && connectionError) && (<Alert variant="destructive" className="mb-5">
<ServerOff className="h-4 w-4"/>
<AlertTitle>Connection Issue</AlertTitle>
<AlertDescription>
There was an issue connecting to the machine at <code>{server.address}</code>. Please check the
server is running and try again.
</AlertDescription>
</Alert>)}
<div className="flex flex-col gap-4 w-full flex-grow">
{selectedCard && (<>
<h4 className="text-2xl font-bold">Selected Card</h4>
<AimeCardEntry card={selectedCard}>
<Button size="icon" variant="ghost" disabled>
<CircleCheckBig className="w-5 h-5 text-green-500"/>
</Button>
</AimeCardEntry>
<Button className="flex-grow-0 bg-green-600 mb-5"
disabled={(cardInStatus || connectionError)}
onClick={() => performCardInAction(selectedCard, server as AimeServer, setCardInStatus, toast)}>
{cardInStatus ? "Carding in..." : <>Card In <ChevronRight className="ml-2 h-4 w-4"/></>}
</Button>
</>)}
<CardList cards={cards?.filter(x => x !== selectedCard).sort(x => x.lastUsed) ?? null}
cardSelected={card => {
toast({
duration: 2500,
title: "Card Selected",
description: `${card.name} has been selected.`
});
setSelectedCard(card);
}}/>
</div>
<Footer/>
</main>
<Toaster/>
</>
);
}
+13 -4
View File
@@ -1,7 +1,8 @@
import {CreditCard} from "lucide-react";
import {CreditCard, History} from "lucide-react";
import {Card} from "@/components/ui/card";
import React from "react";
import {AimeCard} from "@/lib/database";
import TimeAgo from "react-timeago";
export function AimeCardEntry(props: { card: AimeCard, children?: Readonly<React.ReactNode> }) {
return (
@@ -9,9 +10,17 @@ export function AimeCardEntry(props: { card: AimeCard, children?: Readonly<React
<div className="flex p-4 items-center gap-3">
<div className="flex-grow flex flex-col gap-4">
<h4 className="text-2xl font-semibold">{props.card.name}</h4>
<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 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}
@@ -0,0 +1,9 @@
import {CreditCard} from "lucide-react";
import React from "react";
export function NoCardsNotification() {
return (<div className="flex flex-col gap-2 items-center text-gray-500 pt-6 pb-5">
<CreditCard className="h-8 w-8"/>
<span className="text-center">No cards found</span>
</div>);
}