Added ui
This commit is contained in:
25
ui/src/api/index.ts
Normal file
25
ui/src/api/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
const serviceHost = process.env.SERVICE_HOST || 'http://localhost';
|
||||
const servicePort = process.env.SERVICE_PORT || 5000;
|
||||
|
||||
export async function getRequest(endpoint: string, params: any): Promise<AxiosResponse<any, any> | undefined> {
|
||||
const response = await axios
|
||||
.get(`${serviceHost}:${servicePort}/${endpoint}`, { params })
|
||||
.catch((error) => console.error(error));
|
||||
return response || undefined;
|
||||
}
|
||||
|
||||
export async function postRequest(endpoint: string, body: any): Promise<AxiosResponse<any, any> | undefined> {
|
||||
const response = await axios
|
||||
.post(`${serviceHost}:${servicePort}/${endpoint}`, { body })
|
||||
.catch((error) => console.error(error));
|
||||
return response || undefined;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
limit: number;
|
||||
page: number;
|
||||
pages: number;
|
||||
total: number;
|
||||
}
|
||||
37
ui/src/api/spells.ts
Normal file
37
ui/src/api/spells.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getRequest } from '.';
|
||||
import { GetSpellsResponse } from './spells.types';
|
||||
|
||||
interface GetSpellsParams {
|
||||
name?: string;
|
||||
schools?: string[];
|
||||
levels?: number[];
|
||||
ritual?: boolean;
|
||||
concentration?: boolean;
|
||||
classes?: string[];
|
||||
damage_inflict?: string[];
|
||||
damage_resist?: string[];
|
||||
conditions?: string[];
|
||||
saving_throw?: string[];
|
||||
attack_type?: string[];
|
||||
limit?: number;
|
||||
page?: number;
|
||||
}
|
||||
|
||||
export async function getSpells(params?: GetSpellsParams): Promise<GetSpellsResponse> {
|
||||
const response = await getRequest('spells', {
|
||||
name: params?.name,
|
||||
schools: params?.schools?.join(','),
|
||||
levels: params?.levels?.join(','),
|
||||
ritual: params?.ritual,
|
||||
concentration: params?.concentration,
|
||||
classes: params?.classes?.join(','),
|
||||
damage_inflict: params?.damage_inflict?.join(','),
|
||||
damage_resist: params?.damage_resist?.join(','),
|
||||
conditions: params?.conditions?.join(','),
|
||||
saving_throw: params?.saving_throw?.join(','),
|
||||
attack_type: params?.attack_type?.join(','),
|
||||
limit: params?.limit,
|
||||
page: params?.page
|
||||
});
|
||||
return response?.data || { data: [] };
|
||||
}
|
||||
82
ui/src/api/spells.types.ts
Normal file
82
ui/src/api/spells.types.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Metadata } from '.';
|
||||
|
||||
export interface Spell {
|
||||
id: string;
|
||||
name: string;
|
||||
school: string;
|
||||
level: number;
|
||||
ritual: boolean;
|
||||
casting_time: CastingTime;
|
||||
saving_throw?: string[];
|
||||
attack_type?: string;
|
||||
damage_inflict?: string[];
|
||||
damage_resist?: string[];
|
||||
conditions?: string[];
|
||||
range: Range;
|
||||
area?: Area;
|
||||
components: Components;
|
||||
durations: Duration[];
|
||||
classes: string[];
|
||||
sources: Source[];
|
||||
tags?: string[];
|
||||
description?: Description;
|
||||
}
|
||||
|
||||
export interface CastingTime {
|
||||
value: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
type: string;
|
||||
value?: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface Area {
|
||||
type: string;
|
||||
value?: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface Components {
|
||||
verbal: boolean;
|
||||
somatic: boolean;
|
||||
material: boolean;
|
||||
materials_needed?: string;
|
||||
materials_cost?: number;
|
||||
materials_consumed?: boolean;
|
||||
}
|
||||
|
||||
export interface Duration {
|
||||
type: string;
|
||||
value?: number;
|
||||
unit?: string;
|
||||
// concentration: boolean;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
source: string;
|
||||
page?: number;
|
||||
}
|
||||
|
||||
export interface Description {
|
||||
entries: EntryType[];
|
||||
}
|
||||
|
||||
type EntryType = string | Entry;
|
||||
|
||||
export interface Entry {
|
||||
type: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
export interface GetSpellResponse {
|
||||
data: Spell;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export interface GetSpellsResponse {
|
||||
data: Spell[];
|
||||
metadata: Metadata;
|
||||
}
|
||||
5
ui/src/app/backgrounds/page.tsx
Normal file
5
ui/src/app/backgrounds/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
0
ui/src/app/bot/page.tsx
Normal file
0
ui/src/app/bot/page.tsx
Normal file
5
ui/src/app/classes/page.tsx
Normal file
5
ui/src/app/classes/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
5
ui/src/app/feats/page.tsx
Normal file
5
ui/src/app/feats/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
5
ui/src/app/items/page.tsx
Normal file
5
ui/src/app/items/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
40
ui/src/app/layout.tsx
Normal file
40
ui/src/app/layout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import RecoilRootWrapper from '@app/recoil-root-wrapper';
|
||||
import Topbar from '@/components/Topbar';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { Box, MantineProvider } from '@mantine/core';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import 'styles/globals.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Siren',
|
||||
description: ''
|
||||
};
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang='en' className='h-full bg-white'>
|
||||
<head>
|
||||
<title>Siren</title>
|
||||
</head>
|
||||
<body className={`${inter.className} wrapper h-full`}>
|
||||
<RecoilRootWrapper>
|
||||
<MantineProvider>
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<Topbar />
|
||||
<Box p='xl' pt='sm' className='h-full'>
|
||||
{children}
|
||||
</Box>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</RecoilRootWrapper>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
ui/src/app/options/page.tsx
Normal file
5
ui/src/app/options/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
5
ui/src/app/page.tsx
Normal file
5
ui/src/app/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
5
ui/src/app/races/page.tsx
Normal file
5
ui/src/app/races/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return <></>;
|
||||
}
|
||||
8
ui/src/app/recoil-root-wrapper.tsx
Normal file
8
ui/src/app/recoil-root-wrapper.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export default function RecoilRootWrapper({ children }: { children: ReactNode }) {
|
||||
return <RecoilRoot>{children}</RecoilRoot>;
|
||||
}
|
||||
91
ui/src/app/spells/page.tsx
Normal file
91
ui/src/app/spells/page.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { getSpells } from '@/api/spells';
|
||||
import { Spell } from '@/api/spells.types';
|
||||
import SpellModal from '@/components/SpellModal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './spells.css';
|
||||
import { Box, TextInput } from '@mantine/core';
|
||||
import { AiOutlineVerticalAlignTop } from 'react-icons/ai';
|
||||
|
||||
export default function Page() {
|
||||
const [cantrips, setCantrips] = useState<Spell[]>([]);
|
||||
const [level1, setLevel1] = useState<Spell[]>([]);
|
||||
const [level2, setLevel2] = useState<Spell[]>([]);
|
||||
const [level3, setLevel3] = useState<Spell[]>([]);
|
||||
const [level4, setLevel4] = useState<Spell[]>([]);
|
||||
const [level5, setLevel5] = useState<Spell[]>([]);
|
||||
const [level6, setLevel6] = useState<Spell[]>([]);
|
||||
const [level7, setLevel7] = useState<Spell[]>([]);
|
||||
const [level8, setLevel8] = useState<Spell[]>([]);
|
||||
const [level9, setLevel9] = useState<Spell[]>([]);
|
||||
const [activeSpell, setActiveSpell] = useState<Spell | undefined>(undefined);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchName, setSearchName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getSpells({ levels: [0] }).then((s) => setCantrips(s.data));
|
||||
getSpells({ levels: [1] }).then((s) => setLevel1(s.data));
|
||||
getSpells({ levels: [2] }).then((s) => setLevel2(s.data));
|
||||
getSpells({ levels: [3] }).then((s) => setLevel3(s.data));
|
||||
getSpells({ levels: [4] }).then((s) => setLevel4(s.data));
|
||||
getSpells({ levels: [5] }).then((s) => setLevel5(s.data));
|
||||
getSpells({ levels: [6] }).then((s) => setLevel6(s.data));
|
||||
getSpells({ levels: [7] }).then((s) => setLevel7(s.data));
|
||||
getSpells({ levels: [8] }).then((s) => setLevel8(s.data));
|
||||
getSpells({ levels: [9] }).then((s) => setLevel9(s.data));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box style={{ width: '60%', margin: 'auto' }}>
|
||||
<h1>Spells</h1>
|
||||
<TextInput
|
||||
label='Search by name'
|
||||
placeholder='Acid Splash...'
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
style={{ width: '25%' }}
|
||||
/>
|
||||
<SpellSection
|
||||
title='Cantrips'
|
||||
spells={cantrips.filter((s) => s.name.toLowerCase().includes(searchName.toLowerCase()))}
|
||||
onClick={(spell) => {
|
||||
setActiveSpell(spell);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
{activeSpell && <SpellModal spell={activeSpell} isOpen={isOpen} onClose={() => setIsOpen(false)} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SpellSection({ title, spells, onClick }: { title: string; spells: Spell[]; onClick: (spell: Spell) => void }) {
|
||||
const isBrowser = () => typeof window !== 'undefined'; //The approach recommended by Next.js
|
||||
|
||||
function scrollToTop() {
|
||||
if (!isBrowser()) return;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<h2>{title}</h2>
|
||||
<ul>
|
||||
{spells.map((spell) => (
|
||||
<li
|
||||
key={spell.id}
|
||||
className='link spell-item'
|
||||
style={{ width: 'fit-content' }}
|
||||
onClick={() => onClick(spell)}
|
||||
>
|
||||
{spell.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'gray' }} onClick={scrollToTop}>
|
||||
<span style={{ paddingRight: '0.2em' }}>Back to top</span>
|
||||
<AiOutlineVerticalAlignTop />
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
7
ui/src/app/spells/spells.css
Normal file
7
ui/src/app/spells/spells.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.spell-item {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.spell-item:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
153
ui/src/components/SpellModal.tsx
Normal file
153
ui/src/components/SpellModal.tsx
Normal 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>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
57
ui/src/components/Topbar/index.tsx
Normal file
57
ui/src/components/Topbar/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
ui/src/components/Topbar/topbar.css
Normal file
47
ui/src/components/Topbar/topbar.css
Normal 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;
|
||||
}
|
||||
23
ui/src/js/spells.ts
Normal file
23
ui/src/js/spells.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Spell } from '@/api/spells.types';
|
||||
|
||||
export function levelText(spell: Spell) {
|
||||
if (spell.level === 0) {
|
||||
return 'Cantrip';
|
||||
} else {
|
||||
return `Level ${spell.level}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function rollDice(dice: string): number[] {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [count, sides] = dice.split('d');
|
||||
const rolls = [];
|
||||
if (isNaN(parseInt(count))) {
|
||||
count = '1';
|
||||
}
|
||||
for (let i = 0; i < parseInt(count); i++) {
|
||||
rolls.push(Math.floor(Math.random() * parseInt(sides)) + 1);
|
||||
}
|
||||
console.log(rolls);
|
||||
return rolls;
|
||||
}
|
||||
5
ui/src/js/theme.ts
Normal file
5
ui/src/js/theme.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { createTheme } from '@mantine/core';
|
||||
|
||||
export const theme = createTheme({});
|
||||
6
ui/src/js/utils.ts
Normal file
6
ui/src/js/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function capitalize(str: string | undefined): string {
|
||||
if (!str || str.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
Reference in New Issue
Block a user