This commit is contained in:
Benjamin Sherriff
2023-10-05 09:07:53 -04:00
parent ac17be838a
commit 1b41849115
54 changed files with 6473 additions and 129 deletions

View File

@@ -0,0 +1,153 @@
'use client';
import { Spell } from '@/api/spells.types';
import { levelText, rollDice } from '@/js/spells';
import { capitalize } from '@/js/utils';
import { Grid, Modal } from '@mantine/core';
import { notifications } from '@mantine/notifications';
interface SpellModalProps {
spell: Spell;
isOpen: boolean;
onClose(): void;
}
export default function SpellModal({ spell, isOpen, onClose }: SpellModalProps) {
return (
<Modal opened={isOpen} onClose={onClose} withCloseButton={false} size={'50%'} className='modal'>
<h1 style={{ padding: '0', margin: '0' }}>{spell.name}</h1>
<Grid gutter={1}>
<Grid.Col span={4} style={{ paddingBottom: '1rem' }}>
<span style={{ fontWeight: 'bold' }}>
{capitalize(spell.school)} {levelText(spell)}
</span>
</Grid.Col>
<Grid.Col span={8} style={{ paddingBottom: '1rem' }}>
<div style={{ float: 'right' }}>
<span style={{ float: 'right' }}>
{spell.components.verbal && spell.components.somatic ? 'V, ' : 'V '}
{spell.components.somatic && spell.components.material ? 'S, ' : 'S '}
{spell.components.material && spell.components.materials_needed ? 'M*' : 'M'}
</span>
{spell.components.materials_needed && (
<span style={{ fontSize: '0.8em', color: 'gray' }}>
<br />*{capitalize(spell.components.materials_needed)}
</span>
)}
</div>
</Grid.Col>
<Grid.Col span={6}>
<span style={{ fontWeight: 'bold', paddingRight: '1em' }}>Sources:</span>
{spell.sources.map((s) => (
<span style={{ paddingRight: '0.6em' }}>
{s.source}
{s.page ? `.${s.page}` : ''}
</span>
))}
</Grid.Col>
<Grid.Col span={6}>
<span style={{ fontWeight: 'bold', marginRight: '1em' }}>Classes:</span>
<span style={{ overflowWrap: 'break-word' }}>
{spell.classes.map((c) => (
<span style={{ paddingRight: '0.6em', display: 'inline-block' }} className='link'>
{capitalize(c)}
</span>
))}
</span>
</Grid.Col>
<Grid.Col span={6}>
<span style={{ fontWeight: 'bold', paddingRight: '1em' }}>Casting Time:</span>
<span style={{ paddingRight: '0.6em' }}>
{spell.casting_time.value} {capitalize(spell.casting_time.unit)}
</span>
</Grid.Col>
<Grid.Col span={6}>
<span style={{ fontWeight: 'bold', paddingRight: '1em' }}>Range:</span>
<span style={{ paddingRight: '0.6em' }}>
{spell.range.type != 'point' && capitalize(spell.range.type)} {spell.range.value}{' '}
{capitalize(spell.range.unit)}
</span>
</Grid.Col>
<Grid.Col span={6}>
<span style={{ fontWeight: 'bold', paddingRight: '1em' }}>Duration:</span>
<span style={{ paddingRight: '0.6em' }}>
{spell.durations.map((d) => (
<span>
{capitalize(d.type)} {d.value} {capitalize(d.unit)}
</span>
))}
</span>
</Grid.Col>
<Grid.Col span={12}>
<SpellDescription spell={spell} />
</Grid.Col>
</Grid>
</Modal>
);
}
function SpellDescription({ spell }: { spell: Spell }) {
function parseText(text: string) {
const regex = /{@(.*?) (.*?)}/g;
const matches = text.matchAll(regex);
const result = [];
let lastIndex = 0;
for (const match of matches) {
const [full, type, name] = match;
result.push(text.slice(lastIndex, match.index));
if (match.index !== undefined) {
result.push(
<span onClick={() => handleLink(type, name)} className='link'>
{name}
</span>
);
lastIndex = match.index + full.length;
}
}
result.push(text.slice(lastIndex));
return result;
}
function handleLink(type: string, name: string) {
if (type == 'spell') {
console.log(`Link to spell: ${name}`);
} else if (type == 'dice' || type == 'damage') {
const rolls = rollDice(name);
notifications.show({
title: `Rolling ${name}`,
message: `${rolls.join(' + ')} = ${rolls.reduce((a, b) => a + b, 0)}`,
color: 'blue',
autoClose: 5000,
withCloseButton: false
});
} else {
console.error(`Unknown link type: ${type}`);
}
}
return (
<>
{spell.description && (
<>
{spell.description.entries.map((e) =>
typeof e === 'string' ? (
<p>{parseText(e)}</p>
) : (
<>
{e.type == 'list' ? (
<ul>
{e.items.map((text) => (
<li>{parseText(text)}</li>
))}
</ul>
) : (
<></>
)}
</>
)
)}
</>
)}
</>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import './topbar.css';
const headerItems = [
{
name: 'Races',
link: '/races'
},
{
name: 'Classes',
link: '/classes'
},
{
name: 'Feats',
link: '/feats'
},
{
name: 'Options & Features',
link: '/options'
},
{
name: 'Backgrounds',
link: '/backgrounds'
},
{
name: 'Items',
link: '/items'
},
{
name: 'Spells',
link: '/spells'
}
];
export default function Topbar() {
const pathName = usePathname();
return (
<nav className='navbar'>
<div className='left'>
<Link href={'/'} className='title'>
Siren
</Link>
<div className='header-items'>
{headerItems.map((item) => (
<Link className={`header-item ${pathName == item.link && 'active'}`} href={item.link} key={item.name}>
{item.name}
</Link>
))}
</div>
</div>
</nav>
);
}

View File

@@ -0,0 +1,47 @@
.navbar {
display: flex;
justify-content: space-between;
color: black;
border-bottom: 1px solid #e6e6e6;
}
.navbar .left {
display: flex;
}
.navbar .title {
padding-left: 2em;
padding-right: 2em;
margin: auto;
font-size: x-large;
}
.navbar .left .search {
margin: auto;
}
.navbar .avatar {
padding-right: 2em;
margin-top: auto;
margin-bottom: auto;
}
.header-items {
display: flex;
justify-content: space-between;
}
.header-items .header-item {
padding-left: 2em;
padding-right: 2em;
margin: auto;
border-bottom: 2px solid transparent;
}
.header-items .header-item:hover {
border-bottom: 2px solid #e6e6e6;
}
.header-items .active {
border-bottom: 2px solid #5f5f5f;
}