Skip to content

Commit

Permalink
wip: most player modal pages tsx (no data/actions)
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Dec 26, 2023
1 parent 7bb5514 commit abe963e
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 7 deletions.
1 change: 1 addition & 0 deletions panel/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const buttonVariants = cva(
},
size: {
default: "h-10 px-4 py-2",
inline: "h-5 px-1.5 rounded-sm text-xs tracking-wider",
xs: "h-7 rounded-sm px-2 text-sm",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
Expand Down
5 changes: 5 additions & 0 deletions panel/src/layout/playerModal/BanTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function BanTab() {
return <>
Ban ban ban...
</>;
}
67 changes: 67 additions & 0 deletions panel/src/layout/playerModal/HistoryTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { PlayerHistoryItem, PlayerModalSuccess } from "@shared/playerApiTypes";



type HistoryItemProps = {
action: PlayerHistoryItem,
permsDisableWarn: boolean,
permsDisableBan: boolean,
serverTime: number,
}
function HistoryItem({ action, permsDisableWarn, permsDisableBan, serverTime }: HistoryItemProps) {
const isRevokeDisabled = (
!!action.revokedBy ||
(action.type == 'warn' && permsDisableWarn) ||
(action.type == 'ban' && permsDisableBan)
);
const actionDate = (new Date(action.ts * 1000)).toLocaleString();

let footerNote, borderColorClass, actionMessage;
if (action.type == 'ban') {
borderColorClass = 'border-destructive';
actionMessage = `BANNED by ${action.author}`;
} else if (action.type == 'warn') {
borderColorClass = 'border-warning';
actionMessage = `WARNED by ${action.author}`;
}
if (action.revokedBy) {
borderColorClass = '';
footerNote = `Revoked by ${action.revokedBy}.`;
}
if (typeof action.exp == 'number') {
const expirationDate = (new Date(action.exp * 1000)).toLocaleString();
footerNote = (action.exp < serverTime) ? `Expired at ${expirationDate}.` : `Expires at ${expirationDate}.`;
}


return (
<div className={cn('pl-2 border-l-4', borderColorClass)}>
<div className="flex w-full justify-between">
<strong className="text-sm">{actionMessage}</strong>
<small className="text-right text-xxs space-x-1">
<span className="font-mono">({action.id})</span>
<span className="opacity-75">{actionDate}</span>
<Button
variant="outline"
size='inline'
disabled={isRevokeDisabled}
onClick={() => { }}
>Revoke</Button>
</small>
</div>
<span className="text-sm">{action.reason}</span>
{footerNote && <small className="block text-xxs opacity-75">{footerNote}</small>}
</div>
);
}


export default function HistoryTab() {
return <div className="flex flex-col gap-1">
{exampleData.player.actionHistory.map((action) => (
<HistoryItem action={action} permsDisableWarn={false} permsDisableBan={false} serverTime={0} />
))}
</div>;
}
79 changes: 79 additions & 0 deletions panel/src/layout/playerModal/IdsTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { txToast } from "@/components/TxToaster";
import { cn } from "@/lib/utils";
import { CopyIcon } from "lucide-react";
import { useState } from "react";

//DEBUG
const exampleData = {
ids: [],
hwids: [],
oldIds: [],
oldHwids: [],
}

type IdsBlockProps = {
title: string,
emptyMessage: string,
allIds: string[],
currIds: string[],
isSmaller?: boolean,
}
function IdsBlock({ title, emptyMessage, allIds, currIds, isSmaller }: IdsBlockProps) {
const [hasCopiedIds, setHasCopiedIds] = useState(false);
const displayCurrIds = currIds.sort((a, b) => a.localeCompare(b));
const displayOldIds = allIds.filter((id) => !currIds.includes(id)).sort((a, b) => a.localeCompare(b));

const handleCopyIds = () => {
try {
//Just to guarantee the correct visual order
const arrToCopy = [...displayCurrIds, ...displayOldIds];
navigator.clipboard.writeText(arrToCopy.join('\n'));
setHasCopiedIds(true);
} catch (error) {
txToast.error('Failed to copy to clipboard :(');
}
}

return <div>
<div className="flex justify-between items-center pb-1">
<h3 className="text-xl">{title}</h3>
{hasCopiedIds ? (
<span className="text-sm text-success-inline">Copied!</span>
) : (
<button onClick={handleCopyIds}>
<CopyIcon className="h-4 text-secondary hover:text-primary" />
</button>
)}
</div>
<p className={cn(
"font-mono break-all whitespace-pre-wrap border rounded divide-y divide-border/50 tracking-wider text-muted-foreground",
isSmaller ? "text-xxs leading-5" : "text-xs leading-6"
)}>
{displayCurrIds.length === 0 && <span className="block px-1 opacity-50 italic">{emptyMessage}</span>}
{displayCurrIds.map((id) => (
<span className="block px-1 font-semibold">{id}</span>
))}
{displayOldIds.map((id) => (
<span className="block px-1 opacity-50">{id}</span>
))}
</p>
</div>
}


export default function IdsTab() {
return <div className="flex flex-col gap-4">
<IdsBlock
title="Player Identifiers"
emptyMessage="This player has no identifiers."
allIds={exampleData.oldIds}
currIds={exampleData.ids}
/>
<IdsBlock
title="Player Hardware IDs"
emptyMessage="This player has no hardware IDs."
allIds={exampleData.oldHwids}
currIds={exampleData.hwids} isSmaller
/>
</div>;
}
91 changes: 91 additions & 0 deletions panel/src/layout/playerModal/InfoTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import InlineCode from "@/components/InlineCode";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";


function LogActionCounter({ type, count }: { type: 'Ban' | 'Warn', count: number }) {
const pluralLabel = (count > 1) ? `${type}s` : type;
if (count === 0) {
return <span className={cn(
'rounded-sm text-xs font-semibold px-1 py-[0.125rem] tracking-widest text-center inline-block',
'bg-secondary text-secondary-foreground'
)}>
0 {type}s
</span>
} else {
return <span className={cn(
'rounded-sm text-xs font-semibold px-1 py-[0.125rem] tracking-widest text-center inline-block',
type === 'Ban' ? 'bg-destructive text-destructive-foreground' : 'bg-warning text-warning-foreground'
)}>
{count} {pluralLabel}
</span>
}
}

function PlayerNotesBox() {
return <>
<Label htmlFor="playerNotes">
Notes:&nbsp;
<span className="text-muted-foreground">Last modified by <InlineCode>tabarra</InlineCode> in October 18, 2003.</span>
</Label>
<Textarea
placeholder="Type your notes about the player."
id="playerNotes"
className="w-full mt-1 bg-black/10 dark:bg-black/40"
/>
</>
}

export default function InfoTab() {
return <div>
<h3 className="text-xl pb-2">Player Information</h3>

<dl className="pb-2">
<div className="py-0.5 grid grid-cols-3 gap-4 px-0">
<dt className="text-sm font-medium leading-6 text-muted-foreground">Session Time</dt>
<dd className="text-sm leading-6 col-span-2 mt-0">5 hours, 43 minutes</dd>
</div>
<div className="py-0.5 grid grid-cols-3 gap-4 px-0">
<dt className="text-sm font-medium leading-6 text-muted-foreground">Play Time</dt>
<dd className="text-sm leading-6 col-span-2 mt-0">18 days, 21 hours</dd>
</div>
<div className="py-0.5 grid grid-cols-3 gap-4 px-0">
<dt className="text-sm font-medium leading-6 text-muted-foreground">Joined</dt>
<dd className="text-sm leading-6 col-span-2 mt-0">20 December 2022</dd>
</div>
<div className="py-0.5 grid grid-cols-3 gap-4 px-0">
<dt className="text-sm font-medium leading-6 text-muted-foreground">Whitelisted</dt>
<dd className="text-sm leading-6 mt-0">
not yet
</dd>
<dd className="text-right">
<Button
variant="outline"
size='inline'
style={{minWidth: '8ch'}}
onClick={() => { }}
>Add WL</Button>
</dd>
</div>
<div className="py-0.5 grid grid-cols-3 gap-4 px-0">
<dt className="text-sm font-medium leading-6 text-muted-foreground">Log</dt>
<dd className="text-sm leading-6 mt-0 space-x-2">
<LogActionCounter type="Ban" count={12} />
<LogActionCounter type="Warn" count={2} />
</dd>
<dd className="text-right">
<Button
variant="outline"
size='inline'
style={{minWidth: '8ch'}}
onClick={() => { }}
>View</Button>
</dd>
</div>
</dl>

<PlayerNotesBox />
</div>;
}
93 changes: 86 additions & 7 deletions panel/src/layout/playerModal/PlayerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,115 @@ import { Button } from "@/components/ui/button";
import { usePlayerModalStateValue } from "@/hooks/playerModal";
import { AlertTriangleIcon, MailIcon, ShieldCheckIcon, InfoIcon, ListIcon, HistoryIcon, BanIcon } from "lucide-react";
import { KickOneIcon } from '@/components/KickIcons';
import InfoTab from "./InfoTab";
import { useEffect, useState } from "react";
import IdsTab from "./IdsTab";
import { ScrollArea } from "@/components/ui/scroll-area";
import HistoryTab from "./HistoryTab";
import BanTab from "./BanTab";
import GenericSpinner from "@/components/GenericSpinner";


const modalTabs = [
{
title: 'Info',
icon: <InfoIcon className="mr-2 h-5 w-5 hidden xs:block" />,
content: <InfoTab />
},
{
title: 'IDs',
icon: <ListIcon className="mr-2 h-5 w-5 hidden xs:block" />,
content: 'ids ids ids'
},
{
title: 'History',
icon: <HistoryIcon className="mr-2 h-5 w-5 hidden xs:block" />,
content: 'history history history'
},
{
title: 'Ban',
icon: <BanIcon className="mr-2 h-5 w-5 hidden xs:block" />,
content: 'ban ban ban'
}
]


export default function PlayerModal() {
const { isModalOpen, closeModal, playerRef } = usePlayerModalStateValue();
const [selectedTab, setSelectedTab] = useState(modalTabs[0].title);

useEffect(() => {
if (!isModalOpen) {
setTimeout(() => {
setSelectedTab(modalTabs[0].title);
}, 200);
}
}, [isModalOpen]);

const handleOpenClose = (newOpenState: boolean) => {
if (isModalOpen && !newOpenState) {
closeModal();
}
};

// const modalData = undefined;
const modalData = 'whatever';

return (
<Dialog open={isModalOpen} onOpenChange={handleOpenClose}>
<DialogContent className="max-w-2xl" autoFocus={false}>
<DialogHeader>
<DialogTitle>[2] Tang Salvatore</DialogTitle>
<DialogContent
className="max-w-2xl h-full sm:h-auto max-h-full p-0 gap-1 sm:gap-4 flex flex-col"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader className="p-4 border-b">
<DialogTitle>[1234] Whatever Example</DialogTitle>
</DialogHeader>

{JSON.stringify(playerRef)}
<span className="text-fuchsia-500">pretend here lies the player modal content</span>
<div className="flex flex-col md:flex-row md:px-4 h-full">
<div className="flex flex-row md:flex-col gap-1 bg-muted md:bg-transparent p-1 md:p-0 mx-2 md:mx-0 rounded-md !bg-cyan-700x">
{modalTabs.map((tab, index) => (
<Button
key={tab.title}
variant={selectedTab === tab.title ? "secondary" : "ghost"}
className={`w-full tracking-wider justify-center md:justify-start
h-7 rounded-sm px-2 text-sm
md:h-10 md:text-base`}
onClick={() => setSelectedTab(tab.title)}
>
{tab.icon} {tab.title}
</Button>
))}
</div>
{/* NOTE: consistent height: sm:h-[19rem] */}
<ScrollArea className="w-full max-h-[calc(100vh-3.125rem-4rem-5rem)] px-4 py-2 md:py-0">
{!modalData ? (
<div className="flex items-center justify-center min-h-[18.5rem]">
<GenericSpinner msg="Loading..." />
</div>
) : (
<>
{selectedTab === 'Info' && <InfoTab />}
{selectedTab === 'IDs' && <IdsTab />}
{selectedTab === 'History' && <HistoryTab />}
{selectedTab === 'Ban' && <BanTab />}
</>
)}
</ScrollArea>
</div>

<DialogFooter className="w-full justify-centerx xjustify-around sm:flex-colx gap-2">
<DialogFooter className="max-w-2xl gap-2 p-2 md:p-4 border-t grid grid-cols-2 sm:flex">
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
onClick={() => { }} //FIXME:
className="pl-2"
className="pl-2 sm:mr-auto"
>
<ShieldCheckIcon className="h-5 mr-1" /> Give Admin
</Button>
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
onClick={() => { }} //FIXME:
className="pl-2"
Expand All @@ -48,6 +125,7 @@ export default function PlayerModal() {
</Button>
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
onClick={() => { }} //FIXME:
className="pl-2"
Expand All @@ -61,6 +139,7 @@ export default function PlayerModal() {
</Button>
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
onClick={() => { }} //FIXME:
className="pl-2"
Expand Down

0 comments on commit abe963e

Please sign in to comment.