Compare commits

..

10 Commits

Author SHA1 Message Date
Gavin McDonald
f7d10a9b4f tweaks 2025-04-25 16:19:40 -04:00
Gavin McDonald
f6749f3146 z-index adjustments 2025-04-24 18:08:23 -04:00
Gavin McDonald
6e2247d6f3 update colors 2025-04-24 17:54:47 -04:00
Gavin McDonald
969d9f5028 fancy fonts 2025-04-24 14:20:07 -04:00
Gavin McDonald
1a4789af4c animate Settings and Notes 2025-04-24 11:24:15 -04:00
Gavin McDonald
3d3cb7a45e update Settings 2025-04-23 15:50:21 -04:00
Gavin McDonald
0c8d2273ea Notes for card info 2025-04-23 15:49:32 -04:00
Gavin McDonald
5b21c560d6 update card info ordering 2025-04-23 15:44:36 -04:00
Gavin McDonald
bd7d42de55 custom classes for ToolTip 2025-04-23 15:41:20 -04:00
Gavin McDonald
90fd231fbd more-customizable CopyButton 2025-04-23 15:39:28 -04:00
12 changed files with 244 additions and 92 deletions

View File

@@ -6,6 +6,7 @@ import { socket } from '@/socket';
import Settings from '@/components/Settings';
import Card from '@/components/Card';
import Notes from '@/components/Notes';
import NotFound from '@/components/NotFound';
import { cardMap, layout } from '@/constants/tarokka';
@@ -105,6 +106,7 @@ export default function GamePage() {
</div>
))}
</div>
<Notes gameData={gameData} show={cards.every(({ flipped }) => flipped)} />
</main>
) : null;
}

View File

@@ -1,15 +1,23 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { Pirata_One, Eagle_Lake, Cinzel_Decorative } from 'next/font/google';
import './globals.css';
const geistSans = Geist({
variable: '--font-geist-sans',
const pirataOne = Pirata_One({
variable: '--font-pirata',
subsets: ['latin'],
weight: '400',
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
const eagleLake = Eagle_Lake({
variable: '--font-eagle-lake',
subsets: ['latin'],
weight: '400',
});
const cinzel = Cinzel_Decorative({
variable: '--font-cinzel',
subsets: ['latin'],
weight: '400',
});
export const metadata: Metadata = {
@@ -23,8 +31,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
<html
lang="en"
className={`${pirataOne.variable} ${eagleLake.variable} ${cinzel.variable} antialiased`}
>
<body className={`${eagleLake.className} antialiased`}>{children}</body>
</html>
);
}

View File

@@ -19,7 +19,7 @@ export default function Home() {
<main className="min-h-screen flex items-center justify-center bg-[url('/img/table3.png')] bg-cover bg-center">
<button
onClick={handleCreateGame}
className="bg-gray-800 hover:bg-gray-700 text-white text-lg px-6 py-3 rounded-xl shadow transition cursor-pointer"
className="bg-slate-800 hover:bg-slate-700 text-yellow-400 hover:text-yellow-300 text-lg px-6 py-3 rounded-lg shadow transition-all duration-250 cursor-pointer"
>
Create New Game
</button>

View File

@@ -33,8 +33,8 @@ export default function Card({ dm, card, position, settings, flipAction }: CardP
<>
{text.map((t, i) => (
<div key={i}>
<p>{t}</p>
{i < text.length - 1 && <hr className="my-2 border-gray-300" />}
<p className="text-yellow-400">{t}</p>
{i < text.length - 1 && <hr className="my-2 border-yellow-400" />}
</div>
))}
</>
@@ -54,14 +54,14 @@ export default function Card({ dm, card, position, settings, flipAction }: CardP
<img
src={getURL(cardBack as TarokkaGameCard, settings)}
alt="Card Back"
className="rounded-lg border border-gray-600"
className="rounded-lg border border-yellow-500"
/>
</div>
<div className="absolute group inset-0 backface-hidden rotate-y-180">
<img
src={getURL(card, settings)}
alt={aria}
className="rounded-lg border border-gray-600 "
className="rounded-lg border border-yellow-500 "
/>
</div>
</div>

View File

@@ -6,11 +6,18 @@ import { Copy as CopyIcon, Check as CheckIcon } from 'lucide-react';
import ToolTip from '@/components/ToolTip';
type CopyButtonProps = {
title: string;
title?: string;
copy: string;
tooltip?: string | string[];
className?: string;
};
export default function CopyButton({ title, copy }: CopyButtonProps) {
export default function CopyButton({
title,
copy,
tooltip = ['Copy', 'Copied'],
className,
}: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@@ -23,21 +30,24 @@ export default function CopyButton({ title, copy }: CopyButtonProps) {
}
};
const ttContent = (
<span className="text-yellow-300">
{Array.isArray(tooltip) && tooltip.length > 1 ? (copied ? tooltip[1] : tooltip[0]) : tooltip}
</span>
);
return (
<ToolTip content={copy}>
<button
onClick={handleCopy}
className="w-full py-1 px-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg flex flex-col items-start gap-1 shadow transition-all cursor-pointer"
>
<button onClick={handleCopy} className={`cursor-pointer ${className}`}>
<ToolTip content={ttContent} className="w-full font-yellow-400">
<div className="flex items-center gap-2 w-full text-sm font-medium">
{`Copy ${title}`}
{title}
{copied ? (
<CheckIcon className="ml-auto" size={16} />
) : (
<CopyIcon className="ml-auto" size={16} />
)}
</div>
</button>
</ToolTip>
</button>
);
}

78
components/Notes.tsx Normal file
View File

@@ -0,0 +1,78 @@
'use client';
import { useMemo, useState } from 'react';
import { ScrollText } from 'lucide-react';
import CopyButton from '@/components/CopyButton';
import Scrim from '@/components/Scrim';
import getCardInfo from '@/tools/getCardInfo';
import { cardMap, layout } from '@/constants/tarokka';
import { GameUpdate } from '@/types';
type NotesProps = {
gameData: GameUpdate;
show: boolean;
};
export default function Notes({ gameData: { dmID, cards, settings }, show }: NotesProps) {
const isDM = !!dmID;
const [open, setOpen] = useState(false);
const notes: (string[] | undefined)[] = useMemo(
() =>
Array.from({ length: 9 })
.map((_cell: unknown, index: number) => cards[cardMap[index]])
.map((card, index) =>
card ? getCardInfo(card, layout[cardMap[index]], isDM, settings) : null,
)
.map(
(_cell: unknown, index: number, cards) =>
cards[Object.keys(cardMap).find((key) => cardMap[key] === index) || 0],
)
.filter((truthy) => truthy),
[cards, isDM, settings],
);
const showNotes = show && open && (isDM || settings.notes);
return (
<div
className={`fixed bottom-4 right-4 z-25 transition-all duration-250 ${show ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
>
<button
className={`text-yellow-400 hover:text-yellow-300 p-2 transition-all duration-250 cursor-pointer ${showNotes ? 'pointer-events-none opacity-0' : 'pointer-events-auto opacity-100'}`}
onClick={() => setOpen((prev) => !prev)}
>
<ScrollText className="w-5 h-5" />
</button>
<Scrim
clickAction={() => setOpen((prev) => !prev)}
className={`transition-all duration-250 ${showNotes ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
>
<div
className={`fixed bottom-4 right-4 transition-all duration-250 bg-slate-800 border border-yellow-400 rounded-lg space-y-2 ${showNotes ? 'w-[33vw] h-[67vh]' : 'w-0 h-0'}`}
>
<CopyButton
copy={notes.map((note) => note!.join('\n')).join('\n\n')}
className="text-yellow-400 hover:drop-shadow-[0_0_1px_#ffd700] absolute top-2 right-2 p-2 transition-all duration-250 bg-black/20 hover:bg-black/40 rounded-full cursor-pointer"
/>
<div className="text-yellow-400 h-full overflow-scroll p-6 transition-all delay-200 duration-50 ${showNotes ? 'opacity-100' : 'opacity-0'}">
{notes.map((note, index) => (
<div key={index}>
<div className="flex flex-col gap-2">
{note!.map((blurb, index) => (
<p key={index}>{blurb}</p>
))}
</div>
{index < notes.length - 1 && <hr className="my-3 border-yellow-400" />}
</div>
))}
</div>
</div>
</Scrim>
</div>
);
}

26
components/Scrim.tsx Normal file
View File

@@ -0,0 +1,26 @@
'use client';
type ScrimProps = {
children: React.ReactNode;
clickAction: (event: React.MouseEvent<HTMLDivElement>) => void;
show?: boolean;
className?: string;
};
export default function Scrim({ children, clickAction, show = true, className = '' }: ScrimProps) {
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
clickAction(event);
}
};
if (!show) return null;
return (
<div
onClick={handleClick}
className={`fixed inset-0 bg-black/20 backdrop-blur-sm z-40 ${className}`}
>
{children}
</div>
);
}

View File

@@ -1,23 +1,28 @@
'use client';
import { useState } from 'react';
import { Settings as Gear, X } from 'lucide-react';
import { Settings as Gear } from 'lucide-react';
import { Cinzel_Decorative } from 'next/font/google';
import CopyButton from '@/components/CopyButton';
import Scrim from '@/components/Scrim';
import Switch from '@/components/Switch';
import { CardStyle, GameUpdate } from '@/types';
type PermissionTogglePanelProps = {
const cinzel = Cinzel_Decorative({
variable: '--font-cinzel',
subsets: ['latin'],
weight: '400',
});
type SettingsProps = {
gameData: GameUpdate;
changeAction: (updatedSettings: GameUpdate) => void;
};
const cardStyleOptions: CardStyle[] = ['standard', 'color', 'grayscale'];
export default function PermissionTogglePanel({
gameData,
changeAction,
}: PermissionTogglePanelProps) {
export default function Settings({ gameData, changeAction }: SettingsProps) {
const [open, setOpen] = useState(false);
const togglePermission = (key: string) => {
@@ -40,49 +45,46 @@ export default function PermissionTogglePanel({
});
};
return (
<div className="fixed top-4 right-4 z-50">
{!open && (
<button
className="p-2 text-gray-100 hover:text-gray-300 cursor-pointer"
onClick={() => setOpen((prev) => !prev)}
>
<Gear className="w-5 h-5" />
</button>
)}
const Links = () => (
<>
<CopyButton
title="Copy DM link"
copy={`${location.origin}/${gameData.dmID}`}
tooltip={`${location.origin}/${gameData.dmID}`}
className="flex flex-row content-between w-full py-1 px-2 transition-all duration-250 bg-slate-700 hover:bg-slate-600 hover:text-yellow-300 rounded-lg shadow"
/>
<CopyButton
title="Copy Spectator link"
copy={`${location.origin}/${gameData.spectatorID}`}
tooltip={`${location.origin}/${gameData.spectatorID}`}
className="flex flex-row content-between w-full py-1 px-2 transition-all duration-250 bg-slate-700 hover:bg-slate-600 hover:text-yellow-300 rounded-lg shadow"
/>
</>
);
{open && (
<div className="relative text-gray-100 bg-gray-800 shadow-lg rounded-lg border border-gray-500 p-6 space-y-2">
<button
className="absolute top-1 right-1 p-1 hover:text-gray-300 cursor-pointer"
onClick={() => setOpen((prev) => !prev)}
>
<X className="w-4 h-4" />
</button>
<CopyButton title="DM link" copy={`${location.origin}/${gameData.dmID}`} />
<CopyButton title="Spectator link" copy={`${location.origin}/${gameData.spectatorID}`} />
const Permissions = () => (
<>
{Object.entries(gameData.settings)
.filter(([_key, value]) => typeof value === 'boolean')
.map(([key, value]) => (
<Switch
key={key}
label={key}
value={value}
toggleAction={() => togglePermission(key)}
/>
<Switch key={key} label={key} value={value} toggleAction={() => togglePermission(key)} />
))}
</>
);
const CardStyle = () => (
<fieldset className="flex flex-col">
<div className="text-xs text-gray-400 mb-1">Card style:</div>
<div className="text-xs mb-1">Card style:</div>
<div className="inline-flex overflow-hidden rounded-md w-full">
{cardStyleOptions.map((option, index) => (
<label
key={option}
className={`cursor-pointer px-4 py-2 text-sm font-medium transition
${gameData.settings.cardStyle === option ? 'bg-gray-500 text-white' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'}
${gameData.settings.cardStyle === option ? 'bg-slate-700 text-yellow-300 font-extrabold' : 'bg-slate-800 hover:bg-slate-700'}
${index === 0 ? 'rounded-l-md' : ''}
${index === cardStyleOptions.length - 1 ? 'rounded-r-md' : ''}
${index !== 0 && 'border-l border-gray-600'}
border border-gray-600
border border-yellow-500 hover:text-yellow-300
`}
>
<input
@@ -98,8 +100,28 @@ export default function PermissionTogglePanel({
))}
</div>
</fieldset>
);
return (
<div className={`fixed top-4 right-4 z-25 ${cinzel.className}`}>
<Scrim
clickAction={() => setOpen((prev) => !prev)}
className={`transition-all duration-250 ${open ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
>
<div
className={`fixed top-4 right-4 flex flex-col items-center justify-center bg-slate-800 text-yellow-400 rounded-lg border border-yellow-400 p-6 space-y-2 transition-all duration-250 ${open ? 'opacity-100 w-[350px] h-[300px]' : 'opacity-0 w-0 h-0'}`}
>
<Links />
<Permissions />
<CardStyle />
</div>
)}
</Scrim>
<button
className={`p-2 transition-all duration-250 text-yellow-400 hover:text-yellow-300 cursor-pointer ${open ? 'pointer-events-none opacity-0' : 'pointer-events-auto opacity-100'}`}
onClick={() => setOpen((prev) => !prev)}
>
<Gear className="w-5 h-5" />
</button>
</div>
);
}

View File

@@ -6,20 +6,20 @@ export interface SwitchProps {
export default function Switch({ label, value, toggleAction }: SwitchProps) {
return (
<label className="flex items-center justify-between w-full gap-2 cursor-pointer">
<label className="flex items-center justify-between w-full gap-2 cursor-pointer text-yellow-400 hover:text-yellow-300">
<span className="text-sm capitalize">{label}</span>
<div className="relative inline-block w-8 h-4 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" checked={value} onChange={toggleAction} className="sr-only" />
<div
className={`block w-8 h-4 rounded-full transition ${
value ? 'bg-gray-500' : 'bg-gray-600'
value ? 'bg-slate-500' : 'bg-slate-600'
}`}
/>
<div
className={`absolute top-[2px] left-[2px] w-3 h-3 rounded-full transition-transform duration-200 ease-out transform
${value ? 'translate-x-4 scale-110 shadow-[0_0_2px_2px_rgba(255,255,255,0.4)]' : 'scale-95'}
${value ? 'bg-gray-100' : 'bg-gray-400'}`}
className={`absolute top-[2px] left-[2px] w-3 h-3 rounded-full transition-all duration-250 ease-out transform
${value ? 'translate-x-4 scale-110' : 'scale-95'}
${value ? 'bg-yellow-400' : 'bg-yellow-500'}`}
/>
</div>
</label>

View File

@@ -9,6 +9,7 @@ type TooltipProps = {
offsetX?: number;
offsetY?: number;
edgeBuffer?: number;
className?: string;
};
export default function Tooltip({
@@ -19,6 +20,7 @@ export default function Tooltip({
offsetX = 20,
offsetY = 20,
edgeBuffer = 10,
className,
}: TooltipProps) {
const ttRef = useRef<HTMLDivElement | null>(null);
const [show, setShow] = useState(false);
@@ -67,12 +69,13 @@ export default function Tooltip({
onMouseMove={handleMouseMove}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
className={className}
>
{children}
</div>
<div
ref={ttRef}
className={`fixed max-w-[35vh] pointer-events-none z-50 text-xs bg-black text-white rounded-xl border border-gray-300 px-2 py-1 transition-opacity duration-250 ${content && show ? 'opacity-100' : 'opacity-0'}`}
className={`fixed max-w-[35vh] pointer-events-none z-50 text-xs bg-[#1e293b] rounded-lg border border-yellow-500 px-2 py-1 transition-opacity duration-250 ${content && show ? 'opacity-100' : 'opacity-0'}`}
style={{
top: `${pos.y + offsetY}px`,
left: `${pos.x + offsetX}px`,

View File

@@ -6,7 +6,7 @@
"dev": "nodemon",
"build": "next build && tsc --project tsconfig.server.json",
"start": "cross-env NODE_ENV=production TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/server.js",
"deploy": "docker buildx build --platform linux/amd64 -t nasty.mcmorgans:5000/tarokka --push ."
"deploy": "docker buildx build --platform linux/amd64 -t 192.168.0.2:5000/tarokka --push ."
},
"dependencies": {
"cross-env": "^7.0.3",

View File

@@ -19,15 +19,15 @@ export default function getTooltip(
if (isHighCard(card)) {
// High deck ally
if (position.id === 'ally') {
if (dm) text.push(`Ally: ${card.prophecy.allies[0].ally}`);
if (dm) text.push(card.prophecy.allies[0].dmText);
if (dm || settings.prophecy) text.push(card.prophecy.allies[0].playerText);
if (dm) text.push(card.prophecy.allies[0].dmText);
if (dm) text.push(`Ally: ${card.prophecy.allies[0].ally}`);
}
// High deck Strahd
if (position.id === 'strahd') {
if (dm) text.push(card.prophecy.strahd.dmText);
if (dm || settings.prophecy) text.push(card.prophecy.strahd.playerText);
if (dm) text.push(card.prophecy.strahd.dmText);
}
}