Skip to content

Commit

Permalink
wip: playerlist done
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Dec 20, 2023
1 parent 42785c2 commit 6c561c4
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 45 deletions.
21 changes: 11 additions & 10 deletions docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,14 @@ Processo:
- SHELL:
- [x][5d] fully responsive layout (show/hide sidebars, login, addMaster, etc)
- [x][2h] merge new shell design into the `txadmin/panel` codebase
- [ ][~3d] implement most shell+status features
- [x][~3d] implement most shell+status features
- [x][1d] socket.io connection for default room
- [x][2h] warning for outdated tx, visible in all pages
- [x][1h] dynamic title
- [x][1h] dynamic favicon
- [x][1d] server status
- [x][4h] update notices via socket.io
- [x][2h] tooltips on everything
- [ ][1h] zap hosting advertisement
- [x][1d] toasts API
- [x] generic toasts
- [x] markdown toasts
Expand All @@ -85,8 +84,7 @@ Processo:
- [x] remove legacy header + change password code
- [x] admin manager should open "my account" when trying to edit self
- [x] maybe separate the backend routes
- [ ][3d] playerlist
- [ ][1d] add the new logos to shell+auth pages
- [x][3d] playerlist
- [ ][3h] playerlist click opens legacy player modal (`iframe.contentWindow.postMessage("openModal", ???);`)
- [ ][5d] full auth flow
- [x] password login
Expand All @@ -108,6 +106,8 @@ Processo:
- [ ][3d] NEW PAGE: Live console
- [ ][3d] NEW PAGE: Players
- [ ][1d] NEW PAGE: History
- [ ][1h] zap hosting advertisement
- [ ][1d] add the new logos to shell+auth pages

- [x][2d] light/dark theme
- [x][1d] adapt legacy styles to somewhat match shadcn
Expand All @@ -132,11 +132,7 @@ Quickies
- [ ] easter egg with some old music? https://www.youtube.com/watch?v=nNoaXej0Jeg

Bugs
- [ ] when you open the server sheet, the control tooltip shows automatically
- [x] nui iframe scroll on 1080p screen
- should not scroll even with 7 server menu items
- possible solution would be to hide the zap ad if !isWebInterface
- [x] no `target="_blank"` will work in NUI (zap ad + support), so either hide the buttons or find a way to open links
- [ ] make sure the playerlist scroll works if the playergen is stopped (therefore, not re-rendering the component)


=======================================================================
Expand Down Expand Up @@ -200,7 +196,12 @@ Master Actions:
=======================================================================

## Next Up
- [ ] xxxx
- [ ] Playerlist: implement basic tag system with filters, sorting and Fuse.js
- the filter dropdown is written already, check `panel/src/layout/playerlistSidebar/Playerlist.tsx`
- when filterString is present, disable the filter/sort drowdown, as it will show all results sorted by fuse.js
- might be worth to debounce the search


- [ ] write some automated tests for the auth logic and middlewares
- [ ] instead of showing cfg errors when trying to start server, just show "there are errors in your cfg file" and link the user to the cfg editor page
- [ ] fix the eslint config
Expand Down
1 change: 1 addition & 0 deletions panel/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
plugins: {
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
Expand Down
155 changes: 125 additions & 30 deletions panel/src/layout/playerlistSidebar/Playerlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,133 @@ import { playerlistAtom } from "@/hooks/playerlist";
import cleanPlayerName from "@shared/cleanPlayerName";
import { useAtomValue } from "jotai";
import { VirtualItem, useVirtualizer } from '@tanstack/react-virtual';
import { useMemo, useRef, useState } from "react";
import { memo, useMemo, useRef, useState } from "react";
import { PlayerlistPlayerType } from "@shared/socketioTypes";
import { Input } from "@/components/ui/input";
import { ArrowDownWideNarrowIcon, XIcon } from "lucide-react";
import { FilterXIcon, SlidersHorizontalIcon, XIcon } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";


//NOTE: Move the styles (except color) to global.css since this component is rendered often
function TagColor({ color }: { color: string }) {
return <div
className="outline-none focus:outline-none"
style={{
display: 'inline-block',
backgroundColor: color,
width: '0.375rem',
borderRadius: '2px',
}}
>&nbsp;</div>;
}


type PlayerlistFilterProps = {
filterString: string;
setFilterString: (s: string) => void;
};
function PlayerlistFilter({ filterString, setFilterString }: PlayerlistFilterProps) {
return (
<div className="pt-2 px-2 flex gap-2">
<div className="relative w-full">
<Input
className="h-8"
placeholder="Filter by Name or ID"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setFilterString('');
}
}}
/>
{filterString && <button
className="absolute right-2 top-0 bottom-0 text-zinc-500 dark:text-zinc-400 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg"
onClick={() => setFilterString('')}
>
<XIcon />
</button>}
</div>
{/* <DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className={cn(
'h-8 w-8 inline-flex justify-center items-center rounded-md shrink-0',
'ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'border bg-muted shadow-sm',
'hover:bg-primary hover:text-primary-foreground hover:border-primary',
)}
>
<SlidersHorizontalIcon className="h-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Filter by Tag</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={true}
className="cursor-pointer hover:!bg-secondary hover:!text-current focus:!bg-secondary focus:!text-current"
>
<div className="flex justify-around min-w-full">
<span className="grow pr-4">Admin</span>
<TagColor color="#EF4444" />
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={true}
className="cursor-pointer hover:!bg-secondary hover:!text-current focus:!bg-secondary focus:!text-current"
>
<div className="flex justify-around min-w-full">
<span className="grow pr-4">Newcomer</span>
<TagColor color="#A3E635" />
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={false}
className="cursor-pointer hover:!bg-secondary hover:!text-current focus:!bg-secondary focus:!text-current"
>
<div className="flex justify-around min-w-full">
<span className="grow pr-4">Watch List</span>
<TagColor color="#FB923C" />
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuItem
onClick={undefined}
className="cursor-pointer hover:!bg-secondary hover:!text-current focus:!bg-secondary focus:!text-current"
>
<FilterXIcon className="mr-2 h-4 w-4" />
Clear Filter
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuRadioGroup value="id">
<DropdownMenuRadioItem
value="id"
className="cursor-pointer hover:!bg-secondary hover:!text-current focus:!bg-secondary focus:!text-current"
>Join Order</DropdownMenuRadioItem>
<DropdownMenuRadioItem
value="tag"
className="cursor-pointer hover:!bg-secondary hover:!text-current focus:!bg-secondary focus:!text-current"
>Tag Priority</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu> */}
</div>
);
};
const PlayerlistFilterMemo = memo(PlayerlistFilter);


//NOTE: the styles have been added to global.css since this component is rendered A LOT
Expand All @@ -35,7 +155,7 @@ export default function Playerlist() {
const scrollRef = useRef<HTMLDivElement>(null);
const [filterString, setFilterString] = useState('');

//FIXME: temporary logic, use fuse.js or something like that
//TODO: temporary logic, use fuse.js or something like that
const filteredPlayerlist = useMemo(() => {
const pureFilter = cleanPlayerName(filterString).pureName;
if (pureFilter !== 'emptyname') {
Expand Down Expand Up @@ -67,32 +187,7 @@ export default function Playerlist() {

return (
<>
{/* FIXME: With the filter/sorting dropdown, this will become an expensive component, memoize it! */}
<div className="pt-4 px-2 flex gap-2">
<div className="relative w-full">
<Input
className="h-8"
placeholder="Filter by Name or ID"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setFilterString('');
}
}}
/>
{filterString && <button
className="absolute right-2 top-0 bottom-0 text-slate-600"
onClick={() => setFilterString('')}
>
<XIcon />
</button>}
</div>
<Button
variant="default"
className="h-8 w-8"
><ArrowDownWideNarrowIcon /></Button>
</div>
<PlayerlistFilterMemo filterString={filterString} setFilterString={setFilterString} />

<div
className={cn(
Expand Down
1 change: 1 addition & 0 deletions panel/src/layout/playerlistSidebar/PlayerlistSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function PlayerlistSidebar({ isSheet }: PlayerSidebarProps) {
</div>
<div
className={cn(
'min-h-[480px]',
!isSheet && 'rounded-xl border border-border bg-card text-card-foreground shadow-sm',
'flex flex-col gap-2 flex-grow overflow-hidden',
)}
Expand Down
17 changes: 12 additions & 5 deletions panel/src/layout/playerlistSidebar/PlayerlistSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { playerCountAtom } from "@/hooks/playerlist";
import { useAtomValue } from "jotai";
import { UsersIcon } from "lucide-react";
import { useEffect, useRef } from "react";


export default function PlayerlistSummary() {
const playerCount = useAtomValue(playerCountAtom);
const playerCountFormatted = playerCount.toLocaleString("en-US");

return (
<div
className="flex justify-center items-center h-[211px]
text-3xl font-extralight text-center tracking-wider"
>
PLAYERS: {playerCount}
<div className="w-full flex justify-between items-center p-4">
<div className="w-16 h-16 dark:bg-zinc-600/50 bg-zinc-300/75 rounded-full flex items-center justify-center">
<UsersIcon className="w-10 h-10 dark:text-zinc-400 text-zinc-500 text-opacity-80 stroke-1" />
</div>
<div className="flex flex-col items-end">
<div className="text-4xl font-mono font-extralight">{playerCountFormatted}</div>
<div className="opacity-80 text-lg font-light tracking-wider">Players</div>
</div>
</div>
);
}

0 comments on commit 6c561c4

Please sign in to comment.