Compare commits

..

5 Commits

Author SHA1 Message Date
Gavin McDonald
8bf8b4c5cb glowing cards 2025-05-03 16:55:29 -04:00
Gavin McDonald
493891a8e2 Settings tweaks 2025-05-03 16:44:05 -04:00
Gavin McDonald
af70401916 BuyMeACoffee glow 2025-05-03 16:30:40 -04:00
Gavin McDonald
82ccb0f6fb embiggen the spectator link 2025-05-03 16:28:02 -04:00
Gavin McDonald
6a1f1174a3 GitHub and Buy Me a Coffee links, various tweaks 2025-05-02 20:08:04 -04:00
18 changed files with 88 additions and 21 deletions

View File

@@ -2,16 +2,18 @@
**Tarokka** is a real-time Tarokka card reading app for _Dungeons & Dragons: Curse of Strahd_. It simulates Madam Evas fortune-telling, revealing a heros fate and Strahds secrets, and is built to deliver an authentic, immersive experience for DMs and players alike. **Tarokka** is a real-time Tarokka card reading app for _Dungeons & Dragons: Curse of Strahd_. It simulates Madam Evas fortune-telling, revealing a heros fate and Strahds secrets, and is built to deliver an authentic, immersive experience for DMs and players alike.
![screenshot](public/screenshot.png) To be honest, Id say this is overkill for what is a relatively small aspect of _Curse of Strahd_ but I had fun making it. Its a [Next.js](https://nextjs.org/) app running a custom server in order to employ [Socket.IO](https://socket.io/). When a new session is created the DM (dungeon master) is presented a game with the cards shuffled and laid out on the table. There is a link available for sharing the session to any spectators. The DM has access to settings that allow for limiting what info spectators have access to (card purpose, prophecy, notes) and the style of cards used.
![screenshot](public/img/screenshot.png)
You can see it live at: You can see it live at:
👉 [https://tarokka.mcmorgans.us](https://tarokka.mcmorgans.us) 👉 [https://tarokka.app](https://tarokka.app)
--- ---
## ✨ Features ## ✨ Features
- 🔮 **Faithful to the Tarokka Deck**: Supports all cards and positions used by Madam Eva's reading. - 🔮 **Faithful to the Tarokka Deck**: Supports all cards and positions used by Madam Evas reading.
- 💬 Dynamic prophecy rendering based on card and position - 💬 Dynamic prophecy rendering based on card and position
- 🎨 Multiple card styles - 🎨 Multiple card styles
- 🧙 Separate DM and Spectator Views - 🧙 Separate DM and Spectator Views

View File

@@ -96,7 +96,8 @@ export default function GamePage() {
copy={`${location.origin}/${gameData.spectatorID}`} copy={`${location.origin}/${gameData.spectatorID}`}
tooltip={`Spectator link: ${location.origin}/${gameData.spectatorID}`} tooltip={`Spectator link: ${location.origin}/${gameData.spectatorID}`}
Icon={Eye} Icon={Eye}
className={`fixed top-4 left-4 p-2 z-25 transition-all duration-250 text-yellow-400 hover:text-yellow-300 cursor-pointer`} className={`fixed top-3 left-3 p-2 z-25 transition-all duration-250 text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] cursor-pointer`}
size={24}
/> />
)} )}

View File

@@ -0,0 +1,17 @@
'use client';
type BuyMeACoffeeProps = {
className?: string;
};
export default function BuyMeACoffee({ className = '' }: BuyMeACoffeeProps) {
return (
<a
href="https://www.buymeacoffee.com/mcdoh"
className={`transition hover:drop-shadow-[0_0_3px_#ffd700] ${className}`}
target="_blank"
>
<img src="/img/bmc-button.svg" alt="Buy Me A Coffee" className="h-full" />
</a>
);
}

View File

@@ -54,14 +54,14 @@ export default function Card({ dm, card, position, settings, flipAction }: CardP
<img <img
src={getURL(cardBack as TarokkaGameCard, settings)} src={getURL(cardBack as TarokkaGameCard, settings)}
alt="Card Back" alt="Card Back"
className="rounded-lg border border-yellow-500" className="rounded-lg border border-yellow-500 hover:drop-shadow-[0_0_2px_#ffd700]"
/> />
</div> </div>
<div className="absolute group inset-0 backface-hidden rotate-y-180"> <div className="absolute group inset-0 backface-hidden rotate-y-180">
<img <img
src={getURL(card, settings)} src={getURL(card, settings)}
alt={aria} alt={aria}
className="rounded-lg border border-yellow-500 " className="rounded-lg border border-yellow-500 hover:drop-shadow-[0_0_2px_#ffd700]"
/> />
</div> </div>
</div> </div>

View File

@@ -11,6 +11,7 @@ type CopyButtonProps = {
Icon?: ForwardRefExoticComponent<Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>>; Icon?: ForwardRefExoticComponent<Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>>;
tooltip?: string | string[]; tooltip?: string | string[];
className?: string; className?: string;
size?: number;
}; };
export default function CopyButton({ export default function CopyButton({
@@ -19,6 +20,7 @@ export default function CopyButton({
Icon = CopyIcon, Icon = CopyIcon,
tooltip = ['Copy', 'Copied'], tooltip = ['Copy', 'Copied'],
className, className,
size = 16,
}: CopyButtonProps) { }: CopyButtonProps) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -44,9 +46,9 @@ export default function CopyButton({
<div className="flex items-center gap-2 w-full text-sm font-medium"> <div className="flex items-center gap-2 w-full text-sm font-medium">
{title} {title}
{copied ? ( {copied ? (
<CheckIcon className="ml-auto" size={16} /> <CheckIcon className="ml-auto" size={size} />
) : ( ) : (
<Icon className="ml-auto" size={16} /> <Icon className="ml-auto" size={size} />
)} )}
</div> </div>
</ToolTip> </ToolTip>

View File

@@ -0,0 +1,21 @@
'use client';
type GitHubButtonProps = {
className?: string;
};
export default function GitHubButton({ className = '' }: GitHubButtonProps) {
return (
<a
href="https://github.com/mcdoh/tarokka"
className={className}
target="_blank"
rel="noopener noreferrer"
>
<button className="h-full w-full flex flex-row justify-center items-center gap-3 bg-slate-700 rounded-[6px] hover:bg-slate-600 transition cursor-pointer">
<img src="/img/github-mark-white.svg" alt="GitHub" className="h-[22px] w-[22px]" />
<img src="/img/github-logo-white.png" alt="GitHub" className="h-[22px]" />
</button>
</a>
);
}

View File

@@ -42,7 +42,7 @@ export default function Notes({ gameData: { dmID, cards, settings }, show }: Not
className={`fixed bottom-4 right-4 z-25 transition-all duration-250 ${show ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`} className={`fixed bottom-4 right-4 z-25 transition-all duration-250 ${show ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
> >
<button <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'}`} className={`text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] p-2 transition-all duration-250 cursor-pointer ${showNotes ? 'pointer-events-none opacity-0' : 'pointer-events-auto opacity-100'}`}
onClick={() => setOpen((prev) => !prev)} onClick={() => setOpen((prev) => !prev)}
> >
<ScrollText className="w-5 h-5" /> <ScrollText className="w-5 h-5" />
@@ -53,7 +53,7 @@ export default function Notes({ gameData: { dmID, cards, settings }, show }: Not
className={`transition-all duration-250 ${showNotes ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`} className={`transition-all duration-250 ${showNotes ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
> >
<div <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'}`} className={`fixed bottom-4 right-4 transition-all duration-250 bg-slate-800 border border-yellow-400 rounded-lg space-y-2 ${showNotes ? 'sm:w-[33vw] sm:h-[67vh] w-[80vw] h-[80vh]' : 'w-0 h-0'}`}
> >
<CopyButton <CopyButton
copy={notes.map((note) => note!.join('\n')).join('\n\n')} copy={notes.map((note) => note!.join('\n')).join('\n\n')}

View File

@@ -4,7 +4,9 @@ import { useState } from 'react';
import { Settings as Gear } from 'lucide-react'; import { Settings as Gear } from 'lucide-react';
import { Cinzel_Decorative } from 'next/font/google'; import { Cinzel_Decorative } from 'next/font/google';
import BuyMeACoffee from '@/components/BuyMeACoffee';
import CopyButton from '@/components/CopyButton'; import CopyButton from '@/components/CopyButton';
import GitHubButton from '@/components/GitHubButton';
import Scrim from '@/components/Scrim'; import Scrim from '@/components/Scrim';
import Switch from '@/components/Switch'; import Switch from '@/components/Switch';
import { CardStyle, GameUpdate } from '@/types'; import { CardStyle, GameUpdate } from '@/types';
@@ -73,18 +75,18 @@ export default function Settings({ gameData, changeAction }: SettingsProps) {
); );
const CardStyle = () => ( const CardStyle = () => (
<fieldset className="flex flex-col"> <fieldset className="flex flex-col w-full">
<div className="text-xs mb-1">Card style:</div> <div className="text-xs my-1">Card style:</div>
<div className="inline-flex overflow-hidden rounded-md w-full"> <div className="inline-flex overflow-hidden rounded-md w-full">
{cardStyleOptions.map((option, index) => ( {cardStyleOptions.map((option, index) => (
<label <label
key={option} key={option}
className={`cursor-pointer px-4 py-2 text-sm font-medium transition className={`flex justify-center items-center cursor-pointer w-full px-4 py-2 text-sm font-medium transition
${gameData.settings.cardStyle === option ? 'bg-slate-700 text-yellow-300 font-extrabold' : 'bg-slate-800 hover:bg-slate-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 === 0 ? 'rounded-l-md' : ''}
${index === cardStyleOptions.length - 1 ? 'rounded-r-md' : ''} ${index === cardStyleOptions.length - 1 ? 'rounded-r-md' : ''}
${index !== 0 && 'border-l border-gray-600'} ${index !== 0 && 'border-l border-gray-600'}
border border-yellow-500 hover:text-yellow-300 border border-yellow-500 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700]
`} `}
> >
<input <input
@@ -109,15 +111,19 @@ export default function Settings({ gameData, changeAction }: SettingsProps) {
className={`transition-all duration-250 ${open ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`} className={`transition-all duration-250 ${open ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
> >
<div <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'}`} className={`fixed top-4 right-4 flex flex-col items-center justify-evenly bg-slate-800 text-yellow-400 rounded-lg border border-yellow-400 py-3 px-4 transition-all duration-250 ${open ? 'opacity-100 w-[350px] h-[350px]' : 'opacity-0 w-0 h-0'}`}
> >
<Links /> <Links />
<Permissions /> <Permissions />
<CardStyle /> <CardStyle />
<span className="w-full flex flex-row justify-evenly">
<GitHubButton className="h-[35px] w-[125px]" />
<BuyMeACoffee className="h-[35px] w-[125px]" />
</span>
</div> </div>
</Scrim> </Scrim>
<button <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'}`} className={`p-2 transition-all duration-250 text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] cursor-pointer ${open ? 'pointer-events-none opacity-0' : 'pointer-events-auto opacity-100'}`}
onClick={() => setOpen((prev) => !prev)} onClick={() => setOpen((prev) => !prev)}
> >
<Gear className="w-5 h-5" /> <Gear className="w-5 h-5" />

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

22
public/img/bmc-button.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B