mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-26 16:18:17 +03:00
add card-in page
This commit is contained in:
@@ -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/>
|
||||
</>);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
Reference in New Issue
Block a user