├── .env.template ├── .eslintrc.json ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── about.md ├── admin.md ├── editor.md ├── installing.md ├── obs.md ├── settings.md ├── sheet.md └── update.md ├── global.d.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── avatar404.png ├── dice10.webp ├── dice12.webp ├── dice20.webp ├── dice4.webp ├── dice6.webp ├── dice8.webp ├── dice_animation.webm └── favicon.ico ├── src ├── components │ ├── Admin │ │ ├── AdminEnvironmentConfigurations.tsx │ │ ├── AdminUtilityContainer.tsx │ │ ├── AvatarField.tsx │ │ ├── CombatContainer.tsx │ │ ├── DiceList.tsx │ │ ├── Editor │ │ │ ├── AttributeEditorContainer.tsx │ │ │ ├── CharacteristicEditorContainer.tsx │ │ │ ├── CurrencyEditorContainer.tsx │ │ │ ├── EditorContainer.tsx │ │ │ ├── EquipmentEditorContainer.tsx │ │ │ ├── ExtraInfoEditorContainer.tsx │ │ │ ├── InfoEditorContainer.tsx │ │ │ ├── ItemEditorContainer.tsx │ │ │ ├── SkillEditorContainer.tsx │ │ │ ├── SpecEditorContainer.tsx │ │ │ └── SpellEditorContainer.tsx │ │ ├── NPCContainer.tsx │ │ ├── PlayerManager.tsx │ │ ├── PlayerPortraitButton.tsx │ │ └── WelcomePage.tsx │ ├── ApplicationHead.tsx │ ├── BottomTextInput.tsx │ ├── CustomSpinner.tsx │ ├── DataContainer.tsx │ ├── ErrorToast.tsx │ ├── ErrorToastContainer.tsx │ ├── Modals │ │ ├── AddDataModal.tsx │ │ ├── AttributeEditorModal.tsx │ │ ├── AttributeStatusEditorModal.tsx │ │ ├── CharacteristicEditorModal.tsx │ │ ├── CurrencyEditorModal.tsx │ │ ├── DiceRollModal.tsx │ │ ├── EquipmentEditorModal.tsx │ │ ├── ExtraInfoEditorModal.tsx │ │ ├── GeneralDiceRollModal.tsx │ │ ├── GetPortraitModal.tsx │ │ ├── InfoEditorModal.tsx │ │ ├── ItemEditorModal.tsx │ │ ├── PlayerAttributeEditorModal.tsx │ │ ├── PlayerAvatarModal.tsx │ │ ├── PlayerTradeModal.tsx │ │ ├── SheetModal.tsx │ │ ├── SkillEditorModal.tsx │ │ ├── SpecEditorModal.tsx │ │ ├── SpecializationEditorModal.tsx │ │ └── SpellEditorModal.tsx │ ├── Navbar.tsx │ ├── Player │ │ ├── PlayerAnnotationField.tsx │ │ ├── PlayerAttributeContainer.tsx │ │ ├── PlayerCharacteristicContainer.tsx │ │ ├── PlayerEquipmentContainer.tsx │ │ ├── PlayerExtraInfoField.tsx │ │ ├── PlayerInfoContainer.tsx │ │ ├── PlayerItemContainer.tsx │ │ ├── PlayerSkillContainer.tsx │ │ ├── PlayerSpecField.tsx │ │ └── PlayerSpellContainer.tsx │ └── Portrait │ │ ├── PortraitAvatarContainer.tsx │ │ ├── PortraitDiceContainer.tsx │ │ ├── PortraitEnvironmentalContainer.tsx │ │ └── PortraitSideAttributeContainer.tsx ├── contexts │ └── index.ts ├── hooks │ ├── useAuthentication.ts │ ├── useDiceRoll.ts │ ├── useExtendedState.ts │ ├── useSocket.ts │ └── useToast.ts ├── pages │ ├── 404.tsx │ ├── 500.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── admin │ │ ├── configurations.tsx │ │ ├── editor.tsx │ │ └── main.tsx │ ├── api │ │ ├── config.ts │ │ ├── dice.ts │ │ ├── init.ts │ │ ├── login.ts │ │ ├── player.ts │ │ ├── register.ts │ │ ├── sheet │ │ │ ├── attribute │ │ │ │ ├── index.ts │ │ │ │ ├── portrait.ts │ │ │ │ └── status.ts │ │ │ ├── characteristic.ts │ │ │ ├── currency.ts │ │ │ ├── equipment.ts │ │ │ ├── extrainfo.ts │ │ │ ├── info.ts │ │ │ ├── item.ts │ │ │ ├── npc.ts │ │ │ ├── player │ │ │ │ ├── annotation.ts │ │ │ │ ├── attribute │ │ │ │ │ ├── index.ts │ │ │ │ │ └── status.ts │ │ │ │ ├── avatar │ │ │ │ │ ├── [attrStatusID].ts │ │ │ │ │ └── index.ts │ │ │ │ ├── characteristic.ts │ │ │ │ ├── currency.ts │ │ │ │ ├── equipment.ts │ │ │ │ ├── extrainfo.ts │ │ │ │ ├── index.ts │ │ │ │ ├── info.ts │ │ │ │ ├── item.ts │ │ │ │ ├── skill │ │ │ │ │ ├── clearchecks.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── spec.ts │ │ │ │ ├── spell.ts │ │ │ │ └── trade │ │ │ │ │ ├── equipment.ts │ │ │ │ │ └── item.ts │ │ │ ├── skill.ts │ │ │ ├── spec.ts │ │ │ ├── specialization.ts │ │ │ └── spell.ts │ │ └── socket.ts │ ├── index.tsx │ ├── portrait │ │ └── [characterID].tsx │ ├── register │ │ ├── admin.tsx │ │ └── index.tsx │ └── sheet │ │ ├── npc │ │ └── [id] │ │ │ ├── 1.tsx │ │ │ └── 2.tsx │ │ └── player │ │ ├── 1.tsx │ │ └── 2.tsx ├── prisma │ ├── migrations │ │ ├── 20220329205331_init_migration │ │ │ └── migration.sql │ │ ├── 20220403144231_playeravatar_relation_cascade │ │ │ └── migration.sql │ │ ├── 20220415005955_player_skill_checked_added │ │ │ └── migration.sql │ │ ├── 20220506183920_skill_start_value_added │ │ │ └── migration.sql │ │ ├── 20220507181703_player_attribute_show_added │ │ │ └── migration.sql │ │ ├── 20220520225933_player_name_added │ │ │ └── migration.sql │ │ ├── 20220530194625_skill_modifier_add │ │ │ └── migration.sql │ │ ├── 20220617194737_npc_role_added │ │ │ └── migration.sql │ │ ├── 20220617224442_player_portrait_attribute_enum_added │ │ │ └── migration.sql │ │ ├── 20220618203220_visible_to_admin_option_added │ │ │ └── migration.sql │ │ ├── 20220621212142_trade_added │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── styles │ ├── globals.scss │ └── modules │ │ ├── Home.module.scss │ │ └── Portrait.module.scss └── utils │ ├── api.ts │ ├── config.ts │ ├── database.ts │ ├── dice.ts │ ├── encryption.ts │ ├── index.ts │ ├── session.ts │ ├── socket.ts │ └── style.ts └── tsconfig.json /.env.template: -------------------------------------------------------------------------------- 1 | SESSION_SECRET=Hashed Password 2 | CLEARDB_DATABASE_URL=mysql://USERNAME:PASSWORD@HOST:PORT/DATABASE_NAME -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "semi": [2, "always"], 5 | "quotes": [2, "single", "avoid-escape"], 6 | "unused-imports/no-unused-imports": "warn" 7 | }, 8 | "plugins": ["unused-imports"] 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: alyssafernandes 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | *.tsbuildinfo 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | /dist 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # migrations 41 | *.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alyssa Pires Fernandes (alyssapiresfernandescefet) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ATENÇÃO: Projeto Descontinuado 2 | 3 | O Open RPG foi um sucesso, mas a comunidade necessita de algo a mais. Por isso, trago a vocês o [Rollify Beyond](https://rollifybeyond.com). 4 | 5 | ![Rollify Beyond Logo](https://rollifybeyond.com/rollify_white.png) 6 | 7 | Sucessor do Open RPG e Rollify, o Rollify Beyond combina o melhor dos meus projetos até agora. 8 | 9 | O Open RPG e Rollify permanecerão disponíveis, porém não serão mais atualizados e, com o tempo, se tornarão obsoletos. 10 | 11 | Espero vê-lo a bordo do Rollify Beyond! 12 | 13 | # Open RPG: Ficha eletrônica de RPG 14 | 15 | Open RPG é um aplicativo web gratuito que permite o uso de fichas eletrônicas automatizadas e customizáveis para qualquer campanha, bem como utilitários que ajudam o mestre a agilizar tarefas. 16 | 17 | ## Documentação 18 | 19 | - [Criando seu próprio site com o Open RPG](./docs/installing.md) 20 | - [Utilizando a ficha eletrônica do jogador](./docs/sheet.md) 21 | - [Utilizando o painel do mestre](./docs/admin.md) 22 | - [Integrando o Open RPG com o OBS](./docs/obs.md) 23 | - [Modificando a ficha eletrônica](./docs/editor.md) 24 | - [Acessando as configurações de sistema](./docs/settings.md) 25 | - [Mantendo seu Open RPG atualizado](./docs/update.md) 26 | - [Sobre](./docs/about.md) 27 | 28 | ## Introdução 29 | 30 | Atualmente, o Open RPG possui várias funções e recursos tanto para os mestres quanto para os jogadores, sendo elas: 31 | 32 | ### Ficha eletrônica do jogador: 33 | 34 | - Informações Pessoais 35 | - Barras de Atributos, Estados e Avatar 36 | - Características (ou atributos secundários) 37 | - Combate e Equipamentos 38 | - Perícias 39 | - Itens e capacidade de carga 40 | - Moeda 41 | - Magias 42 | - Anotações pessoais 43 | - Rolagem de dados automática 44 | 45 | ### Painel do mestre: 46 | 47 | - Painel de jogadores (com monitoramento de recursos dos personagens) 48 | - Rolagem Rápida 49 | - Combate e Iniciativa 50 | - Histórico de rolagem dos jogadores 51 | - Gerenciamento de NPCs genéricos 52 | - Anotações pessoais 53 | - Editor da ficha eletrônica do jogador 54 | - Configurações do sistema 55 | 56 | ## Integração com OBS 57 | 58 | Além das funcionalidades base, Open RPG possui integração com o OBS através de Browser Sources. Os recursos são: 59 | 60 | - Avatar 61 | - Nome 62 | - Atributos 63 | - Rolagem de dados 64 | 65 | ## Status do projeto 66 | 67 | Atualmente, o Open RPG se encontra em sua versão 1.0.0 Beta e já é considerado completo. No entanto, ainda há a possibilidade de adicionar novas funcionalidades de acordo a demanda dos usuários. 68 | 69 | ## Comunidade 70 | 71 | Para resolução de problemas e bugs, utilize os [Issues](https://github.com/alyssapiresfernandescefet/openrpg/issues). 72 | 73 | Para perguntas ou dúvidas, utilize as [Discussions](https://github.com/alyssapiresfernandescefet/openrpg/discussions). 74 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | ## O que é o Open RPG 2 | 3 | Open RPG é um aplicativo web gratuito que permite o uso de fichas eletrônicas customizáveis para qualquer campanha, bem como utilitários que ajudam o mestre a agilizar algumas tarefas. 4 | 5 | ## Como eu uso o Open RPG? 6 | 7 | O Open RPG é um aplicativo web, ou seja, precisa ser instalado em um provedor para ser usado. Para mais instruções de criar um site com o Open RPG, acesse [aqui](./installing.md). 8 | 9 | ## Por que usar o Open RPG? 10 | 11 | O Open RPG oferece uma solução para todos os jogadores de RPG, com uma UI amigável e uma ficha eletrônica poderosa. É gratuito e possui várias funcionalidades para tirar muito do peso do mestre quando se narra uma história de RPG. Com o Open RPG, você pode usar a extensão do OBS para streamar e compartilhar com seus espectadores a experiência do jogo. 12 | 13 | ## Posso usar o Open RPG com o Tabletop Simulator? 14 | 15 | Você pode usar o Open RPG com qualquer outro aplicativo para aumentar ainda mais a imersão no jogo. Aplicativos como [Tabletop Simulator](https://store.steampowered.com/app/286160/Tabletop_Simulator/), [Owlbear Rodeo](https://www.owlbear.rodeo/) e [Kenku FM](https://www.kenku.fm/) são ótimas ferramentas para se usar em conjunto com o Open RPG. 16 | 17 | ## Sempre quando eu vou fazer o login, a tela fica preta e carregando eternamente! Como resolver esse erro? 18 | 19 | Esse erro é o mais comum quando se usa o Open RPG pelo celular. Esse erro acontece porque o Open RPG só suporta conexões seguras (HTTPS). Para resolvê-lo, você deve ativar o uso de conexões seguras no seu dispositivo. Na maioria dos navegadores mobile, essa configuração não está ativada por padrão, e você deve fazê-la no menu de configurações do navegador. 20 | 21 | ## Quem é você? 22 | 23 | Eu sou Alyssa Fernandes, uma técnica de Informática e desenvolvedora do Open RPG. Sou apaixonada por jogos no geral e sempre estou produzindo novos conteúdos gratuitos para a comunidade. 24 | 25 | ## Gostei do Open RPG, mas ele não atende a minha demanda. 26 | 27 | Eu também faço comissões de fichas eletrônicas de RPG. Para mais informações, me mande um e-mail com o título "Ficha Eletrônica de RPG - Comissão". 28 | 29 | ## Eu gostaria de entrar em contato com você. 30 | 31 | Sinta-se à vontade para abrir um post nas [Discussões](https://github.com/alyssapiresfernandescefet/openrpg/discussions) ou me enviar um e-mail para [alyssapiresfernandes@gmail.com](mailto:alyssapiresfernandes@gmail.com). 32 | 33 | ## Roadmap do Open RPG 34 | 35 | Atualmente, o Open RPG já possui uma estrutura sólida para uso. No entanto, ainda há abertura para a implementação de novas funcionalidades, como: 36 | 37 | - Carregar fontes customizadas para o retrato do jogador. 38 | - Melhorar o modo claro do website. 39 | -------------------------------------------------------------------------------- /docs/admin.md: -------------------------------------------------------------------------------- 1 | # Utilizando o painel do mestre 2 | 3 | Nessa seção, ensinarei tudo o que você precisa saber sobre o painel do mestre. 4 | 5 | ## Criando uma Conta de Mestre 6 | 7 | Primeiramente, para acessar os recursos do mestre, é necessário criar uma conta de mestre. Para criar, acesse a página de cadastro e clique em "Cadastrar-se como mestre". 8 | 9 | Para se cadastrar como mestre, você irá precisar de uma chave especial chamada "chave do mestre". Se você é o primeiro mestre a criar uma conta, então a chave do mestre é desabilitada. No entanto, caso o sistema encontre uma conta de mestre já cadastrada, ele irá exigir a chave. 10 | 11 | A chave do mestre padrão é "123456". 12 | 13 | É recomendável que, após criar sua conta de mestre, modificar a chave do mestre, que é ensinado na seção [Acessando as configurações de sistema](./settings.md). 14 | 15 | ## O Painel do Mestre 16 | 17 | O painel do mestre é a primeira e mais importante página da conta do mestre. Aqui, você monitora os personagens e suas rolagens de dado, gerencia o combate e iniciativa e gerencia NPCs genéricos. Também, é possível fazer rolagens livres no painel do mestre. 18 | 19 | Também, é possível gerenciar os retratos (extensão do OBS) a partir do painel do mestre, por meio do botão "Retrato em Ambiente de Combate? (Extensão OBS)". 20 | 21 | ## Retrato em Ambiente de Combate (Extensão OBS) 22 | 23 | Esse botão serve para controlar o estado dos retratos no OBS. Caso esse botão esteja marcado, apenas os atributos do jogadores estarão em tela. Caso contrário, apenas o nome fica em tela. 24 | 25 | ## Painel dos Jogadores 26 | 27 | O painel dos jogadores é o painel de monitoramento de todos os jogadores cadastrados no seu site. Aqui, você pode apagar jogadores ou copiar o link de seus retratos (extensão OBS). Para cada jogador, é possível monitorar em tempo real atributos, especificações de personagem, moeda, equipamentos e itens. 28 | 29 | ## Utilitários 30 | 31 | O painel do mestre também oferece vários utilitários. 32 | 33 | ### Rolagem 34 | 35 | O utilitário Rolagem permite a Rolagem Livre pelo mestre, da mesma forma que um jogador faria. 36 | 37 | ### Combate 38 | 39 | O utilitário Combate permite você gerenciar o combate do jogo, definindo ordem e contando as rodadas. Cada vez que o ponteiro de ordem reseta, uma nova rodada é computada. 40 | 41 | ### Histórico 42 | 43 | Aqui, você pode monitorar o histórico de rolagens dos jogadores. 44 | 45 | ### NPCs 46 | 47 | O utilitário NPCs permite a criação de NPCs genéricos com nome e vida. 48 | 49 | ### Anotações 50 | 51 | Aqui, você pode fazer anotações de qualquer tipo, como as anotações dos jogadores. 52 | 53 | # Visão Geral do Painel do Mestre 54 | 55 | ![localhost_3000_admin_main](https://user-images.githubusercontent.com/71353674/163498765-21e8be6b-ccb1-4eaf-850a-b473496b3913.png) 56 | -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | # Criando seu próprio site com o Open RPG 2 | 3 | Nessa seção, ensinarei como criar seu site com o Open RPG. 4 | 5 | ## Importante! 6 | 7 | Antes de começarmos, você deverá cumprir alguns passos preliminares. 8 | 9 | 1. Você deverá criar uma conta aqui, no [GitHub](https://github.com/signup). Caso já possua uma conta, pode pular essa preliminar. 10 | 2. Você deverá criar uma conta na [Heroku](https://id.heroku.com/signup), o servidor no qual hospedaremos o nosso aplicativo. 11 | 3. Você deverá fazer o [fork](https://github.com/alyssapiresfernandescefet/openrpg/fork) desse repositório para a sua conta. 12 | 13 | ## Iniciando 14 | 15 | 1. Primeiramente, você deve acessar o seu [Dashboard](https://dashboard.heroku.com/) na Heroku e criar um novo App. Você deverá preencher o nome do aplicativo e a região em que o aplicativo irá ser hospedado. Digite um nome simples mas que lembre o nome do seu RPG, porque esse nome também será o endereço do seu site. Selecione a região mais próxima e clique em Criar App. 16 | 17 | 2. Após isso, você deverá acessar a aba de Deploy. Em "Deployment method", selecione GitHub e conecte a sua conta do GitHub ao Heroku. Depois de conectada, você se direcionará à seção de "Connect to GitHub", logo abaixo de "Deployment method". Nessa seção, em repo-name, você deverá digitar "openrpg" e clicar em "Search". Depois de alguns segundos, um item com o nome do seu GitHub / openrpg vai aparecer, e logo ao lado um botão nomeado "Connect". Clique no botão. 18 | 19 | 3. Depois de conectar o seu repositório GitHub ao Heroku, você precisará configurar o aplicativo para uso. Acesse a aba Settings, e na segunda seção, em "Config Vars", clique em "Reveal Config Vars". Após isso, dois campos de textos irão aparecer com o nome KEY e VALUE, respectivamente. Você deverá preencher alguns campos agora. Abaixo, haverá uma tabela dos campos que devem ser preenchidos. Ao preencher cada valor, clique em "Add" para adicioná-los ao aplicativo. 20 | 21 | | KEY | VALUE | 22 | | -------------- | -------------------------------------------------------------------------------------------------------------------------------- | 23 | | SESSION_SECRET | Um valor aleatório de no mínimo 32 dígitos, que pode ser gerado [aqui](https://onlinehashtools.com/generate-random-sha256-hash). | 24 | 25 | Caso tenha feito tudo corretamente, a seção "Config Vars" deverá estar semelhante a essa aqui: 26 | 27 | ![image](https://user-images.githubusercontent.com/71353674/160728220-49c66b8f-6634-46f2-a55e-78cf698ef810.png) 28 | 29 | ### Configurando o Banco de Dados (Heroku Add-ons) 30 | 31 | Antes de utilizar o seu aplicativo, você precisa configurar o seu banco de dados pela Heroku. 32 | 33 | Primeiramente, você irá precisar de um cartão de crédito. Não se preocupe, a Heroku não te cobra nada, ela só precisa de um cartão para firmar um contrato com um provedor de banco de dados. No caso, o provedor que escolheremos é grátis, então não será cobrado nada no cartão. Os provedores de banco de dados da Heroku são totalmente confiáveis e muito eficientes. 34 | 35 | Siga esses passos: 36 | 37 | 1. Acesse a aba de Resources. Na seção Add-ons, você irá procurar por "ClearDB MySQL". 38 | 39 | ![image](https://user-images.githubusercontent.com/71353674/160009589-58dd6722-0b31-45bc-b4db-65734460627e.png) 40 | 41 | 2. Selecione esse item e logo após surgirá uma tela de planos. Selecione o plano Ignite - Free e clique em "Submit Order Form". Caso ele peça seu cartão de crédito, preencha. 42 | 3. Após um tempo, a ordem de "compra" irá suceder, e o banco de dados estará instalado e pronto para uso! 43 | 44 | ### Fazendo o Deploy 45 | 46 | Após configurar a Heroku e configurar o banco de dados, você irá acessar novamente a página de Deploy e, no final da página, clicar em "Deploy Branch". Espere o seu deploy terminar, e bom jogo! 47 | 48 | Para acessar o seu app, role até o topo da página e clique em "Open App". Compartilhe a url do aplicativo para todos os jogadores! 49 | -------------------------------------------------------------------------------- /docs/obs.md: -------------------------------------------------------------------------------- 1 | # Integrando o Open RPG com o OBS 2 | 3 | Nessa seção, irei ensinar para você como realizar a integração do Open RPG com o OBS. 4 | 5 | ## Retratos 6 | 7 | A integração do Open RPG com o OBS se chama um Retrato. Um Retrato basicamente representa a imagem de um jogador em tela, com atualização de atributos e rolagens em tempo real. 8 | 9 | ![image](https://user-images.githubusercontent.com/71353674/163499503-10330b79-9836-4b90-889f-d6773462ae4e.png) 10 | 11 | ## Adicionando os Retratos ao OBS 12 | 13 | O OBS exibe os retratos através de uma fonte chamada Browser Source (ou Navegador). Essa fonte pede um link e uma altura e largura para ser exibida. Para achar o link, basta ir ao painel do mestre e clicar no botão "Retrato" em um jogador. Um link será copiado para sua área de transferência, e você deverá colar esse link no Browser Source. 14 | 15 | ## Configurando os Retratos 16 | 17 | Os retratos já vêm completamente automatizados, sem precisar de nenhuma outra ação do mestre. No entanto, há algumas situações onde o nome ou atributo do player pode aparecer cortado na tela. Para configurar isso, você deve acessar as propriedades do Browser Source e ajustar a sua altura e largura. 18 | 19 | ## Automatização 20 | 21 | Como já dito, os retratos já funcionam automaticamente, então só é preciso adicioná-lo ao OBS e ajustá-lo quanto a necessidade. Os retratos oferecem automatizações como: 22 | 23 | #### Avatar 24 | 25 | Qualquer mudança no Avatar do personagem será automaticamente refletido no retrato. 26 | 27 | #### Atributos 28 | 29 | Os atributos na tela são sincronizados com os atributos do jogador. 30 | 31 | #### Rolagens de Dado 32 | 33 | As rolagens de dado são automaticamente exibidas na tela quando um jogador rola um ou mais dados. Caso o dado possua uma descrição, como "Fracasso" ou "Sucesso", essa descrição será exibida. 34 | 35 | ## Editando o Retrato 36 | 37 | É possível editar os atributos que parecem no retrato na página de configurações, no painel do mestre. 38 | 39 | Além disso, também é possível editar a posição dos atributos e nome na tela. Para isso, você deve interagir com o retrato pelo OBS e mover os atributos e nome para onde bem entender. Note que o nome e atributos sempre têm posição iguais. 40 | 41 | ![portrait](https://user-images.githubusercontent.com/71353674/167266495-3329e025-4917-4602-8400-91d8445899a8.gif) 42 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Acessando as Configurações de Sistema 2 | 3 | Nessa seção, ensinarei como acessar e modificar as configurações do sistema. 4 | 5 | ## Configurações do Sistema 6 | 7 | As configurações do sistema são acessíveis através da barra de navegação do mestre. 8 | 9 | Nessa página, é possível modificar alguns aspectos fundamentais do sistema, como a chave do mestre, nome dos contêineres da ficha eletrônica, retratos e dados. 10 | 11 | As configurações são dividas em 3 seções. 12 | 13 | ## Geral 14 | 15 | Nas configurações gerais, temos: 16 | 17 | 1. Chave do Mestre 18 | 2. Marcação Automática de Perícia 19 | 3. Título dos contêineres 20 | 21 | Aqui, é possível atualizar a chave do mestre e o título dos contêineres que aparecem na ficha eletrônica do jogador. Isso é interessante se você quer mudar contêineres como a Magia para um nome mais adequado para sua campanha, como "Rituais" ou "Habilidades". 22 | 23 | Além disso, é possível ativar a marcação automática de perícia, que orienta o sistema a marcar a perícia automaticamente caso o teste feito for um sucesso. 24 | 25 | ## Dado 26 | 27 | Aqui, é possível mudar a forma que o sistema faz a resolução de dados. 28 | 29 | ### Tipos de Sucesso 30 | 31 | Os tipos de sucesso são as descrições que vêm com uma rolagem de dado simples (Característica ou Perícia). Elas podem ser "Sucesso" ou "Fracasso". 32 | 33 | ![image](https://user-images.githubusercontent.com/71353674/160731143-5e7e136f-728d-4b90-a97a-74f11c087c7d.png) 34 | 35 | ### Rolagem de Atributo 36 | 37 | Determina o padrão que o sistema irá usar quando rolagens de atributo forem solicitadas. 38 | 39 | Ramificações são tipos de sucesso extras, como "Bom" e "Extremo". 40 | 41 | ### Rolagem de Característica 42 | 43 | Determina o padrão que o sistema irá usar quando rolagens de Característica forem solicitadas. 44 | 45 | Ramificações são tipos de sucesso extras, como "Bom" e "Extremo". 46 | 47 | ### Rolagem de Perícia 48 | 49 | Determina o padrão que o sistema irá usar quando rolagens de Perícia forem solicitadas. 50 | 51 | Ramificações são tipos de sucesso extras, como "Bom" e "Extremo". 52 | 53 | Após qualquer edição, clique em "Aplicar". 54 | 55 | ## Retrato (extensão OBS) 56 | 57 | Aqui, você pode editar a exibição dos retratos. 58 | 59 | ### Orientação do Retrato 60 | 61 | A Orientação do Retrato define a posição dos elementos com relação à imagem. Isso é importante quando se está utilizando imagens que mostram o avatar numa posição inferior, superior ou no centro do retrato. 62 | 63 | ### Atributos Principais 64 | 65 | Editar os atributos principais. 66 | 67 | ![image](https://user-images.githubusercontent.com/71353674/160731512-0a6b8c71-9fb6-45e5-9fcd-39d7089c9048.png) 68 | 69 | ### Atributo Secundário 70 | 71 | Editar o atributo secundário. 72 | 73 | ![image](https://user-images.githubusercontent.com/71353674/160731530-16936850-9489-4468-996a-6717191b4add.png) 74 | 75 | Após qualquer edição, clique em "Aplicar". 76 | -------------------------------------------------------------------------------- /docs/update.md: -------------------------------------------------------------------------------- 1 | # Mantendo seu Open RPG atualizado 2 | 3 | O Open RPG recebe muitas atualizações mensalmente, e é importante que você mantenha seu site do Open RPG sempre atualizado. 4 | 5 | ## Atualizando o seu site do Open RPG 6 | 7 | Para atualizar o seu site, você precisa seguir apenas dois passos simples: 8 | 9 | 1. Atualize o seu repositório do Open RPG no GitHub com as últimas atualizações. 10 | 2. Faça o Redeploy. 11 | 12 | ### Processo de Redeploy 13 | 14 | Para fazer o redeploy depois de atualizar seu repositório, você precisa ir até o seu app no Heroku, ir na seção de Deploy e, no final da página, clicar em Deploy Branch. 15 | 16 | Assim, o site será recarregado com as últimas atualizações. -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare var prisma: PrismaClient; 2 | 3 | declare type ReducerActions = { 4 | [K in keyof T]: { 5 | type: K; 6 | data: T[K]; 7 | }; 8 | }[keyof T]; 9 | 10 | declare type EditorModalData = { 11 | operation: 'edit' | 'create'; 12 | show: boolean; 13 | data?: T; 14 | }; 15 | 16 | declare type EditorModalProps = EditorModalData & { 17 | disabled?: boolean; 18 | onSubmit: (data: T) => void; 19 | onHide: () => void; 20 | }; 21 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const path = require('path'); 3 | 4 | const nextConfig = { 5 | reactStrictMode: false, 6 | sassOptions: { 7 | includePaths: [path.join(__dirname, 'src/styles')], 8 | }, 9 | }; 10 | 11 | module.exports = nextConfig; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openrpg", 3 | "version": "1.0.0-beta", 4 | "private": true, 5 | "cacheDirectories": [ 6 | ".next/cache" 7 | ], 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "prisma migrate deploy & next build", 11 | "start": "next start -p $PORT", 12 | "lint": "next lint" 13 | }, 14 | "prisma": { 15 | "schema": "src/prisma/schema.prisma" 16 | }, 17 | "dependencies": { 18 | "@prisma/client": "^4.2.1", 19 | "axios": "^0.26.0", 20 | "bcrypt": "^5.0.1", 21 | "bootstrap": "^5.1.3", 22 | "copy-to-clipboard": "^3.3.1", 23 | "iron-session": "^6.0.5", 24 | "next": "12.1.0", 25 | "prisma": "^4.2.1", 26 | "react": "17.0.2", 27 | "react-bootstrap": "^2.1.2", 28 | "react-dom": "17.0.2", 29 | "react-draggable": "^4.4.5", 30 | "react-icons": "^4.3.1", 31 | "react-sortable-hoc": "^2.0.0", 32 | "react-window": "^1.8.7", 33 | "sass": "^1.49.9", 34 | "sharp": "^0.30.6", 35 | "socket.io": "^4.4.1", 36 | "socket.io-client": "^4.4.1" 37 | }, 38 | "devDependencies": { 39 | "@types/bcrypt": "^5.0.0", 40 | "@types/react-window": "^1.8.5", 41 | "eslint": "8.9.0", 42 | "eslint-config-next": "12.1.0", 43 | "eslint-plugin-unused-imports": "^2.0.0" 44 | }, 45 | "engines": { 46 | "node": ">=16.0.0", 47 | "npm": ">=8.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/avatar404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/avatar404.png -------------------------------------------------------------------------------- /public/dice10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice10.webp -------------------------------------------------------------------------------- /public/dice12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice12.webp -------------------------------------------------------------------------------- /public/dice20.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice20.webp -------------------------------------------------------------------------------- /public/dice4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice4.webp -------------------------------------------------------------------------------- /public/dice6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice6.webp -------------------------------------------------------------------------------- /public/dice8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice8.webp -------------------------------------------------------------------------------- /public/dice_animation.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/dice_animation.webm -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucaspiresfernandes/openrpg/e98719499d23df196729e5a8c209fd0cc57f94be/public/favicon.ico -------------------------------------------------------------------------------- /src/components/Admin/AdminEnvironmentConfigurations.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEvent } from 'react'; 2 | import { useContext, useState } from 'react'; 3 | import Col from 'react-bootstrap/Col'; 4 | import FormCheck from 'react-bootstrap/FormCheck'; 5 | import { ErrorLogger } from '../../contexts'; 6 | import api from '../../utils/api'; 7 | import type { Environment } from '../../utils/config'; 8 | 9 | export default function AdminEnvironmentConfigurations(props: { 10 | environment: Environment; 11 | }) { 12 | const [environment, setEnvironment] = useState(props.environment); 13 | const logError = useContext(ErrorLogger); 14 | 15 | function environmentChange(ev: ChangeEvent) { 16 | const value = ev.target.checked ? 'combat' : 'idle'; 17 | setEnvironment(value); 18 | api.post('/config', { name: 'environment', value }).catch((err) => { 19 | setEnvironment(environment); 20 | logError(err); 21 | }); 22 | } 23 | 24 | return ( 25 | 26 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Admin/AdminUtilityContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from 'react'; 2 | import Col from 'react-bootstrap/Col'; 3 | import Row from 'react-bootstrap/Row'; 4 | import { ErrorLogger, Socket } from '../../contexts'; 5 | import api from '../../utils/api'; 6 | import DataContainer from '../DataContainer'; 7 | import GeneralDiceRollModal from '../Modals/GeneralDiceRollModal'; 8 | import CombatContainer from './CombatContainer'; 9 | import DiceList from './DiceList'; 10 | import NPCContainer from './NPCContainer'; 11 | 12 | type NPC = { name: string; id: number; npc: boolean }; 13 | 14 | type AdminUtilityContainerProps = { 15 | players: { id: number; name: string; npc: boolean }[]; 16 | npcs: { id: number; name: string }[]; 17 | }; 18 | 19 | export default function AdminUtilityContainer(props: AdminUtilityContainerProps) { 20 | const [basicNpcs, setBasicNpcs] = useState([]); 21 | const [complexNpcs, setComplexNpcs] = useState( 22 | props.npcs.map((n) => ({ ...n, npc: true })) 23 | ); 24 | const componentDidMount = useRef(false); 25 | const socket = useContext(Socket); 26 | const logError = useContext(ErrorLogger); 27 | 28 | useEffect(() => { 29 | setBasicNpcs(JSON.parse(localStorage.getItem('admin_npcs') || '[]') as NPC[]); 30 | 31 | socket.on('playerNameChange', (playerId, value) => { 32 | const npc = complexNpcs.find((npc) => npc.id === playerId); 33 | if (!npc) return; 34 | npc.name = value; 35 | setComplexNpcs([...complexNpcs]); 36 | }); 37 | 38 | return () => { 39 | socket.off('playerNameChange'); 40 | }; 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | }, []); 43 | 44 | useEffect(() => { 45 | if (componentDidMount.current) { 46 | localStorage.setItem('admin_npcs', JSON.stringify(basicNpcs)); 47 | return; 48 | } 49 | componentDidMount.current = true; 50 | }, [basicNpcs]); 51 | 52 | function addBasicNPC() { 53 | setBasicNpcs([ 54 | ...basicNpcs, 55 | { id: Date.now(), name: `NPC ${basicNpcs.length}`, npc: true }, 56 | ]); 57 | } 58 | 59 | function removeBasicNPC(id: number) { 60 | const newNpcs = [...basicNpcs]; 61 | newNpcs.splice( 62 | newNpcs.findIndex((npc) => npc.id === id), 63 | 1 64 | ); 65 | setBasicNpcs(newNpcs); 66 | } 67 | 68 | function addComplexNPC() { 69 | const name = prompt('Digite o nome do NPC:'); 70 | if (!name) return; 71 | api 72 | .put('/sheet/npc', { name }) 73 | .then((res) => { 74 | const id = res.data.id; 75 | setComplexNpcs([...complexNpcs, { id, name, npc: true }]); 76 | }) 77 | .catch(logError); 78 | } 79 | 80 | function removeComplexNPC(id: number) { 81 | if (!confirm('Tem certeza de que deseja apagar esse NPC?')) return; 82 | api 83 | .delete('/sheet/npc', { data: { id } }) 84 | .then(() => { 85 | const newNpcs = [...complexNpcs]; 86 | newNpcs.splice( 87 | newNpcs.findIndex((npc) => npc.id === id), 88 | 1 89 | ); 90 | setComplexNpcs(newNpcs); 91 | }) 92 | .catch(logError); 93 | } 94 | 95 | return ( 96 | <> 97 | 98 | 99 | 100 | 101 | 102 | Geral 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | { 119 | const npc = basicNpcs.find((npc) => npc.id === id); 120 | if (!npc) return; 121 | npc.name = ev.target.value; 122 | setBasicNpcs([...basicNpcs]); 123 | }} 124 | onAddComplexNpc={addComplexNPC} 125 | onRemoveComplexNpc={removeComplexNPC} 126 | /> 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /src/components/Admin/AvatarField.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import Col from 'react-bootstrap/Col'; 3 | import Image from 'react-bootstrap/Image'; 4 | import api from '../../utils/api'; 5 | 6 | const style = { maxHeight: 250 }; 7 | 8 | type AvatarFieldProps = { 9 | status: { id: number; value: boolean }[]; 10 | playerId: number; 11 | }; 12 | 13 | export default function AvatarField({ status, playerId }: AvatarFieldProps) { 14 | const [src, setSrc] = useState('#'); 15 | const previousStatusID = useRef(Number.MAX_SAFE_INTEGER); 16 | 17 | useEffect(() => { 18 | let statusID = 0; 19 | for (const stat of status) { 20 | if (stat.value) { 21 | statusID = stat.id; 22 | break; 23 | } 24 | } 25 | if (statusID === previousStatusID.current) return; 26 | previousStatusID.current = statusID; 27 | api 28 | .get(`/sheet/player/avatar/${statusID}?playerID=${playerId}`) 29 | .then((res) => setSrc(res.data.link)) 30 | .catch((err) => setSrc('/avatar404.png')); 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [status]); 33 | 34 | return ( 35 | 36 | Avatar setSrc('/avatar404.png')} 42 | /> 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Admin/DiceList.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from 'react'; 2 | import Col from 'react-bootstrap/Col'; 3 | import ListGroup from 'react-bootstrap/ListGroup'; 4 | import Row from 'react-bootstrap/Row'; 5 | import { Socket } from '../../contexts'; 6 | import DataContainer from '../DataContainer'; 7 | 8 | const highlightStyle = { color: '#00a000', fontWeight: 'bold' }; 9 | 10 | type Dice = { name: string; dices: string; results: string }; 11 | 12 | export default function DiceList(props: { players: { id: number; name: string }[] }) { 13 | const [values, setValues] = useState([]); 14 | const wrapper = useRef(null); 15 | const socket = useContext(Socket); 16 | const componentDidMount = useRef(false); 17 | 18 | useEffect(() => { 19 | setValues(JSON.parse(localStorage.getItem('admin_dice_history') || '[]') as Dice[]); 20 | 21 | socket.on('diceResult', (playerID, _results, _dices) => { 22 | const playerName = 23 | props.players.find((p) => p.id === playerID)?.name || 'Desconhecido'; 24 | 25 | const isArray = Array.isArray(_dices); 26 | 27 | const dices = isArray 28 | ? _dices.map((dice) => { 29 | const num = dice.num; 30 | const roll = dice.roll; 31 | return num > 0 ? `${num}d${roll}` : roll; 32 | }) 33 | : _dices.num > 0 34 | ? [`${_dices.num}d${_dices.roll}`] 35 | : [_dices.roll]; 36 | 37 | const results = _results.map((res) => { 38 | const roll = res.roll; 39 | const description = res.resultType?.description; 40 | if (description) return `${roll} (${description})`; 41 | return roll; 42 | }); 43 | 44 | const message = { 45 | name: playerName, 46 | dices: dices.join(', '), 47 | results: results.join(', '), 48 | }; 49 | 50 | setValues((values) => { 51 | if (values.length > 10) { 52 | const newValues = [...values]; 53 | newValues.unshift(message); 54 | newValues.splice(newValues.length - 1, 1); 55 | return newValues; 56 | } 57 | return [message, ...values]; 58 | }); 59 | }); 60 | 61 | return () => { 62 | socket.off('diceResult'); 63 | }; 64 | // eslint-disable-next-line react-hooks/exhaustive-deps 65 | }, []); 66 | 67 | useEffect(() => { 68 | if (wrapper.current) wrapper.current.scrollTo({ top: 0, behavior: 'auto' }); 69 | 70 | if (componentDidMount.current) { 71 | localStorage.setItem('admin_dice_history', JSON.stringify(values)); 72 | return; 73 | } 74 | componentDidMount.current = true; 75 | }, [values]); 76 | 77 | return ( 78 | { 81 | setValues([]); 82 | } 83 | }}> 84 | 85 | 86 |
87 | 88 | {values.map((val, index) => ( 89 | 90 | {val.name} 91 | rolou 92 | {val.dices} e tirou 93 | {val.results}. 94 | 95 | ))} 96 | 97 |
98 | 99 |
100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/CharacteristicEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Characteristic } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import CharacteristicEditorModal from '../../Modals/CharacteristicEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type CharacteristicEditorContainerProps = { 11 | characteristics: Characteristic[]; 12 | title: string; 13 | }; 14 | 15 | export default function CharacteristicEditorContainer( 16 | props: CharacteristicEditorContainerProps 17 | ) { 18 | const [loading, setLoading] = useState(false); 19 | const [characteristicModal, setCharacteristicModal] = useState< 20 | EditorModalData 21 | >({ 22 | show: false, 23 | operation: 'create', 24 | }); 25 | const [characteristics, setCharacteristics] = useState(props.characteristics); 26 | const logError = useContext(ErrorLogger); 27 | 28 | function onModalSubmit({ id, name, visibleToAdmin }: Characteristic) { 29 | setLoading(true); 30 | 31 | const config: AxiosRequestConfig = 32 | characteristicModal.operation === 'create' 33 | ? { 34 | method: 'PUT', 35 | data: { name, visibleToAdmin }, 36 | } 37 | : { 38 | method: 'POST', 39 | data: { id, name, visibleToAdmin }, 40 | }; 41 | 42 | api('/sheet/characteristic', config) 43 | .then((res) => { 44 | if (characteristicModal.operation === 'create') { 45 | setCharacteristics([ 46 | ...characteristics, 47 | { id: res.data.id, name, visibleToAdmin }, 48 | ]); 49 | return; 50 | } 51 | characteristics[characteristics.findIndex((char) => char.id === id)] = { 52 | id, 53 | name, 54 | visibleToAdmin, 55 | }; 56 | setCharacteristics([...characteristics]); 57 | }) 58 | .catch(logError) 59 | .finally(() => setLoading(false)); 60 | } 61 | 62 | function deleteCharacteristic(id: number) { 63 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 64 | setLoading(true); 65 | api 66 | .delete('/sheet/characteristic', { data: { id } }) 67 | .then(() => { 68 | const newCharacteristic = [...characteristics]; 69 | const index = newCharacteristic.findIndex((char) => char.id === id); 70 | if (index > -1) { 71 | newCharacteristic.splice(index, 1); 72 | setCharacteristics(newCharacteristic); 73 | } 74 | }) 75 | .catch(logError) 76 | .finally(() => setLoading(false)); 77 | } 78 | 79 | return ( 80 | <> 81 | setCharacteristicModal({ operation: 'create', show: true }), 88 | disabled: loading, 89 | }}> 90 | 93 | setCharacteristicModal({ 94 | operation: 'edit', 95 | show: true, 96 | data: characteristics.find((char) => char.id === id), 97 | }) 98 | } 99 | onDelete={(id) => deleteCharacteristic(id)} 100 | disabled={loading} 101 | /> 102 | 103 | setCharacteristicModal({ operation: 'create', show: false })} 106 | onSubmit={onModalSubmit} 107 | disabled={loading} 108 | /> 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/CurrencyEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Currency } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import CurrencyEditorModal from '../../Modals/CurrencyEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type CurrencyEditorContainerProps = { 11 | currencies: Currency[]; 12 | }; 13 | 14 | export default function CurrencyEditorContainer(props: CurrencyEditorContainerProps) { 15 | const [loading, setLoading] = useState(false); 16 | const [currencyModal, setCurrencyModal] = useState>({ 17 | show: false, 18 | operation: 'create', 19 | }); 20 | const [currency, setCurrency] = useState(props.currencies); 21 | const logError = useContext(ErrorLogger); 22 | 23 | function onModalSubmit({ id, name, visibleToAdmin }: Currency) { 24 | setLoading(true); 25 | 26 | const config: AxiosRequestConfig = 27 | currencyModal.operation === 'create' 28 | ? { 29 | method: 'PUT', 30 | data: { name, visibleToAdmin }, 31 | } 32 | : { 33 | method: 'POST', 34 | data: { id, name, visibleToAdmin }, 35 | }; 36 | 37 | api('/sheet/currency', config) 38 | .then((res) => { 39 | if (currencyModal.operation === 'create') { 40 | setCurrency([...currency, { id: res.data.id, name, visibleToAdmin }]); 41 | return; 42 | } 43 | currency[currency.findIndex((cur) => cur.id === id)] = { 44 | id, 45 | name, 46 | visibleToAdmin, 47 | }; 48 | setCurrency([...currency]); 49 | }) 50 | .catch(logError) 51 | .finally(() => setLoading(false)); 52 | } 53 | 54 | function deleteCurrency(id: number) { 55 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 56 | setLoading(true); 57 | api 58 | .delete('/sheet/currency', { data: { id } }) 59 | .then(() => { 60 | currency.splice( 61 | currency.findIndex((cur) => cur.id === id), 62 | 1 63 | ); 64 | setCurrency([...currency]); 65 | }) 66 | .catch(logError) 67 | .finally(() => setLoading(false)); 68 | } 69 | 70 | return ( 71 | <> 72 | setCurrencyModal({ operation: 'create', show: true }), 79 | disabled: loading, 80 | }}> 81 | 84 | setCurrencyModal({ 85 | operation: 'edit', 86 | show: true, 87 | data: currency.find((cur) => cur.id === id), 88 | }) 89 | } 90 | onDelete={(id) => deleteCurrency(id)} 91 | disabled={loading} 92 | /> 93 | 94 | setCurrencyModal({ operation: 'create', show: false })} 97 | onSubmit={onModalSubmit} 98 | disabled={loading} 99 | /> 100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/EditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | import { FixedSizeList } from 'react-window'; 3 | import Row from 'react-bootstrap/Row'; 4 | import Col from 'react-bootstrap/Col'; 5 | import Button from 'react-bootstrap/Button'; 6 | import CustomSpinner from '../../CustomSpinner'; 7 | import { BsPencil, BsTrash } from 'react-icons/bs'; 8 | 9 | export default function EditorContainer({ 10 | data, 11 | onEdit, 12 | onDelete, 13 | disabled, 14 | }: { 15 | data: { id: number; name: string }[]; 16 | onEdit: (id: number) => void; 17 | onDelete: (id: number) => void; 18 | disabled?: boolean; 19 | }) { 20 | const DataRow = ({ index, style }: { index: number; style: CSSProperties }) => ( 21 | 29 | ); 30 | 31 | return ( 32 | 33 | 34 | 40 | {DataRow} 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | type EditorRowProps = { 48 | id: number; 49 | name: string; 50 | onEdit: (id: number) => void; 51 | onDelete: (id: number) => void; 52 | disabled?: boolean; 53 | style: CSSProperties; 54 | }; 55 | 56 | function EditorRow(props: EditorRowProps) { 57 | return ( 58 | 59 | 60 | 71 | 72 | 73 | 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/EquipmentEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Equipment } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import EquipmentEditorModal from '../../Modals/EquipmentEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type EquipmentEditorContainerProps = { 11 | equipments: Equipment[]; 12 | title: string; 13 | }; 14 | 15 | export default function EquipmentEditorContainer(props: EquipmentEditorContainerProps) { 16 | const [loading, setLoading] = useState(false); 17 | const [equipmentModal, setEquipmentModal] = useState>({ 18 | show: false, 19 | operation: 'create', 20 | }); 21 | const [equipment, setEquipment] = useState(props.equipments); 22 | const logError = useContext(ErrorLogger); 23 | 24 | function onModalSubmit(equip: Equipment) { 25 | if (!equip.name || !equip.attacks || !equip.damage || !equip.range || !equip.type) 26 | return alert( 27 | 'Nenhum campo pode ser vazio. Para definir um campo vazio, utilize "-"' 28 | ); 29 | 30 | setLoading(true); 31 | 32 | const config: AxiosRequestConfig = 33 | equipmentModal.operation === 'create' 34 | ? { 35 | method: 'PUT', 36 | data: { ...equip, id: undefined }, 37 | } 38 | : { 39 | method: 'POST', 40 | data: equip, 41 | }; 42 | 43 | api('/sheet/equipment', config) 44 | .then((res) => { 45 | if (equipmentModal.operation === 'create') { 46 | setEquipment([...equipment, { ...equip, id: res.data.id }]); 47 | return; 48 | } 49 | equipment[equipment.findIndex((eq) => eq.id === equip.id)] = equip; 50 | setEquipment([...equipment]); 51 | }) 52 | .catch(logError) 53 | .finally(() => setLoading(false)); 54 | } 55 | 56 | function deleteEquipment(id: number) { 57 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 58 | setLoading(true); 59 | api 60 | .delete('/sheet/equipment', { data: { id } }) 61 | .then(() => { 62 | equipment.splice( 63 | equipment.findIndex((eq) => eq.id === id), 64 | 1 65 | ); 66 | setEquipment([...equipment]); 67 | }) 68 | .catch(logError) 69 | .finally(() => setLoading(false)); 70 | } 71 | 72 | return ( 73 | <> 74 | setEquipmentModal({ operation: 'create', show: true }), 81 | disabled: loading, 82 | }}> 83 | 86 | setEquipmentModal({ 87 | operation: 'edit', 88 | show: true, 89 | data: equipment.find((eq) => eq.id === id), 90 | }) 91 | } 92 | onDelete={(id) => deleteEquipment(id)} 93 | disabled={loading} 94 | /> 95 | 96 | setEquipmentModal({ operation: 'create', show: false })} 99 | onSubmit={onModalSubmit} 100 | disabled={loading} 101 | /> 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/ExtraInfoEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtraInfo } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import ExtraInfoEditorModal from '../../Modals/ExtraInfoEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type ExtraInfoEditorContainerProps = { 11 | extraInfo: ExtraInfo[]; 12 | title: string; 13 | }; 14 | 15 | export default function ExtraInfoEditorContainer(props: ExtraInfoEditorContainerProps) { 16 | const [loading, setLoading] = useState(false); 17 | const [infoModal, setInfoModal] = useState>({ 18 | show: false, 19 | operation: 'create', 20 | }); 21 | const [extraInfo, setExtraInfo] = useState(props.extraInfo); 22 | const logError = useContext(ErrorLogger); 23 | 24 | function modalSubmit({ id, name }: ExtraInfo) { 25 | setLoading(true); 26 | 27 | const config: AxiosRequestConfig = 28 | infoModal.operation === 'create' 29 | ? { 30 | method: 'PUT', 31 | data: { name }, 32 | } 33 | : { 34 | method: 'POST', 35 | data: { id, name }, 36 | }; 37 | 38 | api('/sheet/extrainfo', config) 39 | .then((res) => { 40 | if (infoModal.operation === 'create') { 41 | setExtraInfo([...extraInfo, { id: res.data.id, name }]); 42 | return; 43 | } 44 | extraInfo[extraInfo.findIndex((info) => info.id === id)].name = name; 45 | setExtraInfo([...extraInfo]); 46 | }) 47 | .catch(logError) 48 | .finally(() => setLoading(false)); 49 | } 50 | 51 | function deleteExtraInfo(id: number) { 52 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 53 | setLoading(true); 54 | api 55 | .delete('/sheet/extrainfo', { data: { id } }) 56 | .then(() => { 57 | const newExtraInfo = [...extraInfo]; 58 | const index = newExtraInfo.findIndex((extraInfo) => extraInfo.id === id); 59 | if (index > -1) { 60 | newExtraInfo.splice(index, 1); 61 | setExtraInfo(newExtraInfo); 62 | } 63 | }) 64 | .catch(logError) 65 | .finally(() => setLoading(false)); 66 | } 67 | 68 | return ( 69 | <> 70 | setInfoModal({ operation: 'create', show: true }), 77 | disabled: loading, 78 | }}> 79 | 82 | setInfoModal({ 83 | operation: 'edit', 84 | show: true, 85 | data: extraInfo.find((info) => info.id === id), 86 | }) 87 | } 88 | onDelete={(id) => deleteExtraInfo(id)} 89 | disabled={loading} 90 | /> 91 | 92 | setInfoModal({ operation: 'create', show: false })} 95 | onSubmit={modalSubmit} 96 | disabled={loading} 97 | /> 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/InfoEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Info } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import InfoEditorModal from '../../Modals/InfoEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type InfoEditorContainerProps = { 11 | info: Info[]; 12 | title: string; 13 | }; 14 | 15 | export default function InfoEditorContainer(props: InfoEditorContainerProps) { 16 | const [loading, setLoading] = useState(false); 17 | const [infoModal, setInfoModal] = useState>({ 18 | show: false, 19 | operation: 'create', 20 | }); 21 | const [info, setInfo] = useState(props.info); 22 | const logError = useContext(ErrorLogger); 23 | 24 | function onModalSubmit({ id, name, visibleToAdmin }: Info) { 25 | setLoading(true); 26 | 27 | const config: AxiosRequestConfig = 28 | infoModal.operation === 'create' 29 | ? { 30 | method: 'PUT', 31 | data: { name, visibleToAdmin }, 32 | } 33 | : { 34 | method: 'POST', 35 | data: { id, name, visibleToAdmin }, 36 | }; 37 | 38 | api('/sheet/info', config) 39 | .then((res) => { 40 | if (infoModal.operation === 'create') { 41 | setInfo([...info, { id: res.data.id, name, visibleToAdmin }]); 42 | return; 43 | } 44 | info[info.findIndex((info) => info.id === id)] = { 45 | id, 46 | name, 47 | visibleToAdmin, 48 | }; 49 | setInfo([...info]); 50 | }) 51 | .catch(logError) 52 | .finally(() => setLoading(false)); 53 | } 54 | 55 | function deleteInfo(id: number) { 56 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 57 | setLoading(true); 58 | api 59 | .delete('/sheet/info', { data: { id } }) 60 | .then(() => { 61 | const newInfo = [...info]; 62 | const index = newInfo.findIndex((info) => info.id === id); 63 | if (index > -1) { 64 | newInfo.splice(index, 1); 65 | setInfo(newInfo); 66 | } 67 | }) 68 | .catch(logError) 69 | .finally(() => setLoading(false)); 70 | } 71 | 72 | return ( 73 | <> 74 | setInfoModal({ operation: 'create', show: true }), 81 | disabled: loading, 82 | }}> 83 | 86 | setInfoModal({ 87 | operation: 'edit', 88 | show: true, 89 | data: info.find((i) => i.id === id), 90 | }) 91 | } 92 | onDelete={(id) => deleteInfo(id)} 93 | disabled={loading} 94 | /> 95 | 96 | setInfoModal({ operation: 'create', show: false })} 99 | onSubmit={onModalSubmit} 100 | disabled={loading} 101 | /> 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/ItemEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Item } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import ItemEditorModal from '../../Modals/ItemEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type ItemEditorContainerProps = { 11 | items: Item[]; 12 | title: string; 13 | }; 14 | 15 | export default function ItemEditorContainer(props: ItemEditorContainerProps) { 16 | const [loading, setLoading] = useState(false); 17 | const [itemModal, setItemModal] = useState>({ 18 | show: false, 19 | operation: 'create', 20 | }); 21 | const [item, setItem] = useState(props.items); 22 | const logError = useContext(ErrorLogger); 23 | 24 | function onModalSubmit(it: Item) { 25 | if (!it.name || !it.description) 26 | return alert( 27 | 'Nenhum campo pode ser vazio. Para definir um campo vazio, utilize "-"' 28 | ); 29 | 30 | setLoading(true); 31 | 32 | const config: AxiosRequestConfig = 33 | itemModal.operation === 'create' 34 | ? { 35 | method: 'PUT', 36 | data: { ...it, id: undefined }, 37 | } 38 | : { 39 | method: 'POST', 40 | data: it, 41 | }; 42 | 43 | api('/sheet/item', config) 44 | .then((res) => { 45 | if (itemModal.operation === 'create') { 46 | setItem([...item, { ...it, id: res.data.id }]); 47 | return; 48 | } 49 | item[item.findIndex((eq) => eq.id === it.id)] = it; 50 | setItem([...item]); 51 | }) 52 | .catch(logError) 53 | .finally(() => setLoading(false)); 54 | } 55 | 56 | function deleteItem(id: number) { 57 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 58 | setLoading(true); 59 | api 60 | .delete('/sheet/item', { data: { id } }) 61 | .then(() => { 62 | item.splice( 63 | item.findIndex((eq) => eq.id === id), 64 | 1 65 | ); 66 | setItem([...item]); 67 | }) 68 | .catch(logError) 69 | .finally(() => setLoading(false)); 70 | } 71 | 72 | return ( 73 | <> 74 | setItemModal({ operation: 'create', show: true }), 81 | disabled: loading, 82 | }}> 83 | 86 | setItemModal({ 87 | operation: 'edit', 88 | show: true, 89 | data: item.find((it) => it.id === id), 90 | }) 91 | } 92 | onDelete={(id) => deleteItem(id)} 93 | disabled={loading} 94 | /> 95 | 96 | setItemModal({ operation: 'create', show: false })} 99 | onSubmit={onModalSubmit} 100 | disabled={loading} 101 | /> 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/SpecEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Spec } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import SpecEditorModal from '../../Modals/SpecEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type SpecEditorContainerProps = { 11 | specs: Spec[]; 12 | }; 13 | 14 | export default function SpecEditorContainer(props: SpecEditorContainerProps) { 15 | const [loading, setLoading] = useState(false); 16 | const [specModal, setSpecModal] = useState>({ 17 | show: false, 18 | operation: 'create', 19 | }); 20 | const [spec, setSpec] = useState(props.specs); 21 | const logError = useContext(ErrorLogger); 22 | 23 | function onModalSubmit({ id, name, visibleToAdmin }: Spec) { 24 | setLoading(true); 25 | 26 | const config: AxiosRequestConfig = 27 | specModal.operation === 'create' 28 | ? { 29 | method: 'PUT', 30 | data: { name, visibleToAdmin }, 31 | } 32 | : { 33 | method: 'POST', 34 | data: { id, name, visibleToAdmin }, 35 | }; 36 | 37 | api('/sheet/spec', config) 38 | .then((res) => { 39 | if (specModal.operation === 'create') { 40 | setSpec([...spec, { id: res.data.id, name, visibleToAdmin }]); 41 | return; 42 | } 43 | spec[spec.findIndex((spec) => spec.id === id)] = { 44 | id, 45 | name, 46 | visibleToAdmin, 47 | }; 48 | setSpec([...spec]); 49 | }) 50 | .catch(logError) 51 | .finally(() => setLoading(false)); 52 | } 53 | 54 | function deleteSpec(id: number) { 55 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 56 | setLoading(true); 57 | api 58 | .delete('/sheet/spec', { data: { id } }) 59 | .then(() => { 60 | const newSpec = [...spec]; 61 | const index = newSpec.findIndex((spec) => spec.id === id); 62 | if (index > -1) { 63 | newSpec.splice(index, 1); 64 | setSpec(newSpec); 65 | } 66 | }) 67 | .catch(logError) 68 | .finally(() => setLoading(false)); 69 | } 70 | 71 | return ( 72 | <> 73 | setSpecModal({ operation: 'create', show: true }), 80 | disabled: loading, 81 | }}> 82 | 85 | setSpecModal({ 86 | operation: 'edit', 87 | show: true, 88 | data: spec.find((sp) => sp.id === id), 89 | }) 90 | } 91 | onDelete={(id) => deleteSpec(id)} 92 | disabled={loading} 93 | /> 94 | 95 | setSpecModal({ operation: 'create', show: false })} 99 | disabled={loading} 100 | /> 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/components/Admin/Editor/SpellEditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { Spell } from '@prisma/client'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | import { useContext, useState } from 'react'; 4 | import { ErrorLogger } from '../../../contexts'; 5 | import api from '../../../utils/api'; 6 | import DataContainer from '../../DataContainer'; 7 | import SpellEditorModal from '../../Modals/SpellEditorModal'; 8 | import EditorContainer from './EditorContainer'; 9 | 10 | type SpellEditorContainerProps = { 11 | spells: Spell[]; 12 | title: string; 13 | }; 14 | 15 | export default function SpellEditorContainer(props: SpellEditorContainerProps) { 16 | const [loading, setLoading] = useState(false); 17 | const [spellModal, setSpellModal] = useState>({ 18 | show: false, 19 | operation: 'create', 20 | }); 21 | const [spell, setSpell] = useState(props.spells); 22 | const logError = useContext(ErrorLogger); 23 | 24 | function onModalSubmit(sp: Spell) { 25 | if ( 26 | !sp.name || 27 | !sp.description || 28 | !sp.castingTime || 29 | !sp.cost || 30 | !sp.damage || 31 | !sp.duration || 32 | !sp.range || 33 | !sp.target || 34 | !sp.type 35 | ) 36 | return alert( 37 | 'Nenhum campo pode ser vazio. Para definir um campo vazio, utilize "-"' 38 | ); 39 | 40 | setLoading(true); 41 | 42 | const config: AxiosRequestConfig = 43 | spellModal.operation === 'create' 44 | ? { 45 | method: 'PUT', 46 | data: { ...sp, id: undefined }, 47 | } 48 | : { 49 | method: 'POST', 50 | data: sp, 51 | }; 52 | 53 | api('/sheet/spell', config) 54 | .then((res) => { 55 | if (spellModal.operation === 'create') { 56 | setSpell([...spell, { ...sp, id: res.data.id }]); 57 | return; 58 | } 59 | spell[spell.findIndex((_sp) => _sp.id === sp.id)] = sp; 60 | setSpell([...spell]); 61 | }) 62 | .catch(logError) 63 | .finally(() => setLoading(false)); 64 | } 65 | 66 | function deleteSpell(id: number) { 67 | if (!confirm('Tem certeza de que deseja apagar esse item?')) return; 68 | setLoading(true); 69 | api 70 | .delete('/sheet/spell', { data: { id } }) 71 | .then(() => { 72 | spell.splice( 73 | spell.findIndex((eq) => eq.id === id), 74 | 1 75 | ); 76 | setSpell([...spell]); 77 | }) 78 | .catch(logError) 79 | .finally(() => setLoading(false)); 80 | } 81 | 82 | return ( 83 | <> 84 | setSpellModal({ operation: 'create', show: true }), 91 | disabled: loading, 92 | }}> 93 | 96 | setSpellModal({ 97 | operation: 'edit', 98 | show: true, 99 | data: spell.find((sp) => sp.id === id), 100 | }) 101 | } 102 | onDelete={(id) => deleteSpell(id)} 103 | disabled={loading} 104 | /> 105 | 106 | setSpellModal({ operation: 'create', show: false })} 109 | onSubmit={onModalSubmit} 110 | disabled={loading} 111 | /> 112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/components/Admin/NPCContainer.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEvent, MouseEventHandler } from 'react'; 2 | import Button from 'react-bootstrap/Button'; 3 | import Col from 'react-bootstrap/Col'; 4 | import DropdownItem from 'react-bootstrap/DropdownItem'; 5 | import ListGroup from 'react-bootstrap/ListGroup'; 6 | import Row from 'react-bootstrap/Row'; 7 | import BottomTextInput from '../BottomTextInput'; 8 | import DataContainer from '../DataContainer'; 9 | import PlayerPortraitButton from './PlayerPortraitButton'; 10 | 11 | const style = { maxWidth: '5rem' }; 12 | 13 | type NPCContainerProps = { 14 | basicNpcs: { id: number; name: string }[]; 15 | complexNpcs: { id: number; name: string }[]; 16 | onChangeBasicNpc: (ev: ChangeEvent, id: number) => void; 17 | onAddBasicNpc: MouseEventHandler; 18 | onRemoveBasicNpc: (id: number) => void; 19 | onAddComplexNpc: MouseEventHandler; 20 | onRemoveComplexNpc: (id: number) => void; 21 | }; 22 | 23 | export default function NPCContainer(props: NPCContainerProps) { 24 | return ( 25 | 34 | Básico 35 | Complexo 36 | 37 | ), 38 | }}> 39 | 40 | 41 |
42 | 43 | {props.basicNpcs.map(({ id, name }) => ( 44 | 45 | props.onChangeBasicNpc(ev, id)} 48 | className='w-25 mx-1' 49 | /> 50 | 51 | 58 | 59 | ))} 60 | {props.complexNpcs.map(({ id, name }) => ( 61 | 62 | 63 | 71 | {name} 72 | 79 | 80 | ))} 81 | 82 |
83 | 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/Admin/PlayerPortraitButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Button from 'react-bootstrap/Button'; 3 | import GetPortraitModal from '../Modals/GetPortraitModal'; 4 | 5 | type PlayerPortraitButtonProps = { 6 | playerId: number; 7 | }; 8 | 9 | export default function PlayerPortraitButton(props: PlayerPortraitButtonProps) { 10 | const [getPortraitModalShow, setGetPortraitModalShow] = useState(false); 11 | 12 | return ( 13 | <> 14 | 17 | setGetPortraitModalShow(false)} 20 | playerId={props.playerId} 21 | /> 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Admin/WelcomePage.tsx: -------------------------------------------------------------------------------- 1 | import Router from 'next/router'; 2 | import { useEffect, useState } from 'react'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Container from 'react-bootstrap/Container'; 5 | import Row from 'react-bootstrap/Row'; 6 | import Spinner from 'react-bootstrap/Spinner'; 7 | import api from '../../utils/api'; 8 | 9 | export default function WelcomePage() { 10 | const [error, setError] = useState(null); 11 | 12 | useEffect(() => { 13 | api 14 | .post('/init') 15 | .then(() => Router.reload()) 16 | .catch((err) => setError(err as Error)); 17 | }, []); 18 | 19 | if (error) 20 | return ( 21 | 22 | 23 | 24 | Algo de errado aconteceu. O Open RPG não pôde concluir a configuração inicial 25 | do sistema. Confira se o banco de dados está corretamente vinculado na Heroku 26 | e faça o redeploy. Caso esse erro persista, contate o(a) administrador(a) do 27 | Open RPG no{' '} 28 | 32 | GitHub 33 | 34 | . 35 | 36 | 37 | 38 | 39 | Descrição do erro: 40 |
41 | {error.toString()} 42 | 43 |
44 |
45 | ); 46 | 47 | return ( 48 | 49 | 50 | Realizando configuração inicial... 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/ApplicationHead.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | type ApplicationHeadProps = { 4 | title?: string; 5 | children?: React.ReactChild; 6 | }; 7 | 8 | export default function ApplicationHead({ title, children }: ApplicationHeadProps) { 9 | return ( 10 | 11 | 15 | 16 | {`${title || ''} - Open RPG`} 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/BottomTextInput.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, InputHTMLAttributes } from 'react'; 2 | 3 | export default function BottomTextInput( 4 | props: DetailedHTMLProps, HTMLInputElement> 5 | ) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/CustomSpinner.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | 3 | const style: CSSProperties = { 4 | width: '1.5rem', 5 | height: '1.5rem', 6 | verticalAlign: '-0.5rem', 7 | }; 8 | 9 | export default function CustomSpinner() { 10 | return
; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/DataContainer.tsx: -------------------------------------------------------------------------------- 1 | import Button from 'react-bootstrap/Button'; 2 | import type { ColProps } from 'react-bootstrap/Col'; 3 | import Col from 'react-bootstrap/Col'; 4 | import DropdownButton from 'react-bootstrap/DropdownButton'; 5 | import Row from 'react-bootstrap/Row'; 6 | import CustomSpinner from './CustomSpinner'; 7 | 8 | interface DataContainerProps extends ColProps { 9 | title: string; 10 | children?: React.ReactNode; 11 | addButton?: { 12 | name?: string; 13 | type?: 'button' | 'dropdown'; 14 | onAdd?: () => void; 15 | children?: React.ReactNode; 16 | disabled?: boolean; 17 | }; 18 | htmlFor?: string; 19 | outline?: boolean; 20 | } 21 | 22 | export default function DataContainer({ 23 | title, 24 | children, 25 | addButton, 26 | htmlFor, 27 | outline, 28 | ...props 29 | }: DataContainerProps) { 30 | const _title = htmlFor ? : <>{title}; 31 | 32 | return ( 33 | 34 | 35 | 36 | {addButton ? ( 37 | 38 | 39 | {_title} 40 | 41 | 42 | {addButton.type === 'dropdown' ? ( 43 | 49 | {addButton.children} 50 | 51 | ) : ( 52 | 59 | )} 60 | 61 |
62 |
63 | ) : ( 64 | 65 | {_title} 66 |
67 |
68 | )} 69 | {children} 70 | 71 |
72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/ErrorToast.tsx: -------------------------------------------------------------------------------- 1 | import Toast from 'react-bootstrap/Toast'; 2 | 3 | type ErrorToastProps = { 4 | id: number; 5 | err?: any; 6 | onClose: (ev?: React.MouseEvent | React.KeyboardEvent | undefined) => void; 7 | }; 8 | 9 | export default function ErrorToast({ err, onClose }: ErrorToastProps) { 10 | let message = err.message; 11 | const responseData = err.response?.data; 12 | if (responseData) { 13 | const responseMessage = responseData.message; 14 | if (responseMessage) message = `Mensagem do servidor: ${responseMessage}`; 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | Erro 24 | 25 | {message} 26 | 27 | ); 28 | } 29 | 30 | export type { ErrorToastProps }; 31 | -------------------------------------------------------------------------------- /src/components/ErrorToastContainer.tsx: -------------------------------------------------------------------------------- 1 | import ToastContainer from 'react-bootstrap/ToastContainer'; 2 | import type { ErrorToastProps } from './ErrorToast'; 3 | import ErrorToast from './ErrorToast'; 4 | 5 | type ErrorToastContainerProps = { 6 | toasts: ErrorToastProps[]; 7 | }; 8 | 9 | export default function ErrorToastContainer({ toasts }: ErrorToastContainerProps) { 10 | return ( 11 | 12 | {toasts.map((toast) => ( 13 | 19 | ))} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Modals/AddDataModal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Container from 'react-bootstrap/Container'; 3 | import FormSelect from 'react-bootstrap/FormSelect'; 4 | import SheetModal from './SheetModal'; 5 | 6 | type AddDataModalProps = { 7 | show: boolean; 8 | onHide: () => void; 9 | onAddData: (id: number) => void; 10 | data: { id: number; name: string }[]; 11 | title: string; 12 | disabled?: boolean; 13 | }; 14 | 15 | export default function AddDataModal(props: AddDataModalProps) { 16 | const [value, setValue] = useState(0); 17 | 18 | useEffect(() => { 19 | if (props.data.length > 0) setValue(props.data[0].id); 20 | else setValue(0); 21 | }, [props.data]); 22 | 23 | return ( 24 | props.onAddData(value), 31 | disabled: props.data.length === 0 || props.disabled, 32 | }}> 33 | 34 | setValue(parseInt(ev.currentTarget.value))} 38 | disabled={props.data.length === 0}> 39 | {props.data.map((eq) => ( 40 | 43 | ))} 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Modals/AttributeEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Attribute } from '@prisma/client'; 2 | import { ChangeEventHandler } from 'react'; 3 | import { useEffect, useState } from 'react'; 4 | import Container from 'react-bootstrap/Container'; 5 | import FormCheck from 'react-bootstrap/FormCheck'; 6 | import FormControl from 'react-bootstrap/FormControl'; 7 | import FormGroup from 'react-bootstrap/FormGroup'; 8 | import FormLabel from 'react-bootstrap/FormLabel'; 9 | import SheetModal from './SheetModal'; 10 | 11 | const initialState: Attribute = { 12 | id: 0, 13 | name: '', 14 | rollable: false, 15 | color: '#ff0000', 16 | portrait: 'PRIMARY', 17 | visibleToAdmin: true, 18 | }; 19 | 20 | export default function AttributeEditorModal(props: EditorModalProps) { 21 | const [attribute, setAttribute] = useState(initialState); 22 | 23 | useEffect(() => { 24 | if (!props.data) return; 25 | setAttribute(props.data); 26 | }, [props.data]); 27 | 28 | function hide() { 29 | setAttribute(initialState); 30 | props.onHide(); 31 | } 32 | 33 | const onAttributeColorChange: ChangeEventHandler = (ev) => { 34 | const color = ev.target.value.slice(1); 35 | 36 | const red = parseInt(`0x${color.slice(0, 2)}`); 37 | const green = parseInt(`0x${color.slice(2, 4)}`); 38 | const blue = parseInt(`0x${color.slice(4, 6)}`); 39 | 40 | const gray = (red + green + blue) / 3; 41 | if (gray >= 205) return; 42 | setAttribute((attr) => ({ ...attr, color: ev.target.value })); 43 | }; 44 | 45 | return ( 46 | { 54 | props.onSubmit(attribute); 55 | hide(); 56 | }, 57 | disabled: props.disabled, 58 | }}> 59 | 60 | 61 | Nome 62 | 67 | setAttribute((attr) => ({ ...attr, name: ev.target.value })) 68 | } 69 | /> 70 | 71 | 72 | Cor 73 | 79 | 80 | 84 | setAttribute((attr) => ({ ...attr, rollable: ev.target.checked })) 85 | } 86 | id='createAttributeRollable' 87 | label='Testável?' 88 | /> 89 | 93 | setAttribute((attr) => ({ ...attr, visibleToAdmin: ev.target.checked })) 94 | } 95 | id='createAttributeVisibleToAdmin' 96 | label='Visível no Painel do Mestre?' 97 | /> 98 | 99 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Modals/AttributeStatusEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Attribute, AttributeStatus } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormControl from 'react-bootstrap/FormControl'; 5 | import FormGroup from 'react-bootstrap/FormGroup'; 6 | import FormLabel from 'react-bootstrap/FormLabel'; 7 | import FormSelect from 'react-bootstrap/FormSelect'; 8 | import SheetModal from './SheetModal'; 9 | 10 | type ModalProps = EditorModalProps & { 11 | attributes: Attribute[]; 12 | }; 13 | 14 | export default function AttributeStatusEditorModal(props: ModalProps) { 15 | const initialState: AttributeStatus = { 16 | id: 0, 17 | name: '', 18 | attribute_id: props.attributes[0]?.id || 0, 19 | }; 20 | 21 | const [attributeStatus, setAttributeStatus] = useState(initialState); 22 | 23 | useEffect(() => { 24 | if (!props.data) return; 25 | setAttributeStatus(props.data); 26 | }, [props.data]); 27 | 28 | function hide() { 29 | setAttributeStatus(initialState); 30 | props.onHide(); 31 | } 32 | 33 | return ( 34 | { 46 | props.onSubmit(attributeStatus); 47 | hide(); 48 | }, 49 | disabled: props.attributes.length === 0 || props.disabled, 50 | }}> 51 | 52 | 53 | Nome 54 | 59 | setAttributeStatus((attr) => ({ ...attr, name: ev.target.value })) 60 | } 61 | /> 62 | 63 | 64 | Atributo 65 | 69 | setAttributeStatus((attr) => ({ 70 | ...attr, 71 | attribute_id: parseInt(ev.target.value), 72 | })) 73 | }> 74 | {props.attributes.map((attr) => ( 75 | 78 | ))} 79 | 80 | 81 | 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/Modals/CharacteristicEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Characteristic } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormControl from 'react-bootstrap/FormControl'; 5 | import FormGroup from 'react-bootstrap/FormGroup'; 6 | import FormCheck from 'react-bootstrap/FormCheck'; 7 | import FormLabel from 'react-bootstrap/FormLabel'; 8 | import SheetModal from './SheetModal'; 9 | 10 | const initialState: Characteristic = { 11 | id: 0, 12 | name: '', 13 | visibleToAdmin: false, 14 | }; 15 | 16 | export default function CharacteristicEditorModal( 17 | props: EditorModalProps 18 | ) { 19 | const [characteristic, setCharacteristic] = useState(initialState); 20 | 21 | useEffect(() => { 22 | if (!props.data) return; 23 | setCharacteristic(props.data); 24 | }, [props.data]); 25 | 26 | function hide() { 27 | setCharacteristic(initialState); 28 | props.onHide(); 29 | } 30 | 31 | return ( 32 | { 38 | props.onSubmit(characteristic); 39 | hide(); 40 | }, 41 | disabled: props.disabled, 42 | }} 43 | show={props.show} 44 | onHide={hide}> 45 | 46 | 47 | Nome 48 | setCharacteristic((i) => ({ ...i, name: ev.target.value }))} 53 | /> 54 | 55 | 59 | setCharacteristic((char) => ({ ...char, visibleToAdmin: ev.target.checked })) 60 | } 61 | id='createCharacteristicVisibleToAdmin' 62 | label='Visível no Painel do Mestre?' 63 | /> 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Modals/CurrencyEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Currency } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormCheck from 'react-bootstrap/FormCheck'; 5 | import FormControl from 'react-bootstrap/FormControl'; 6 | import FormGroup from 'react-bootstrap/FormGroup'; 7 | import FormLabel from 'react-bootstrap/FormLabel'; 8 | import SheetModal from './SheetModal'; 9 | 10 | const initialState: Currency = { 11 | id: 0, 12 | name: '', 13 | visibleToAdmin: true, 14 | }; 15 | 16 | export default function CurrencyEditorModal(props: EditorModalProps) { 17 | const [currency, setCurrency] = useState(initialState); 18 | 19 | useEffect(() => { 20 | if (!props.data) return; 21 | setCurrency(props.data); 22 | }, [props.data]); 23 | 24 | function hide() { 25 | setCurrency(initialState); 26 | props.onHide(); 27 | } 28 | 29 | return ( 30 | { 36 | props.onSubmit(currency); 37 | hide(); 38 | }, 39 | disabled: props.disabled, 40 | }} 41 | show={props.show} 42 | onHide={hide}> 43 | 44 | 45 | Nome 46 | setCurrency((i) => ({ ...i, name: ev.target.value }))} 51 | /> 52 | 53 | 57 | setCurrency((curr) => ({ ...curr, visibleToAdmin: ev.target.checked })) 58 | } 59 | id='createCurrencyVisibleToAdmin' 60 | label='Visível no Painel do Mestre?' 61 | /> 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Modals/ExtraInfoEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtraInfo } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormControl from 'react-bootstrap/FormControl'; 5 | import FormGroup from 'react-bootstrap/FormGroup'; 6 | import FormLabel from 'react-bootstrap/FormLabel'; 7 | import SheetModal from './SheetModal'; 8 | 9 | const initialState: ExtraInfo = { 10 | id: 0, 11 | name: '', 12 | }; 13 | 14 | export default function ExtraInfoEditorModal(props: EditorModalProps) { 15 | const [info, setInfo] = useState(initialState); 16 | 17 | useEffect(() => { 18 | if (!props.data) return; 19 | setInfo(props.data); 20 | }, [props.data]); 21 | 22 | function hide() { 23 | setInfo(initialState); 24 | props.onHide(); 25 | } 26 | 27 | return ( 28 | { 34 | props.onSubmit(info); 35 | hide(); 36 | }, 37 | disabled: props.disabled, 38 | }} 39 | show={props.show} 40 | onHide={hide}> 41 | 42 | 43 | Nome 44 | setInfo((i) => ({ ...i, name: ev.target.value }))} 49 | /> 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Modals/GeneralDiceRollModal.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import Button from 'react-bootstrap/Button'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Container from 'react-bootstrap/Container'; 5 | import Image from 'react-bootstrap/Image'; 6 | import Row from 'react-bootstrap/Row'; 7 | import useDiceRoll from '../../hooks/useDiceRoll'; 8 | import { clamp } from '../../utils'; 9 | import type { DiceRequest } from '../../utils/dice'; 10 | import DiceRollModal from './DiceRollModal'; 11 | import SheetModal from './SheetModal'; 12 | 13 | type DiceOption = { 14 | num: number; 15 | roll: number; 16 | }; 17 | 18 | type GeneralDiceRollModalProps = { 19 | npcId?: number; 20 | }; 21 | 22 | const DEFAULT_ROLL = [{ num: 1, roll: 20 }]; 23 | 24 | export default function GeneralDiceRollModal(props: GeneralDiceRollModalProps) { 25 | const [show, setShow] = useState(false); 26 | 27 | const [diceRoll, rollDice] = useDiceRoll(props.npcId); 28 | const applyRef = useRef(false); 29 | const [dices, setDices] = useState([ 30 | { 31 | num: 0, 32 | roll: 4, 33 | }, 34 | { 35 | num: 0, 36 | roll: 6, 37 | }, 38 | { 39 | num: 0, 40 | roll: 8, 41 | }, 42 | { 43 | num: 0, 44 | roll: 10, 45 | }, 46 | { 47 | num: 0, 48 | roll: 12, 49 | }, 50 | { 51 | num: 0, 52 | roll: 20, 53 | }, 54 | ]); 55 | 56 | function reset() { 57 | const rollDices: DiceRequest[] = []; 58 | dices.map((dice) => { 59 | if (dice.num > 0) rollDices.push({ num: dice.num, roll: dice.roll }); 60 | }); 61 | if (rollDices.length > 0 && applyRef.current) { 62 | rollDice({ dices: rollDices }); 63 | applyRef.current = false; 64 | } 65 | setDices( 66 | dices.map((dice) => { 67 | dice.num = 0; 68 | return dice; 69 | }) 70 | ); 71 | } 72 | 73 | function setDice(index: number, coeff: number) { 74 | setDices( 75 | dices.map((dice, i) => { 76 | if (i === index) dice.num = clamp(dice.num + coeff, 0, 9); 77 | return dice; 78 | }) 79 | ); 80 | } 81 | 82 | return ( 83 | <> 84 | Dado Geral { 90 | if (ev.ctrlKey) return rollDice({ dices: DEFAULT_ROLL }); 91 | setShow(true); 92 | }} 93 | /> 94 | { 101 | applyRef.current = true; 102 | setShow(false); 103 | }, 104 | }} 105 | onHide={() => setShow(false)} 106 | centered> 107 | 108 | 109 | {dices.map((dice, index) => ( 110 | 111 | 112 | 113 | {`${dice.num 119 | 120 | 121 | 122 | 123 | 129 | 130 | 134 | {dice.num} 135 | 136 | 137 | 143 | 144 | 145 | 146 | ))} 147 | 148 | 149 | 150 | 151 | 152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/components/Modals/InfoEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Info } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormControl from 'react-bootstrap/FormControl'; 5 | import FormCheck from 'react-bootstrap/FormCheck'; 6 | import FormGroup from 'react-bootstrap/FormGroup'; 7 | import FormLabel from 'react-bootstrap/FormLabel'; 8 | import SheetModal from './SheetModal'; 9 | 10 | const initialState: Info = { 11 | id: 0, 12 | name: '', 13 | visibleToAdmin: false, 14 | }; 15 | 16 | export default function InfoEditorModal(props: EditorModalProps) { 17 | const [info, setInfo] = useState(initialState); 18 | 19 | useEffect(() => { 20 | if (!props.data) return; 21 | setInfo(props.data); 22 | }, [props.data]); 23 | 24 | function hide() { 25 | setInfo(initialState); 26 | props.onHide(); 27 | } 28 | 29 | return ( 30 | { 36 | props.onSubmit(info); 37 | hide(); 38 | }, 39 | disabled: props.disabled, 40 | }} 41 | show={props.show} 42 | onHide={hide}> 43 | 44 | 45 | Nome 46 | setInfo((i) => ({ ...i, name: ev.target.value }))} 51 | /> 52 | 53 | 57 | setInfo((info) => ({ ...info, visibleToAdmin: ev.target.checked })) 58 | } 59 | id='createInfoVisibleToAdmin' 60 | label='Visível no Painel do Mestre?' 61 | /> 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Modals/ItemEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Item } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormCheck from 'react-bootstrap/FormCheck'; 5 | import FormControl from 'react-bootstrap/FormControl'; 6 | import FormGroup from 'react-bootstrap/FormGroup'; 7 | import FormLabel from 'react-bootstrap/FormLabel'; 8 | import SheetModal from './SheetModal'; 9 | 10 | const initialState = { 11 | id: 0, 12 | name: '', 13 | description: '', 14 | weight: '0', 15 | visible: true, 16 | }; 17 | 18 | export default function ItemEditorModal(props: EditorModalProps) { 19 | const [item, setItem] = useState(initialState); 20 | 21 | useEffect(() => { 22 | if (!props.data) return; 23 | setItem({ ...props.data, weight: props.data.weight.toString() }); 24 | }, [props.data]); 25 | 26 | function hide() { 27 | setItem(initialState); 28 | props.onHide(); 29 | } 30 | 31 | return ( 32 | { 40 | props.onSubmit({ ...item, weight: Number(item.weight) }); 41 | hide(); 42 | }, 43 | disabled: props.disabled, 44 | }}> 45 | 46 | 47 | Nome 48 | setItem((it) => ({ ...it, name: ev.target.value }))} 53 | /> 54 | 55 | 56 | Descrição 57 | setItem((it) => ({ ...it, description: ev.target.value }))} 61 | /> 62 | 63 | 64 | Peso 65 | setItem((it) => ({ ...it, weight: ev.target.value }))} 70 | /> 71 | 72 | setItem((it) => ({ ...it, visible: ev.target.checked }))} 76 | id='createItemVisible' 77 | label='Visível?' 78 | /> 79 | 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Modals/PlayerAttributeEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Container from 'react-bootstrap/Container'; 3 | import FormControl from 'react-bootstrap/FormControl'; 4 | import FormGroup from 'react-bootstrap/FormGroup'; 5 | import FormLabel from 'react-bootstrap/FormLabel'; 6 | import SheetModal from './SheetModal'; 7 | 8 | type PlayerAttributeEditorModalProps = { 9 | value: { id: number; value: number; maxValue: number; show: boolean }; 10 | onHide: () => void; 11 | onSubmit: (value: number, maxValue: number) => void; 12 | disabled?: boolean; 13 | }; 14 | 15 | export default function PlayerAttributeEditorModal( 16 | props: PlayerAttributeEditorModalProps 17 | ) { 18 | const [value, setValue] = useState('0'); 19 | const [maxValue, setMaxValue] = useState('0'); 20 | 21 | useEffect(() => { 22 | if (props.value.id > 0) { 23 | setValue(props.value.value.toString()); 24 | setMaxValue(props.value.maxValue.toString()); 25 | } 26 | }, [props.value]); 27 | 28 | return ( 29 | props.onSubmit(parseInt(value) || 0, parseInt(maxValue) || 0), 37 | disabled: props.disabled, 38 | }}> 39 | 40 | 41 | Valor Atual: 42 | setValue(ev.currentTarget.value)} 47 | /> 48 | 49 | 50 | Valor Máximo: 51 | setMaxValue(ev.currentTarget.value)} 56 | /> 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Modals/PlayerAvatarModal.tsx: -------------------------------------------------------------------------------- 1 | import type { ChangeEvent } from 'react'; 2 | import { useContext, useState } from 'react'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Container from 'react-bootstrap/Container'; 5 | import FormControl from 'react-bootstrap/FormControl'; 6 | import FormGroup from 'react-bootstrap/FormGroup'; 7 | import FormLabel from 'react-bootstrap/FormLabel'; 8 | import Row from 'react-bootstrap/Row'; 9 | import { ErrorLogger } from '../../contexts'; 10 | import api from '../../utils/api'; 11 | import SheetModal from './SheetModal'; 12 | 13 | type AvatarData = { 14 | id: number | null; 15 | link: string | null; 16 | name: string; 17 | }; 18 | 19 | type PlayerAvatarModalProps = { 20 | playerAvatars: { 21 | link: string | null; 22 | AttributeStatus: { 23 | id: number; 24 | name: string; 25 | } | null; 26 | }[]; 27 | show?: boolean; 28 | onHide?: () => void; 29 | onUpdate?: () => void; 30 | npcId?: number; 31 | }; 32 | 33 | export default function PlayerAvatarModal(props: PlayerAvatarModalProps) { 34 | const [avatars, setAvatars] = useState( 35 | props.playerAvatars.map((avatar) => { 36 | if (avatar.AttributeStatus) 37 | return { 38 | id: avatar.AttributeStatus.id, 39 | name: avatar.AttributeStatus.name, 40 | link: avatar.link, 41 | }; 42 | else 43 | return { 44 | id: null, 45 | name: 'Padrão', 46 | link: avatar.link, 47 | }; 48 | }) 49 | ); 50 | const [loading, setLoading] = useState(false); 51 | const logError = useContext(ErrorLogger); 52 | 53 | function onUpdateAvatar() { 54 | setLoading(true); 55 | api 56 | .post('/sheet/player/avatar', { avatarData: avatars, npcId: props.npcId }) 57 | .then(props.onUpdate) 58 | .catch(logError) 59 | .finally(() => { 60 | setLoading(false); 61 | props.onHide?.(); 62 | }); 63 | } 64 | 65 | function onAvatarChange( 66 | id: number | null, 67 | ev: ChangeEvent 68 | ) { 69 | setAvatars((avatars) => { 70 | const newAvatars = [...avatars]; 71 | const av = newAvatars.find((avatar) => avatar.id === id); 72 | if (av) av.link = ev.target.value || null; 73 | return newAvatars; 74 | }); 75 | } 76 | 77 | return ( 78 | 84 | 85 | 86 | 87 | Caso vá usar a extensão do OBS, é recomendado que as imagens estejam no 88 | tamanho de 420x600 (ou no aspecto de 7:10) e em formato PNG. 89 | 90 | 91 | {avatars.map((avatar) => ( 92 | 96 | Avatar ({avatar.name}) 97 | onAvatarChange(avatar.id, ev)} 101 | disabled={loading} 102 | /> 103 | 104 | ))} 105 | 106 | 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/Modals/SheetModal.tsx: -------------------------------------------------------------------------------- 1 | import type { FormEvent, MouseEvent } from 'react'; 2 | import Button from 'react-bootstrap/Button'; 3 | import Form from 'react-bootstrap/Form'; 4 | import Modal, { ModalProps } from 'react-bootstrap/Modal'; 5 | 6 | interface SheetModalProps extends ModalProps { 7 | children?: React.ReactNode; 8 | applyButton?: { 9 | name: string; 10 | onApply: (ev: MouseEvent | FormEvent | undefined) => any; 11 | disabled?: boolean; 12 | }; 13 | closeButton?: { 14 | name?: string; 15 | disabled?: boolean; 16 | }; 17 | bodyStyle?: React.CSSProperties; 18 | onCancel?: () => void; 19 | } 20 | 21 | export default function SheetModal({ 22 | title, 23 | children, 24 | applyButton, 25 | closeButton, 26 | bodyStyle, 27 | onCancel, 28 | ...props 29 | }: SheetModalProps) { 30 | function submit(ev: FormEvent) { 31 | ev.preventDefault(); 32 | applyButton?.onApply(ev); 33 | } 34 | 35 | return ( 36 | 37 | 38 | {title} 39 | 40 |
41 | {children} 42 | 43 | {applyButton && ( 44 | 47 | )} 48 | 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Modals/SkillEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Skill, Specialization } from '@prisma/client'; 2 | import type { ChangeEvent } from 'react'; 3 | import { useEffect, useState } from 'react'; 4 | import Container from 'react-bootstrap/Container'; 5 | import FormCheck from 'react-bootstrap/FormCheck'; 6 | import FormControl from 'react-bootstrap/FormControl'; 7 | import FormGroup from 'react-bootstrap/FormGroup'; 8 | import FormLabel from 'react-bootstrap/FormLabel'; 9 | import FormSelect from 'react-bootstrap/FormSelect'; 10 | import SheetModal from './SheetModal'; 11 | 12 | type ModalProps = EditorModalProps & { 13 | specializations: Specialization[]; 14 | }; 15 | 16 | const initialState: Skill = { 17 | id: 0, 18 | name: '', 19 | specialization_id: 0, 20 | mandatory: false, 21 | startValue: 0, 22 | visibleToAdmin: false, 23 | }; 24 | 25 | export default function SkillEditorModal(props: ModalProps) { 26 | const [skill, setSkill] = useState(initialState); 27 | 28 | useEffect(() => { 29 | if (!props.data) return; 30 | setSkill(props.data); 31 | }, [props.data]); 32 | 33 | function onValueChange(ev: ChangeEvent) { 34 | const aux = ev.target.value; 35 | let newValue = parseInt(aux); 36 | 37 | if (aux.length === 0) newValue = 0; 38 | else if (isNaN(newValue)) return; 39 | 40 | setSkill((sk) => ({ ...sk, startValue: newValue })); 41 | } 42 | 43 | function hide() { 44 | setSkill(initialState); 45 | props.onHide(); 46 | } 47 | 48 | return ( 49 | { 57 | props.onSubmit(skill); 58 | hide(); 59 | }, 60 | disabled: props.disabled, 61 | }}> 62 | 63 | 64 | Nome 65 | setSkill((sk) => ({ ...sk, name: ev.target.value }))} 70 | /> 71 | 72 | 73 | Especialização 74 | 78 | setSkill((sk) => ({ ...sk, specialization_id: parseInt(ev.target.value) })) 79 | }> 80 | 81 | {props.specializations.map((spec) => ( 82 | 85 | ))} 86 | 87 | 88 | 89 | Valor Inicial 90 | 95 | 96 | setSkill((sk) => ({ ...sk, mandatory: ev.target.checked }))} 100 | id='createSkillMandatory' 101 | label='Obrigatório?' 102 | /> 103 | 107 | setSkill((sk) => ({ ...sk, visibleToAdmin: ev.target.checked })) 108 | } 109 | id='createSkillVisibleToAdmin' 110 | label='Visível no Painel do Mestre?' 111 | /> 112 | 113 | 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/components/Modals/SpecEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Spec } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormCheck from 'react-bootstrap/FormCheck'; 5 | import FormControl from 'react-bootstrap/FormControl'; 6 | import FormGroup from 'react-bootstrap/FormGroup'; 7 | import FormLabel from 'react-bootstrap/FormLabel'; 8 | import SheetModal from './SheetModal'; 9 | 10 | const initialState: Spec = { 11 | id: 0, 12 | name: '', 13 | visibleToAdmin: true, 14 | }; 15 | 16 | export default function SpecEditorModal(props: EditorModalProps) { 17 | const [spec, setSpec] = useState(initialState); 18 | 19 | useEffect(() => { 20 | if (!props.data) return; 21 | setSpec(props.data); 22 | }, [props.data]); 23 | 24 | function hide() { 25 | setSpec(initialState); 26 | props.onHide(); 27 | } 28 | 29 | return ( 30 | { 36 | props.onSubmit(spec); 37 | hide(); 38 | }, 39 | disabled: props.disabled, 40 | }} 41 | show={props.show} 42 | onHide={hide}> 43 | 44 | 45 | Nome 46 | setSpec((i) => ({ ...i, name: ev.target.value }))} 51 | /> 52 | 53 | 57 | setSpec((spec) => ({ ...spec, visibleToAdmin: ev.target.checked })) 58 | } 59 | id='createSpecVisibleToAdmin' 60 | label='Visível no Painel do Mestre?' 61 | /> 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Modals/SpecializationEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import type { Specialization } from '@prisma/client'; 2 | import { useEffect, useState } from 'react'; 3 | import Container from 'react-bootstrap/Container'; 4 | import FormControl from 'react-bootstrap/FormControl'; 5 | import FormGroup from 'react-bootstrap/FormGroup'; 6 | import FormLabel from 'react-bootstrap/FormLabel'; 7 | import SheetModal from './SheetModal'; 8 | 9 | const initialState: Specialization = { 10 | id: 0, 11 | name: '', 12 | }; 13 | 14 | export default function SpecializationEditorModal( 15 | props: EditorModalProps 16 | ) { 17 | const [specialization, setSpecialization] = useState(initialState); 18 | 19 | useEffect(() => { 20 | if (!props.data) return; 21 | setSpecialization(props.data); 22 | }, [props.data]); 23 | 24 | function hide() { 25 | setSpecialization(initialState); 26 | props.onHide(); 27 | } 28 | 29 | return ( 30 | { 39 | props.onSubmit(specialization); 40 | hide(); 41 | }, 42 | disabled: props.disabled, 43 | }} 44 | show={props.show} 45 | onHide={hide}> 46 | 47 | 48 | Nome 49 | setSpecialization((i) => ({ ...i, name: ev.target.value }))} 54 | /> 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import { useEffect, useState } from 'react'; 4 | import Container from 'react-bootstrap/Container'; 5 | import FormCheck from 'react-bootstrap/FormCheck'; 6 | import Nav from 'react-bootstrap/Nav'; 7 | import BootstrapNavbar from 'react-bootstrap/Navbar'; 8 | import api from '../utils/api'; 9 | 10 | export default function Navbar() { 11 | const router = useRouter(); 12 | 13 | if (router.pathname.includes('/portrait')) return null; 14 | 15 | const isActive = (path: string) => router.pathname === path; 16 | const onPlayerSheet = router.pathname.includes('/sheet/player/'); 17 | const onNpcSheet = router.pathname.includes('/sheet/npc/'); 18 | const onAdminPanel = router.pathname.includes('/admin/'); 19 | 20 | return ( 21 | 22 | 23 | Open RPG 24 | 25 | 26 | 63 | 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | function ThemeManager() { 83 | const [darkMode, setDarkMode] = useState(true); 84 | 85 | useEffect(() => { 86 | const theme = localStorage.getItem('application_theme'); 87 | setDarkMode(theme ? theme === 'dark' : true); 88 | }, []); 89 | 90 | useEffect(() => { 91 | if (darkMode) { 92 | localStorage.setItem('application_theme', 'dark'); 93 | document.body.classList.remove('light'); 94 | document.body.classList.add('dark'); 95 | return; 96 | } 97 | localStorage.setItem('application_theme', 'light'); 98 | document.body.classList.remove('dark'); 99 | document.body.classList.add('light'); 100 | }, [darkMode]); 101 | 102 | return ( 103 | setDarkMode(ev.target.checked)} 109 | /> 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/components/Player/PlayerAnnotationField.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import Col from 'react-bootstrap/Col'; 3 | import FormControl from 'react-bootstrap/FormControl'; 4 | import Row from 'react-bootstrap/Row'; 5 | import { ErrorLogger } from '../../contexts'; 6 | import useExtendedState from '../../hooks/useExtendedState'; 7 | import api from '../../utils/api'; 8 | 9 | export default function PlayerAnnotationsField(props: { value?: string, npcId?: number }) { 10 | const [value, setValue, isClean] = useExtendedState(props.value || ''); 11 | 12 | const logError = useContext(ErrorLogger); 13 | 14 | function onValueBlur() { 15 | if (isClean()) return; 16 | api.post('/sheet/player/annotation', { value, npcId: props.npcId }).catch(logError); 17 | } 18 | 19 | return ( 20 | 21 | 22 | setValue(ev.currentTarget.value)} 28 | onBlur={onValueBlur} 29 | className='theme-element' 30 | /> 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Player/PlayerExtraInfoField.tsx: -------------------------------------------------------------------------------- 1 | import FormControl from 'react-bootstrap/FormControl'; 2 | import useExtendedState from '../../hooks/useExtendedState'; 3 | import api from '../../utils/api'; 4 | 5 | export default function PlayerExtraInfoField(props: { 6 | npcId?: number; 7 | extraInfoId: number; 8 | value: string; 9 | logError: (err: any) => void; 10 | }) { 11 | const [value, setValue, isClean] = useExtendedState(props.value); 12 | 13 | function onValueBlur() { 14 | if (isClean()) return; 15 | api 16 | .post('/sheet/player/extrainfo', { 17 | id: props.extraInfoId, 18 | value, 19 | npcId: props.npcId, 20 | }) 21 | .catch(props.logError); 22 | } 23 | 24 | return ( 25 | setValue(ev.currentTarget.value)} 30 | onBlur={onValueBlur} 31 | className='theme-element' 32 | /> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Player/PlayerSpecField.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ErrorLogger } from '../../contexts'; 3 | import useExtendedState from '../../hooks/useExtendedState'; 4 | import api from '../../utils/api'; 5 | import BottomTextInput from '../BottomTextInput'; 6 | 7 | type PlayerSpecFieldProps = { 8 | value: string; 9 | name: string; 10 | specId: number; 11 | npcId?: number; 12 | }; 13 | 14 | export default function PlayerSpecField(props: PlayerSpecFieldProps) { 15 | const [value, setValue, isClean] = useExtendedState(props.value); 16 | 17 | const logError = useContext(ErrorLogger); 18 | const specID = props.specId; 19 | 20 | function onValueBlur() { 21 | if (isClean()) return; 22 | api 23 | .post('/sheet/player/spec', { id: specID, value, npcId: props.npcId }) 24 | .catch(logError); 25 | } 26 | 27 | return ( 28 | setValue(ev.currentTarget.value)} 36 | /> 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Portrait/PortraitAvatarContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import Fade from 'react-bootstrap/Fade'; 3 | import Image from 'react-bootstrap/Image'; 4 | import type { SocketIO } from '../../hooks/useSocket'; 5 | import styles from '../../styles/modules/Portrait.module.scss'; 6 | import api from '../../utils/api'; 7 | import type { PlayerAttributeStatusChangeEvent } from '../../utils/socket'; 8 | 9 | export type PortraitAttributeStatus = { 10 | value: boolean; 11 | attribute_status_id: number; 12 | }[]; 13 | 14 | export default function PortraitAvatar(props: { 15 | attributeStatus: PortraitAttributeStatus; 16 | playerId: number; 17 | socket: SocketIO; 18 | }) { 19 | const [src, setSrc] = useState('#'); 20 | const [showAvatar, setShowAvatar] = useState(false); 21 | const [attributeStatus, setAttributeStatus] = useState(props.attributeStatus); 22 | const previousStatusID = useRef(Number.MAX_SAFE_INTEGER); 23 | 24 | useEffect(() => { 25 | const id = attributeStatus.find((stat) => stat.value)?.attribute_status_id || 0; 26 | previousStatusID.current = id; 27 | api 28 | .get(`/sheet/player/avatar/${id}`, { params: { playerID: props.playerId } }) 29 | .then((res) => setSrc(`${res.data.link}?v=${Date.now()}`)) 30 | .catch(() => setSrc('/avatar404.png')); 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, []); 33 | 34 | const socket_playerAttributeStatusChange = useRef( 35 | () => {} 36 | ); 37 | 38 | useEffect(() => { 39 | socket_playerAttributeStatusChange.current = (playerId, id, value) => { 40 | if (playerId !== props.playerId) return; 41 | const newStatus = [...attributeStatus]; 42 | 43 | const index = newStatus.findIndex((stat) => stat.attribute_status_id === id); 44 | if (index === -1) return; 45 | 46 | newStatus[index].value = value; 47 | 48 | const newStatusID = newStatus.find((stat) => stat.value)?.attribute_status_id || 0; 49 | setAttributeStatus(newStatus); 50 | 51 | if (newStatusID !== previousStatusID.current) { 52 | previousStatusID.current = newStatusID; 53 | api 54 | .get(`/sheet/player/avatar/${newStatusID}`, { 55 | params: { playerID: props.playerId }, 56 | }) 57 | .then((res) => { 58 | if (res.data.link === src.split('?')[0]) return; 59 | setShowAvatar(false); 60 | setSrc(`${res.data.link}?v=${Date.now()}`); 61 | }) 62 | .catch(() => setSrc('/avatar404.png')); 63 | } 64 | }; 65 | }); 66 | 67 | useEffect(() => { 68 | props.socket.on('playerAttributeStatusChange', (playerId, id, value) => 69 | socket_playerAttributeStatusChange.current(playerId, id, value) 70 | ); 71 | 72 | return () => { 73 | props.socket.off('playerAttributeStatusChange'); 74 | }; 75 | // eslint-disable-next-line react-hooks/exhaustive-deps 76 | }, [props.socket]); 77 | 78 | return ( 79 | 80 |
81 | Avatar setSrc('/avatar404.png')} 85 | onLoad={() => setShowAvatar(true)} 86 | className={styles.avatar} 87 | /> 88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/components/Portrait/PortraitSideAttributeContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import type { ControlPosition, DraggableData, DraggableEvent } from 'react-draggable'; 3 | import Draggable from 'react-draggable'; 4 | import type { SocketIO } from '../../hooks/useSocket'; 5 | import styles from '../../styles/modules/Portrait.module.scss'; 6 | import { clamp } from '../../utils'; 7 | import { getAttributeStyle } from '../../utils/style'; 8 | 9 | type PortraitSideAttribute = { 10 | value: number; 11 | show: boolean; 12 | Attribute: { 13 | id: number; 14 | name: string; 15 | color: string; 16 | }; 17 | } | null; 18 | 19 | const bounds = { 20 | bottom: 475, 21 | left: 5, 22 | top: 5, 23 | right: 215, 24 | }; 25 | 26 | export default function PortraitSideAttributeContainer(props: { 27 | socket: SocketIO; 28 | sideAttribute: PortraitSideAttribute; 29 | }) { 30 | const [sideAttribute, setSideAttribute] = useState(props.sideAttribute); 31 | const [position, setPosition] = useState({ x: 0, y: 0 }); 32 | 33 | useEffect(() => { 34 | setPosition( 35 | (JSON.parse( 36 | localStorage.getItem('side-attribute-pos') || 'null' 37 | ) as ControlPosition) || { x: 0, y: 420 } 38 | ); 39 | }, []); 40 | 41 | useEffect(() => { 42 | props.socket.on( 43 | 'playerAttributeChange', 44 | (playerId, attributeId, value, maxValue, show) => { 45 | setSideAttribute((attr) => { 46 | if (attr === null || attributeId !== attr.Attribute.id) return attr; 47 | return { value, show, Attribute: { ...attr.Attribute } }; 48 | }); 49 | } 50 | ); 51 | 52 | return () => { 53 | props.socket.off('playerAttributeChange'); 54 | }; 55 | // eslint-disable-next-line react-hooks/exhaustive-deps 56 | }, [props.socket]); 57 | 58 | const attributeStyle = useMemo( 59 | () => getAttributeStyle(sideAttribute?.Attribute.color || 'ffffff'), 60 | // eslint-disable-next-line react-hooks/exhaustive-deps 61 | [] 62 | ); 63 | 64 | if (!sideAttribute) return null; 65 | 66 | function onDragStop(_ev: DraggableEvent, data: DraggableData) { 67 | const pos = { 68 | x: clamp(data.x, bounds.left, bounds.right), 69 | y: clamp(data.y, bounds.top, bounds.bottom), 70 | }; 71 | setPosition(pos); 72 | localStorage.setItem('side-attribute-pos', JSON.stringify(pos)); 73 | } 74 | 75 | return ( 76 | 77 |
78 |
79 | 83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import type { SocketIO } from '../hooks/useSocket'; 3 | 4 | export const ErrorLogger = createContext<(err: any) => void>(() => {}); 5 | export const Socket = createContext(undefined as any); 6 | -------------------------------------------------------------------------------- /src/hooks/useAuthentication.ts: -------------------------------------------------------------------------------- 1 | import type { IronSessionData } from 'iron-session'; 2 | import { useEffect } from 'react'; 3 | import api from '../utils/api'; 4 | 5 | export default function useAuthentication( 6 | onPlayerReceived: (sessionData: IronSessionData['player']) => void 7 | ) { 8 | useEffect(() => { 9 | api.get('/player').then((res) => onPlayerReceived(res.data.player)); 10 | // eslint-disable-next-line react-hooks/exhaustive-deps 11 | }, []); 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/useDiceRoll.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import type { DiceRoll, DiceRollModalProps } from '../components/Modals/DiceRollModal'; 3 | 4 | export type DiceRollEvent = (diceRoll: DiceRoll) => void; 5 | 6 | export default function useDiceRoll(npcId?: number): [DiceRollModalProps, DiceRollEvent] { 7 | const [diceRoll, setDiceRoll] = useState({ dices: null }); 8 | 9 | const lastRoll = useRef({ dices: null }); 10 | 11 | const onDiceRoll: DiceRollEvent = ({ dices, resolverKey, onResult }) => { 12 | const roll = { dices, resolverKey, onResult }; 13 | lastRoll.current = roll; 14 | setDiceRoll(roll); 15 | }; 16 | 17 | const diceRollModalProps: DiceRollModalProps = { 18 | ...diceRoll, 19 | onHide: () => setDiceRoll({ dices: null }), 20 | onRollAgain: () => setDiceRoll(lastRoll.current), 21 | npcId, 22 | }; 23 | 24 | return [diceRollModalProps, onDiceRoll]; 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useExtendedState.ts: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import type { Dispatch, SetStateAction } from 'react'; 3 | 4 | export default function useExtendedState( 5 | initialState: T | (() => T) 6 | ): [T, Dispatch>, () => boolean] { 7 | const [value, setValue] = useState(initialState); 8 | const lastValue = useRef( 9 | initialState instanceof Function ? initialState() : initialState 10 | ); 11 | 12 | const isClean = () => { 13 | const clean = value === lastValue.current; 14 | if (!clean) lastValue.current = value; 15 | return clean; 16 | }; 17 | 18 | return [value, setValue, isClean]; 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useSocket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import io, { Socket } from 'socket.io-client'; 3 | import api from '../utils/api'; 4 | import type { ClientToServerEvents, ServerToClientEvents } from '../utils/socket'; 5 | 6 | export type SocketIO = Socket; 7 | 8 | export default function useSocket(roomJoin: string) { 9 | const [socket, setSocket] = useState(null); 10 | 11 | useEffect(() => { 12 | api.get('/socket').then(() => { 13 | const socket: SocketIO = io(); 14 | socket.on('connect', () => { 15 | socket.emit('roomJoin', roomJoin); 16 | setSocket(socket); 17 | }); 18 | socket.on('connect_error', (err) => console.error('Socket.IO error:', err)); 19 | socket.on('disconnect', (reason) => 20 | console.warn('Socket.IO disconnect. Reason:', reason) 21 | ); 22 | }); 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, []); 25 | 26 | return socket; 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useToast.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import type { ErrorToastProps } from '../components/ErrorToast'; 3 | 4 | export default function useToast(): [ErrorToastProps[], (err: any) => void] { 5 | const [toasts, setToasts] = useState([]); 6 | 7 | function addToast(err: any) { 8 | const id = Date.now(); 9 | setToasts((toasts) => [...toasts, { id, err, onClose: () => removeToast(id) }]); 10 | } 11 | 12 | function removeToast(id: number) { 13 | setToasts((toasts) => toasts.filter((e) => e.id !== id)); 14 | } 15 | 16 | return [toasts, addToast]; 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Container from 'react-bootstrap/Container'; 2 | import Row from 'react-bootstrap/Row'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Link from 'next/link'; 5 | import styles from '../styles/modules/Home.module.scss'; 6 | import ApplicationHead from '../components/ApplicationHead'; 7 | 8 | export default function notFound() { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 |
18 | 404 19 |
20 |
Essa página não foi encontrada.
21 |
22 | 23 | Voltar para o início 24 | 25 |
26 | 27 |
28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/500.tsx: -------------------------------------------------------------------------------- 1 | import Container from 'react-bootstrap/Container'; 2 | import Row from 'react-bootstrap/Row'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Link from 'next/link'; 5 | import styles from '../styles/modules/Home.module.scss'; 6 | import ApplicationHead from '../components/ApplicationHead'; 7 | 8 | export default function internalServerError() { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 |
18 | 500 19 |
20 |
21 | Algo de errado aconteceu. Contate o mestre ou o administrador do aplicativo. 22 |
23 |
24 | 25 | Voltar para o início 26 | 27 |
28 | 29 |
30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import SSRProvider from 'react-bootstrap/SSRProvider'; 3 | import Navbar from '../components/Navbar'; 4 | import '../styles/globals.scss'; 5 | 6 | export default function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/api/config.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import prisma from '../../utils/database'; 3 | import type { NextApiResponseServerIO } from '../../utils/socket'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 6 | const name: string = req.body.name; 7 | let value: string | undefined = undefined; 8 | 9 | if (req.body.value !== undefined) { 10 | if (typeof req.body.value === 'object') value = JSON.stringify(req.body.value); 11 | else value = String(req.body.value); 12 | } 13 | 14 | if (!name || value === undefined) { 15 | res.status(400).end(); 16 | return; 17 | } 18 | 19 | await prisma.config.upsert({ 20 | where: { name }, 21 | update: { value }, 22 | create: { name, value }, 23 | }); 24 | 25 | res.end(); 26 | 27 | const behaviour = customBehaviours.get(name); 28 | if (behaviour) behaviour(res, value); 29 | } 30 | 31 | const customBehaviours = new Map< 32 | string, 33 | (res: NextApiResponseServerIO, value: any) => void 34 | >([ 35 | ['environment', (res, value) => res.socket.server.io?.emit('environmentChange', value)], 36 | ]); -------------------------------------------------------------------------------- /src/pages/api/login.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../utils/database'; 3 | import { compare } from '../../utils/encryption'; 4 | import { sessionAPI } from '../../utils/session'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | res.status(404).end(); 9 | } 10 | 11 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 12 | const username: string | undefined = req.body.username; 13 | const plainPassword: string | undefined = req.body.password; 14 | 15 | if (!username || !plainPassword) { 16 | res.status(400).send({ message: 'Usuário ou senha está em branco.' }); 17 | return; 18 | } 19 | 20 | const user = await database.player.findFirst({ 21 | where: { username }, 22 | select: { 23 | id: true, 24 | password: true, 25 | role: true, 26 | }, 27 | }); 28 | 29 | if (!user || !user.password) { 30 | res.status(401).send({ message: 'Usuário ou senha estão incorretos.' }); 31 | return; 32 | } 33 | 34 | const isValidPassword = compare(plainPassword, user.password); 35 | 36 | if (!isValidPassword) { 37 | res.status(401).send({ message: 'Usuário ou senha estão incorretos.' }); 38 | return; 39 | } 40 | 41 | const isAdmin = user.role === 'ADMIN'; 42 | 43 | req.session.player = { 44 | id: user.id, 45 | admin: isAdmin, 46 | }; 47 | await req.session.save(); 48 | 49 | res.send({ admin: isAdmin }); 50 | } 51 | 52 | export default sessionAPI(handler); 53 | -------------------------------------------------------------------------------- /src/pages/api/player.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { sessionAPI } from '../../utils/session'; 3 | 4 | async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method === 'GET') return handleGet(req, res); 6 | if (req.method === 'DELETE') return handleDelete(req, res); 7 | res.status(404).end(); 8 | } 9 | 10 | function handleGet(req: NextApiRequest, res: NextApiResponse) { 11 | res.send({ player: req.session.player }); 12 | } 13 | 14 | function handleDelete(req: NextApiRequest, res: NextApiResponse) { 15 | req.session.destroy(); 16 | res.end(); 17 | } 18 | 19 | export default sessionAPI(handler); 20 | -------------------------------------------------------------------------------- /src/pages/api/sheet/attribute/index.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '@prisma/client'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import database from '../../../../utils/database'; 4 | import { sessionAPI } from '../../../../utils/session'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | if (req.method === 'PUT') return handlePut(req, res); 9 | if (req.method === 'DELETE') return handleDelete(req, res); 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 14 | const player = req.session.player; 15 | 16 | if (!player || !player.admin) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const id: number | undefined = req.body.id; 22 | const name: string | undefined = req.body.name; 23 | const color: string | undefined = req.body.color; 24 | const rollable: boolean | undefined = req.body.rollable; 25 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 26 | 27 | if (!id || !name || !color || rollable === undefined || visibleToAdmin === undefined) { 28 | res.status(400).send({ 29 | message: 'ID, nome, cor, rolável ou visível(mestre) do atributo estão em branco.', 30 | }); 31 | return; 32 | } 33 | 34 | await database.attribute.update({ 35 | where: { id }, 36 | data: { name, color, rollable, visibleToAdmin }, 37 | }); 38 | 39 | res.end(); 40 | } 41 | 42 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 43 | const player = req.session.player; 44 | 45 | if (!player || !player.admin) { 46 | res.status(401).end(); 47 | return; 48 | } 49 | 50 | const name: string | undefined = req.body.name; 51 | const color: string | undefined = req.body.color; 52 | const rollable: boolean | undefined = req.body.rollable; 53 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 54 | 55 | if (!name || !color || rollable === undefined || visibleToAdmin === undefined) { 56 | res.status(400).send({ 57 | message: 'Nome, cor, rolável ou visível(mestre) do atributo estão em branco.', 58 | }); 59 | return; 60 | } 61 | 62 | const [attr, players] = await database.$transaction([ 63 | database.attribute.create({ data: { name, color, rollable, visibleToAdmin } }), 64 | database.player.findMany({ 65 | where: { role: { in: ['PLAYER', 'NPC'] } }, 66 | select: { id: true }, 67 | }), 68 | ]); 69 | 70 | if (players.length > 0) { 71 | await database.playerAttribute.createMany({ 72 | data: players.map((player) => { 73 | return { 74 | attribute_id: attr.id, 75 | player_id: player.id, 76 | value: 0, 77 | maxValue: 0, 78 | }; 79 | }), 80 | }); 81 | } 82 | 83 | res.send({ id: attr.id }); 84 | } 85 | 86 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 87 | const player = req.session.player; 88 | 89 | if (!player || !player.admin) { 90 | res.status(401).end(); 91 | return; 92 | } 93 | 94 | const id: number | undefined = req.body.id; 95 | 96 | if (!id) { 97 | res.status(401).send({ message: 'ID do atributo está em branco.' }); 98 | return; 99 | } 100 | 101 | try { 102 | await database.attribute.delete({ where: { id } }); 103 | res.end(); 104 | } catch (err) { 105 | if (err instanceof Prisma.PrismaClientKnownRequestError) { 106 | if (err.code === 'P2003') { 107 | res.status(400).send({ 108 | message: 109 | 'Não foi possível remover esse atributo pois ainda há algum status de atributo usando-a.', 110 | }); 111 | return; 112 | } 113 | } 114 | res.status(500).end(); 115 | } 116 | } 117 | 118 | export default sessionAPI(handler); 119 | -------------------------------------------------------------------------------- /src/pages/api/sheet/attribute/portrait.ts: -------------------------------------------------------------------------------- 1 | import { PortraitAttribute } from '@prisma/client'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import prisma from '../../../../utils/database'; 4 | import { sessionAPI } from '../../../../utils/session'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | res.status(404).end(); 9 | } 10 | 11 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 12 | const player = req.session.player; 13 | 14 | if (!player || !player.admin) { 15 | res.status(401).end(); 16 | return; 17 | } 18 | 19 | type Attribute = { id: number; portrait: PortraitAttribute | null }; 20 | 21 | const primary: Attribute[] | undefined = req.body.primary; 22 | const secondary: Attribute | null | undefined = req.body.secondary; 23 | 24 | if (primary === undefined || secondary === undefined) { 25 | res.status(400).send({ message: 'Atributo primário ou secundário estão vazios.' }); 26 | return; 27 | } 28 | 29 | const primaryIds = primary.map((p) => p.id); 30 | 31 | await prisma.attribute.updateMany({ data: { portrait: null } }); 32 | await prisma.attribute.updateMany({ 33 | where: { id: { in: primaryIds } }, 34 | data: { portrait: 'PRIMARY' }, 35 | }); 36 | if (secondary !== null) 37 | await prisma.attribute.update({ 38 | where: { id: secondary.id }, 39 | data: { portrait: 'SECONDARY' }, 40 | }); 41 | 42 | res.end(); 43 | } 44 | 45 | export default sessionAPI(handler); 46 | -------------------------------------------------------------------------------- /src/pages/api/sheet/attribute/status.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const id: number | undefined = req.body.id; 21 | const name: string | undefined = req.body.name; 22 | const attribute_id: number | undefined = req.body.attribute_id; 23 | 24 | if (!id || !name || attribute_id === undefined) { 25 | res 26 | .status(401) 27 | .send({ message: 'ID, nome ou ID de atributo do status estão em branco.' }); 28 | return; 29 | } 30 | 31 | await database.attributeStatus.update({ 32 | data: { name, attribute_id }, 33 | where: { id: id }, 34 | }); 35 | 36 | res.end(); 37 | } 38 | 39 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 40 | const player = req.session.player; 41 | 42 | if (!player || !player.admin) { 43 | res.status(401).end(); 44 | return; 45 | } 46 | 47 | const name: string | undefined = req.body.name; 48 | const attribute_id: number | undefined = req.body.attribute_id; 49 | 50 | if (!name || attribute_id === undefined) { 51 | res 52 | .status(401) 53 | .send({ message: 'Nome ou ID de atributo do status estão em branco.' }); 54 | return; 55 | } 56 | 57 | const [attributeStatus, players] = await database.$transaction([ 58 | database.attributeStatus.create({ data: { name, attribute_id } }), 59 | database.player.findMany({ 60 | where: { role: { in: ['PLAYER', 'NPC'] } }, 61 | select: { id: true }, 62 | }), 63 | ]); 64 | 65 | if (players.length > 0) { 66 | await database.$transaction([ 67 | database.playerAttributeStatus.createMany({ 68 | data: players.map((player) => { 69 | return { 70 | attribute_status_id: attributeStatus.id, 71 | player_id: player.id, 72 | value: false, 73 | }; 74 | }), 75 | }), 76 | database.playerAvatar.createMany({ 77 | data: players.map((player) => { 78 | return { 79 | attribute_status_id: attributeStatus.id, 80 | player_id: player.id, 81 | link: null, 82 | }; 83 | }), 84 | }), 85 | ]); 86 | } 87 | 88 | res.send({ id: attributeStatus.id }); 89 | } 90 | 91 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 92 | const player = req.session.player; 93 | 94 | if (!player || !player.admin) { 95 | res.status(401).end(); 96 | return; 97 | } 98 | 99 | const id: number | undefined = req.body.id; 100 | 101 | if (!id) { 102 | res.status(401).send({ message: 'ID do status está em branco.' }); 103 | return; 104 | } 105 | 106 | await database.attributeStatus.delete({ where: { id } }); 107 | 108 | res.end(); 109 | } 110 | 111 | export default sessionAPI(handler); 112 | -------------------------------------------------------------------------------- /src/pages/api/sheet/characteristic.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const id: number | undefined = req.body.id; 21 | const name: string | undefined = req.body.name; 22 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 23 | 24 | if (!id || !name || visibleToAdmin === undefined) { 25 | res 26 | .status(400) 27 | .send({ message: 'ID, nome ou visível(mestre) da característica está em branco.' }); 28 | return; 29 | } 30 | 31 | await database.characteristic.update({ where: { id }, data: { name, visibleToAdmin } }); 32 | 33 | res.end(); 34 | } 35 | 36 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 37 | const player = req.session.player; 38 | 39 | if (!player || !player.admin) { 40 | res.status(401).end(); 41 | return; 42 | } 43 | 44 | const name: string | undefined = req.body.name; 45 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 46 | 47 | if (!name || visibleToAdmin === undefined) { 48 | res 49 | .status(400) 50 | .send({ message: 'Nome ou visível(mestre) da característica está em branco.' }); 51 | return; 52 | } 53 | 54 | const [char, players] = await database.$transaction([ 55 | database.characteristic.create({ 56 | data: { name, visibleToAdmin }, 57 | select: { id: true }, 58 | }), 59 | database.player.findMany({ 60 | where: { role: { in: ['PLAYER', 'NPC'] } }, 61 | select: { id: true }, 62 | }), 63 | ]); 64 | 65 | if (players.length > 0) { 66 | await database.playerCharacteristic.createMany({ 67 | data: players.map((player) => { 68 | return { 69 | characteristic_id: char.id, 70 | player_id: player.id, 71 | value: 0, 72 | modifier: 0, 73 | }; 74 | }), 75 | }); 76 | } 77 | 78 | res.send({ id: char.id }); 79 | } 80 | 81 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 82 | const player = req.session.player; 83 | 84 | if (!player || !player.admin) { 85 | res.status(401).end(); 86 | return; 87 | } 88 | 89 | const id: number | undefined = req.body.id; 90 | 91 | if (!id) { 92 | res.status(401).send({ message: 'ID da característica está em branco.' }); 93 | return; 94 | } 95 | 96 | await database.characteristic.delete({ where: { id } }); 97 | 98 | res.end(); 99 | } 100 | 101 | export default sessionAPI(handler); 102 | -------------------------------------------------------------------------------- /src/pages/api/sheet/currency.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const id: number | undefined = req.body.id; 21 | const name: string | undefined = req.body.name; 22 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 23 | 24 | if (!id || !name || visibleToAdmin === undefined) { 25 | res 26 | .status(401) 27 | .send({ message: 'ID, nome ou visível(mestre) da moeda está em branco.' }); 28 | return; 29 | } 30 | 31 | await database.currency.update({ data: { name, visibleToAdmin }, where: { id } }); 32 | 33 | res.end(); 34 | } 35 | 36 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 37 | const player = req.session.player; 38 | 39 | if (!player || !player.admin) { 40 | res.status(401).end(); 41 | return; 42 | } 43 | 44 | const name: string | undefined = req.body.name; 45 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 46 | 47 | if (!name || visibleToAdmin === undefined) { 48 | res.status(401).send({ message: 'Nome ou visível(mestre) da moeda está em branco.' }); 49 | return; 50 | } 51 | 52 | const [currency, players] = await database.$transaction([ 53 | database.currency.create({ data: { name, visibleToAdmin }, select: { id: true } }), 54 | database.player.findMany({ 55 | where: { role: { in: ['PLAYER', 'NPC'] } }, 56 | select: { id: true }, 57 | }), 58 | ]); 59 | 60 | if (players.length > 0) { 61 | await database.playerCurrency.createMany({ 62 | data: players.map((player) => { 63 | return { 64 | currency_id: currency.id, 65 | player_id: player.id, 66 | value: '', 67 | }; 68 | }), 69 | }); 70 | } 71 | 72 | res.send({ id: currency.id }); 73 | } 74 | 75 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 76 | const player = req.session.player; 77 | 78 | if (!player || !player.admin) { 79 | res.status(401).end(); 80 | return; 81 | } 82 | 83 | const id = req.body.id; 84 | 85 | if (!id) { 86 | res.status(401).send({ message: 'ID da moeda está em branco.' }); 87 | return; 88 | } 89 | 90 | await database.currency.delete({ where: { id } }); 91 | 92 | res.end(); 93 | } 94 | 95 | export default sessionAPI(handler); 96 | -------------------------------------------------------------------------------- /src/pages/api/sheet/equipment.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | if (req.method === 'PUT') return handlePut(req, res); 9 | if (req.method === 'DELETE') return handleDelete(req, res); 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | 16 | if (!player || !player.admin) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const id: number | undefined = req.body.id; 22 | const name: string | undefined = req.body.name; 23 | const type: string | undefined = req.body.type; 24 | const damage: string | undefined = req.body.damage; 25 | const range: string | undefined = req.body.range; 26 | const attacks: string | undefined = req.body.attacks; 27 | const ammo: number | null | undefined = req.body.ammo; 28 | const visible: boolean | undefined = req.body.visible; 29 | 30 | if (!id) { 31 | } 32 | 33 | if ( 34 | !id || 35 | !name || 36 | !type || 37 | !damage || 38 | !range || 39 | !attacks || 40 | ammo === undefined || 41 | visible === undefined 42 | ) { 43 | res.status(400).send({ 44 | message: 45 | 'ID, nome, tipo, dano, alcance, ataques, munição ou visível do equipamento estão em branco.', 46 | }); 47 | return; 48 | } 49 | 50 | const eq = await database.equipment.update({ 51 | where: { id }, 52 | data: { name, type, damage, range, attacks, ammo, visible }, 53 | }); 54 | 55 | res.end(); 56 | 57 | res.socket.server.io?.emit('equipmentChange', eq); 58 | } 59 | 60 | async function handlePut(req: NextApiRequest, res: NextApiResponseServerIO) { 61 | const player = req.session.player; 62 | 63 | if (!player || !player.admin) { 64 | res.status(401).end(); 65 | return; 66 | } 67 | 68 | const name: string | undefined = req.body.name; 69 | const type: string | undefined = req.body.type; 70 | const damage: string | undefined = req.body.damage; 71 | const range: string | undefined = req.body.range; 72 | const attacks: string | undefined = req.body.attacks; 73 | const ammo: number | null | undefined = req.body.ammo; 74 | 75 | if (!name || !type || !damage || !range || !attacks || ammo === undefined) { 76 | res.status(400).send({ 77 | message: 78 | 'Nome, tipo, dano, alcance, ataques, munição ou visível do equipamento estão em branco.', 79 | }); 80 | return; 81 | } 82 | 83 | const eq = await database.equipment.create({ 84 | data: { name, ammo, attacks, damage, range, type, visible: true }, 85 | }); 86 | 87 | res.send({ id: eq.id }); 88 | 89 | res.socket.server.io?.emit('equipmentAdd', eq.id, eq.name); 90 | } 91 | 92 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 93 | const player = req.session.player; 94 | 95 | if (!player || !player.admin) { 96 | res.status(401).end(); 97 | return; 98 | } 99 | 100 | const id: number | undefined = req.body.id; 101 | 102 | if (!id) { 103 | res.status(401).send({ message: 'ID do equipamento está em branco.' }); 104 | return; 105 | } 106 | 107 | await database.equipment.delete({ where: { id } }); 108 | 109 | res.end(); 110 | 111 | res.socket.server.io?.emit('equipmentRemove', id); 112 | } 113 | 114 | export default sessionAPI(handler); 115 | -------------------------------------------------------------------------------- /src/pages/api/sheet/extrainfo.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const id: number | undefined = req.body.id; 21 | const name: string | undefined = req.body.name; 22 | 23 | if (!id || !name) { 24 | res.status(401).send({ message: 'ID ou nome da informação estão em branco.' }); 25 | return; 26 | } 27 | 28 | await database.extraInfo.update({ data: { name }, where: { id } }); 29 | 30 | res.end(); 31 | } 32 | 33 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 34 | const player = req.session.player; 35 | 36 | if (!player || !player.admin) { 37 | res.status(401).end(); 38 | return; 39 | } 40 | 41 | const name: string | undefined = req.body.name; 42 | 43 | if (!name) { 44 | res.status(401).send({ message: 'Nome da informação está em branco.' }); 45 | return; 46 | } 47 | 48 | const [extraInfo, players] = await database.$transaction([ 49 | database.extraInfo.create({ data: { name }, select: { id: true } }), 50 | database.player.findMany({ 51 | where: { role: { in: ['PLAYER', 'NPC'] } }, 52 | select: { id: true }, 53 | }), 54 | ]); 55 | 56 | if (players.length > 0) { 57 | await database.playerExtraInfo.createMany({ 58 | data: players.map((player) => { 59 | return { 60 | extra_info_id: extraInfo.id, 61 | player_id: player.id, 62 | value: '', 63 | }; 64 | }), 65 | }); 66 | } 67 | 68 | res.send({ id: extraInfo.id }); 69 | } 70 | 71 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 72 | const player = req.session.player; 73 | 74 | if (!player || !player.admin) { 75 | res.status(401).end(); 76 | return; 77 | } 78 | 79 | const id: number | undefined = req.body.id; 80 | 81 | if (!id) { 82 | res.status(401).send({ message: 'ID da informação está em branco.' }); 83 | return; 84 | } 85 | 86 | await database.extraInfo.delete({ where: { id } }); 87 | 88 | res.end(); 89 | } 90 | 91 | export default sessionAPI(handler); 92 | -------------------------------------------------------------------------------- /src/pages/api/sheet/info.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const id: number | undefined = req.body.id; 21 | const name: string | undefined = req.body.name; 22 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 23 | 24 | if (!id || !name || visibleToAdmin === undefined) { 25 | res 26 | .status(401) 27 | .send({ message: 'ID, nome ou visível(mestre) da informação estão em branco.' }); 28 | return; 29 | } 30 | 31 | await database.info.update({ data: { name, visibleToAdmin }, where: { id } }); 32 | 33 | res.end(); 34 | } 35 | 36 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 37 | const player = req.session.player; 38 | 39 | if (!player || !player.admin) { 40 | res.status(401).end(); 41 | return; 42 | } 43 | 44 | const name: string | undefined = req.body.name; 45 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 46 | 47 | if (!name || visibleToAdmin === undefined) { 48 | res 49 | .status(401) 50 | .send({ message: 'Nome ou visível(mestre) da informação está em branco.' }); 51 | return; 52 | } 53 | 54 | const [info, players] = await database.$transaction([ 55 | database.info.create({ data: { name, visibleToAdmin }, select: { id: true } }), 56 | database.player.findMany({ 57 | where: { role: { in: ['PLAYER', 'NPC'] } }, 58 | select: { id: true }, 59 | }), 60 | ]); 61 | 62 | if (players.length > 0) { 63 | await database.playerInfo.createMany({ 64 | data: players.map((player) => { 65 | return { 66 | info_id: info.id, 67 | player_id: player.id, 68 | value: '', 69 | }; 70 | }), 71 | }); 72 | } 73 | 74 | res.send({ id: info.id }); 75 | } 76 | 77 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 78 | const player = req.session.player; 79 | 80 | if (!player || !player.admin) { 81 | res.status(401).end(); 82 | return; 83 | } 84 | 85 | const id: number | undefined = req.body.id; 86 | 87 | if (!id) { 88 | res.status(401).send({ message: 'ID da informação está em branco.' }); 89 | return; 90 | } 91 | 92 | await database.info.delete({ where: { id } }); 93 | 94 | res.end(); 95 | } 96 | 97 | export default sessionAPI(handler); 98 | -------------------------------------------------------------------------------- /src/pages/api/sheet/item.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | if (req.method === 'PUT') return handlePut(req, res); 9 | if (req.method === 'DELETE') return handleDelete(req, res); 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | 16 | if (!player || !player.admin) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const id: number | undefined = req.body.id; 22 | const name: string | undefined = req.body.name; 23 | const description: string | undefined = req.body.description; 24 | const weight: number | undefined = req.body.weight; 25 | const visible: boolean | undefined = req.body.visible; 26 | 27 | if (!id || !name || !description || weight === undefined || visible === undefined) { 28 | res 29 | .status(401) 30 | .send({ message: 'ID, nome, descrição, peso ou visível do item estão em branco.' }); 31 | return; 32 | } 33 | 34 | const item = await database.item.update({ 35 | data: { name, description, weight, visible }, 36 | where: { id }, 37 | }); 38 | 39 | res.socket.server.io?.emit('itemChange', item); 40 | 41 | res.end(); 42 | } 43 | 44 | async function handlePut(req: NextApiRequest, res: NextApiResponseServerIO) { 45 | const player = req.session.player; 46 | 47 | if (!player || !player.admin) { 48 | res.status(401).end(); 49 | return; 50 | } 51 | 52 | const name: string | undefined = req.body.name; 53 | const description: string | undefined = req.body.description; 54 | const weight: number | undefined = req.body.weight; 55 | 56 | if (!name || !description || weight === undefined) { 57 | res.status(401).send({ message: 'Nome, descrição ou peso do item estão em branco.' }); 58 | return; 59 | } 60 | 61 | const item = await database.item.create({ 62 | data: { name, description, weight, visible: true }, 63 | }); 64 | 65 | res.send({ id: item.id }); 66 | 67 | res.socket.server.io?.emit('itemAdd', item.id, item.name); 68 | } 69 | 70 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 71 | const player = req.session.player; 72 | 73 | if (!player || !player.admin) { 74 | res.status(401).end(); 75 | return; 76 | } 77 | 78 | const id: number | undefined = req.body.id; 79 | 80 | if (!id) { 81 | res.status(401).send({ message: 'ID do item está em branco.' }); 82 | return; 83 | } 84 | 85 | await database.item.delete({ where: { id } }); 86 | 87 | res.end(); 88 | 89 | res.socket.server.io?.emit('itemRemove', id); 90 | } 91 | 92 | export default sessionAPI(handler); 93 | -------------------------------------------------------------------------------- /src/pages/api/sheet/npc.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | import { registerPlayerData } from '../register'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const name: string | undefined = req.body.name; 21 | 22 | if (!name) { 23 | res.status(401).send({ message: 'Nome do NPC está em branco.' }); 24 | return; 25 | } 26 | 27 | const npc = await database.player.create({ 28 | data: { role: 'NPC', name }, 29 | select: { id: true }, 30 | }); 31 | 32 | await registerPlayerData(npc.id); 33 | 34 | res.send({ id: npc.id }); 35 | } 36 | 37 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 38 | const player = req.session.player; 39 | 40 | if (!player || !player.admin) { 41 | res.status(401).end(); 42 | return; 43 | } 44 | 45 | const id: number | undefined = req.body.id; 46 | 47 | if (!id) { 48 | res.status(401).send({ message: 'ID do NPC está em branco.' }); 49 | return; 50 | } 51 | 52 | await database.player.delete({ where: { id } }); 53 | 54 | res.end(); 55 | } 56 | 57 | export default sessionAPI(handler); 58 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/annotation.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | res.status(404).end(); 8 | } 9 | 10 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 11 | const player = req.session.player; 12 | 13 | if (!player) { 14 | res.status(401).end(); 15 | return; 16 | } 17 | 18 | const value: string | undefined = req.body.value; 19 | const npcId: number | undefined = req.body.npcId; 20 | 21 | if (value === undefined) { 22 | res.status(400).send({ message: 'Valor da anotação está em branco.' }); 23 | return; 24 | } 25 | 26 | await database.playerNote.update({ 27 | data: { value }, 28 | where: { player_id: npcId ? npcId : player.id }, 29 | }); 30 | 31 | res.end(); 32 | } 33 | 34 | export default sessionAPI(handler); 35 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/attribute/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import prisma from '../../../../../utils/database'; 3 | import { sessionAPI } from '../../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../../utils/socket'; 5 | 6 | async function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method !== 'POST') { 8 | res.status(401).end(); 9 | return; 10 | } 11 | 12 | const player = req.session.player; 13 | const npcId: number | undefined = req.body.npcId; 14 | 15 | if (!player || (player.admin && !npcId)) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const attributeID: number | undefined = parseInt(req.body.id); 21 | 22 | if (!attributeID) { 23 | res.status(401).send({ message: 'ID do atributo está em branco.' }); 24 | return; 25 | } 26 | 27 | const value: number | undefined = req.body.value; 28 | const maxValue: number | undefined = req.body.maxValue; 29 | const show: boolean | undefined = req.body.show; 30 | 31 | const playerId = npcId ? npcId : player.id; 32 | 33 | const attr = await prisma.playerAttribute.update({ 34 | where: { 35 | player_id_attribute_id: { 36 | player_id: playerId, 37 | attribute_id: attributeID, 38 | }, 39 | }, 40 | data: { value, maxValue, show }, 41 | }); 42 | 43 | res.end(); 44 | 45 | res.socket.server.io?.to(`portrait${playerId}`).to('admin').emit( 46 | 'playerAttributeChange', 47 | playerId, 48 | attributeID, 49 | attr.value, 50 | attr.maxValue, 51 | attr.show 52 | ); 53 | } 54 | 55 | export default sessionAPI(handler); 56 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/attribute/status.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import prisma from '../../../../../utils/database'; 3 | import { sessionAPI } from '../../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../../utils/socket'; 5 | 6 | async function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method !== 'POST') { 8 | res.status(401).end(); 9 | return; 10 | } 11 | 12 | const player = req.session.player; 13 | const npcId: number | undefined = req.body.npcId; 14 | 15 | if (!player || (player.admin && !npcId)) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const statusID: number | undefined = parseInt(req.body.attrStatusID); 21 | const value: boolean | undefined = req.body.value; 22 | 23 | if (!statusID || value === undefined) { 24 | res.status(401).send({ message: 'ID ou valor do status está em branco.' }); 25 | return; 26 | } 27 | 28 | const playerId = npcId ? npcId : player.id; 29 | 30 | await prisma.playerAttributeStatus.update({ 31 | where: { 32 | player_id_attribute_status_id: { 33 | player_id: playerId, 34 | attribute_status_id: statusID, 35 | }, 36 | }, 37 | data: { value }, 38 | }); 39 | 40 | res.end(); 41 | 42 | res.socket.server.io?.emit('playerAttributeStatusChange', playerId, statusID, value); 43 | } 44 | 45 | export default sessionAPI(handler); 46 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/avatar/[attrStatusID].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import prisma from '../../../../../utils/database'; 3 | import { sessionAPI } from '../../../../../utils/session'; 4 | 5 | async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'GET') { 7 | res.status(404).end(); 8 | return; 9 | } 10 | 11 | const playerID = parseInt(req.query.playerID as string) || req.session.player?.id; 12 | const statusID = parseInt(req.query.attrStatusID as string) || null; 13 | 14 | if (!playerID) { 15 | res.status(401).end(); 16 | return; 17 | } 18 | 19 | let avatar = await prisma.playerAvatar.findFirst({ 20 | where: { 21 | player_id: playerID, 22 | attribute_status_id: statusID, 23 | }, 24 | select: { link: true }, 25 | }); 26 | 27 | if (avatar === null || avatar.link === null) { 28 | if (statusID === null) { 29 | res.status(404).end(); 30 | return; 31 | } 32 | 33 | const defaultAvatar = await prisma.playerAvatar.findFirst({ 34 | where: { 35 | player_id: playerID, 36 | attribute_status_id: null, 37 | }, 38 | select: { link: true }, 39 | }); 40 | 41 | if (defaultAvatar === null || defaultAvatar.link === null) { 42 | res.status(404).end(); 43 | return; 44 | } 45 | 46 | avatar = defaultAvatar; 47 | } 48 | 49 | res.send({ link: avatar.link }); 50 | } 51 | 52 | export default sessionAPI(handler); 53 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import prisma from '../../../../../utils/database'; 3 | import { sessionAPI } from '../../../../../utils/session'; 4 | 5 | export type AvatarData = { 6 | id: number | null; 7 | link: string | null; 8 | }; 9 | 10 | async function handler(req: NextApiRequest, res: NextApiResponse) { 11 | if (req.method !== 'POST') return; 12 | 13 | const player = req.session.player; 14 | const avatarData: AvatarData[] = req.body.avatarData; 15 | const npcId: number | undefined = req.body.npcId; 16 | 17 | if (!player || !avatarData || (player.admin && !npcId)) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const playerId = npcId ? npcId : player.id; 23 | 24 | const avatars = await prisma.playerAvatar.findMany({ 25 | where: { player_id: playerId }, 26 | select: { id: true, attribute_status_id: true, link: true }, 27 | }); 28 | 29 | if (avatars.length !== avatarData.length) { 30 | res.status(401).end(); 31 | return; 32 | } 33 | 34 | await Promise.all( 35 | avatars.map((avatar) => { 36 | const statusID = avatar.attribute_status_id; 37 | const newAvatar = avatarData.find((av) => av.id === statusID); 38 | 39 | if (!newAvatar || newAvatar.link === avatar.link) return; 40 | 41 | return prisma.playerAvatar.update({ 42 | where: { id: avatar.id }, 43 | data: { link: newAvatar.link }, 44 | }); 45 | }) 46 | ); 47 | 48 | res.end(); 49 | } 50 | 51 | export default sessionAPI(handler); 52 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/characteristic.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') { 8 | return handlePost(req, res); 9 | } 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | const npcId: number | undefined = req.body.npcId; 16 | 17 | if (!player || (player.admin && !npcId)) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const id: number | undefined = parseInt(req.body.id); 23 | 24 | if (!id) { 25 | res.status(401).send({ message: 'Characteristic ID is undefined.' }); 26 | return; 27 | } 28 | 29 | const value: number | undefined = req.body.value; 30 | const modifier: number | undefined = req.body.modifier; 31 | 32 | const playerId = npcId ? npcId : player.id; 33 | 34 | const char = await database.playerCharacteristic.update({ 35 | data: { value, modifier }, 36 | where: { 37 | player_id_characteristic_id: { player_id: playerId, characteristic_id: id }, 38 | }, 39 | }); 40 | 41 | res.socket.server.io?.emit( 42 | 'playerCharacteristicChange', 43 | playerId, 44 | id, 45 | char.value, 46 | char.modifier 47 | ); 48 | 49 | res.end(); 50 | } 51 | 52 | export default sessionAPI(handler); 53 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/currency.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') { 8 | return handlePost(req, res); 9 | } 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | const npcId: number | undefined = req.body.npcId; 16 | 17 | if (!player || (player.admin && !npcId)) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const currencyID: number | undefined = req.body.id; 23 | const value: string | undefined = req.body.value; 24 | 25 | if (!currencyID || value === undefined) { 26 | res.status(401).send({ message: 'Currency ID or value is undefined.' }); 27 | return; 28 | } 29 | 30 | const playerId = npcId ? npcId : player.id; 31 | 32 | await database.playerCurrency.update({ 33 | data: { value }, 34 | where: { player_id_currency_id: { player_id: playerId, currency_id: currencyID } }, 35 | }); 36 | 37 | res.end(); 38 | 39 | res.socket.server.io?.emit('playerCurrencyChange', playerId, currencyID, value); 40 | } 41 | 42 | export default sessionAPI(handler); 43 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/equipment.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import prisma from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'GET') return handleGet(req, res); 8 | if (req.method === 'POST') return handlePost(req, res); 9 | if (req.method === 'PUT') return handlePut(req, res); 10 | if (req.method === 'DELETE') return handleDelete(req, res); 11 | res.status(404).send({ message: 'Supported methods: POST | PUT | DELETE' }); 12 | } 13 | 14 | async function handleGet(req: NextApiRequest, res: NextApiResponse) { 15 | const player = req.session.player; 16 | 17 | if (!player) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const playerId = parseInt(req.query.playerId as string); 23 | 24 | if (!playerId) { 25 | res.status(400).end(); 26 | return; 27 | } 28 | 29 | const pe = await prisma.playerEquipment.findMany({ 30 | where: { player_id: playerId }, 31 | select: { Equipment: true }, 32 | }); 33 | 34 | const equipments = pe.map((eq) => eq.Equipment); 35 | 36 | res.send({ equipments }); 37 | } 38 | 39 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 40 | const player = req.session.player; 41 | const npcId: number | undefined = req.body.npcId; 42 | 43 | if (!player || (player.admin && !npcId)) { 44 | res.status(401).end(); 45 | return; 46 | } 47 | 48 | const id: number | undefined = req.body.id; 49 | 50 | if (!id) { 51 | res.status(400).send({ message: 'Equipment ID is undefined.' }); 52 | return; 53 | } 54 | 55 | const currentAmmo: number | undefined = req.body.currentAmmo; 56 | 57 | const playerId = npcId ? npcId : player.id; 58 | 59 | await prisma.playerEquipment.update({ 60 | where: { player_id_equipment_id: { player_id: playerId, equipment_id: id } }, 61 | data: { currentAmmo }, 62 | }); 63 | 64 | res.end(); 65 | } 66 | 67 | async function handlePut(req: NextApiRequest, res: NextApiResponseServerIO) { 68 | const player = req.session.player; 69 | const npcId: number | undefined = req.body.npcId; 70 | 71 | if (!player || (player.admin && !npcId)) { 72 | res.status(401).end(); 73 | return; 74 | } 75 | 76 | const equipID = req.body.id; 77 | 78 | if (!equipID) { 79 | res.status(400).send({ message: 'Equipment ID is undefined.' }); 80 | return; 81 | } 82 | 83 | const playerId = npcId ? npcId : player.id; 84 | 85 | const equipment = await prisma.playerEquipment.create({ 86 | data: { 87 | currentAmmo: 0, 88 | player_id: playerId, 89 | equipment_id: equipID, 90 | }, 91 | select: { currentAmmo: true, Equipment: true }, 92 | }); 93 | 94 | res.send({ equipment }); 95 | 96 | res.socket.server.io 97 | ?.to('admin') 98 | .emit('playerEquipmentAdd', playerId, equipment.Equipment); 99 | } 100 | 101 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 102 | const player = req.session.player; 103 | const npcId: number | undefined = req.body.npcId; 104 | 105 | if (!player || (player.admin && !npcId)) { 106 | res.status(401).end(); 107 | return; 108 | } 109 | 110 | const equipID: number | undefined = req.body.id; 111 | 112 | if (!equipID) { 113 | res.status(400).send({ message: 'Equipment ID is undefined.' }); 114 | return; 115 | } 116 | 117 | const playerId = npcId ? npcId : player.id; 118 | 119 | await prisma.playerEquipment.delete({ 120 | where: { player_id_equipment_id: { player_id: playerId, equipment_id: equipID } }, 121 | }); 122 | 123 | res.end(); 124 | 125 | res.socket.server.io?.to('admin').emit('playerEquipmentRemove', playerId, equipID); 126 | } 127 | 128 | export default sessionAPI(handler); 129 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/extrainfo.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | res.status(404).end(); 8 | } 9 | 10 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 11 | const player = req.session.player; 12 | const npcId: number | undefined = req.body.npcId; 13 | 14 | if (!player || (player.admin && !npcId)) { 15 | res.status(401).end(); 16 | return; 17 | } 18 | 19 | const id: number | undefined = req.body.id; 20 | const value: string | undefined = req.body.value; 21 | 22 | if (!id || value === undefined) { 23 | res.status(400).send({ message: 'ID ou valor estão em branco.' }); 24 | return; 25 | } 26 | 27 | await database.playerExtraInfo.update({ 28 | data: { value }, 29 | where: { 30 | player_id_extra_info_id: { player_id: npcId ? npcId : player.id, extra_info_id: id }, 31 | }, 32 | }); 33 | 34 | res.end(); 35 | } 36 | 37 | export default sessionAPI(handler); 38 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'DELETE') return handleDelete(req, res); 8 | if (req.method === 'POST') return handlePost(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 13 | const player = req.session.player; 14 | const npcId: number | undefined = req.body.npcId; 15 | 16 | if (!player || (player.admin && !npcId)) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const name: string | undefined = req.body.name; 22 | const showName: boolean | undefined = req.body.showName; 23 | const maxLoad: number | undefined = req.body.maxLoad; 24 | const maxSlots: number | undefined = req.body.maxSlots; 25 | 26 | const playerId = npcId ? npcId : player.id; 27 | 28 | await database.player.update({ 29 | where: { id: playerId }, 30 | data: { name, showName, maxLoad, spellSlots: maxSlots }, 31 | }); 32 | 33 | res.end(); 34 | 35 | if (!npcId) { 36 | if (maxSlots !== undefined) 37 | res.socket.server.io?.emit('playerSpellSlotsChange', playerId, maxSlots); 38 | if (maxLoad !== undefined) 39 | res.socket.server.io?.emit('playerMaxLoadChange', playerId, maxLoad); 40 | } 41 | 42 | if (name !== undefined) res.socket.server.io?.emit('playerNameChange', playerId, name); 43 | if (showName !== undefined) 44 | res.socket.server.io?.emit('playerNameShowChange', playerId, showName); 45 | } 46 | 47 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 48 | const player = req.session.player; 49 | 50 | if (!player || !player.admin) { 51 | res.status(401).end(); 52 | return; 53 | } 54 | 55 | const playerID = req.body.id; 56 | 57 | if (!playerID) { 58 | res.status(400).send({ message: 'Player ID is undefined.' }); 59 | return; 60 | } 61 | 62 | await database.player.delete({ where: { id: playerID } }); 63 | 64 | res.end(); 65 | 66 | res.socket.server.io?.to(`player${playerID}`).emit('playerDelete'); 67 | } 68 | 69 | export default sessionAPI(handler); 70 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/info.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') { 8 | return handlePost(req, res); 9 | } 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | const npcId: number | undefined = req.body.npcId; 16 | 17 | if (!player || (player.admin && !npcId)) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const infoID: number | undefined = req.body.id; 23 | const value: string | undefined = req.body.value; 24 | 25 | if (!infoID || value === undefined) { 26 | res.status(401).send({ message: 'Info ID or value is undefined.' }); 27 | return; 28 | } 29 | 30 | const playerId = npcId ? npcId : player.id; 31 | 32 | await database.playerInfo.update({ 33 | data: { value }, 34 | where: { player_id_info_id: { player_id: playerId, info_id: infoID } }, 35 | }); 36 | 37 | res.end(); 38 | 39 | if (!npcId) res.socket.server.io?.emit('playerInfoChange', playerId, infoID, value); 40 | } 41 | 42 | export default sessionAPI(handler); 43 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/item.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import prisma from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'GET') return handleGet(req, res); 8 | if (req.method === 'POST') return handlePost(req, res); 9 | if (req.method === 'PUT') return handlePut(req, res); 10 | if (req.method === 'DELETE') return handleDelete(req, res); 11 | res.status(404).send({ message: 'Supported methods: POST | PUT | DELETE' }); 12 | } 13 | 14 | async function handleGet(req: NextApiRequest, res: NextApiResponseServerIO) { 15 | const player = req.session.player; 16 | 17 | if (!player) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const playerId = parseInt(req.query.playerId as string); 23 | 24 | if (!playerId) { 25 | res.status(400).end(); 26 | return; 27 | } 28 | 29 | const pe = await prisma.playerItem.findMany({ 30 | where: { player_id: playerId }, 31 | select: { Item: true }, 32 | }); 33 | 34 | const items = pe.map((eq) => eq.Item); 35 | 36 | res.send({ items }); 37 | } 38 | 39 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 40 | const player = req.session.player; 41 | const npcId: number | undefined = req.body.npcId; 42 | 43 | if (!player || (player.admin && !npcId)) { 44 | res.status(401).end(); 45 | return; 46 | } 47 | 48 | const itemID = req.body.id; 49 | 50 | if (!itemID) { 51 | res.status(400).send({ message: 'Item ID is undefined.' }); 52 | return; 53 | } 54 | 55 | const quantity = req.body.quantity; 56 | const currentDescription = req.body.currentDescription; 57 | 58 | const playerId = npcId ? npcId : player.id; 59 | 60 | const item = await prisma.playerItem.update({ 61 | where: { player_id_item_id: { player_id: playerId, item_id: itemID } }, 62 | data: { quantity, currentDescription }, 63 | }); 64 | 65 | res.end(); 66 | 67 | res.socket.server.io 68 | ?.to('admin') 69 | .emit('playerItemChange', playerId, itemID, item.currentDescription, item.quantity); 70 | } 71 | 72 | async function handlePut(req: NextApiRequest, res: NextApiResponseServerIO) { 73 | const player = req.session.player; 74 | const npcId: number | undefined = req.body.npcId; 75 | 76 | if (!player || (player.admin && !npcId)) { 77 | res.status(401).end(); 78 | return; 79 | } 80 | 81 | const itemID = req.body.id; 82 | 83 | if (!itemID) { 84 | res.status(400).send({ message: 'Item ID is undefined.' }); 85 | return; 86 | } 87 | 88 | 89 | const playerId = npcId ? npcId : player.id; 90 | 91 | const item = await prisma.playerItem.create({ 92 | data: { 93 | currentDescription: '', 94 | quantity: 1, 95 | player_id: playerId, 96 | item_id: itemID, 97 | }, 98 | include: { Item: true }, 99 | }); 100 | 101 | await prisma.playerItem.update({ 102 | where: { player_id_item_id: { player_id: playerId, item_id: itemID } }, 103 | data: { currentDescription: item.Item.description }, 104 | }); 105 | 106 | item.currentDescription = item.Item.description; 107 | 108 | res.send({ item }); 109 | 110 | res.socket.server.io 111 | ?.to('admin') 112 | .emit('playerItemAdd', playerId, item.Item, item.currentDescription, item.quantity); 113 | } 114 | 115 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 116 | const player = req.session.player; 117 | const npcId: number | undefined = req.body.npcId; 118 | 119 | if (!player || (player.admin && !npcId)) { 120 | res.status(401).end(); 121 | return; 122 | } 123 | 124 | const itemID = req.body.id; 125 | 126 | if (!itemID) { 127 | res.status(400).send({ message: 'Item ID is undefined.' }); 128 | return; 129 | } 130 | 131 | 132 | const playerId = npcId ? npcId : player.id; 133 | 134 | await prisma.playerItem.delete({ 135 | where: { player_id_item_id: { player_id: playerId, item_id: itemID } }, 136 | }); 137 | 138 | res.end(); 139 | 140 | res.socket.server.io?.to('admin').emit('playerItemRemove', playerId, itemID); 141 | } 142 | 143 | export default sessionAPI(handler); 144 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/skill/clearchecks.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import prisma from '../../../../../utils/database'; 3 | import { sessionAPI } from '../../../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | res.status(404).send({ message: 'Supported methods: POST' }); 8 | } 9 | 10 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 11 | const player = req.session.player; 12 | const npcId: number | undefined = req.body.npcId; 13 | 14 | if (!player || (player.admin && !npcId)) { 15 | res.status(401).end(); 16 | return; 17 | } 18 | 19 | 20 | const playerId = npcId ? npcId : player.id; 21 | 22 | await prisma.playerSkill.updateMany({ 23 | where: { player_id: playerId }, 24 | data: { checked: false }, 25 | }); 26 | 27 | res.end(); 28 | } 29 | 30 | export default sessionAPI(handler); -------------------------------------------------------------------------------- /src/pages/api/sheet/player/skill/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import prisma from '../../../../../utils/database'; 3 | import { sessionAPI } from '../../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../../utils/socket'; 5 | import type { DiceConfig } from '../../../../../utils/config'; 6 | 7 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 8 | if (req.method === 'POST') return handlePost(req, res); 9 | if (req.method === 'PUT') return handlePut(req, res); 10 | res.status(404).send({ message: 'Supported methods: POST | PUT' }); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | const npcId: number | undefined = req.body.npcId; 16 | 17 | if (!player || (player.admin && !npcId)) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const skillID: number | undefined = req.body.id; 23 | const value: number | undefined = req.body.value; 24 | const checked: boolean | undefined = req.body.checked; 25 | const modifier: number | undefined = req.body.modifier; 26 | 27 | if (!skillID) { 28 | res.status(400).send({ message: 'Skill ID or value is undefined.' }); 29 | return; 30 | } 31 | 32 | const playerId = npcId ? npcId : player.id; 33 | 34 | const skill = await prisma.playerSkill.update({ 35 | where: { player_id_skill_id: { player_id: playerId, skill_id: skillID } }, 36 | data: { value, checked, modifier }, 37 | }); 38 | 39 | res.socket.server.io?.emit( 40 | 'playerSkillChange', 41 | playerId, 42 | skillID, 43 | skill.value, 44 | skill.modifier 45 | ); 46 | 47 | res.end(); 48 | } 49 | 50 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 51 | const player = req.session.player; 52 | const npcId: number | undefined = req.body.npcId; 53 | 54 | if (!player || (player.admin && !npcId)) { 55 | res.status(401).end(); 56 | return; 57 | } 58 | 59 | const skillID = req.body.id; 60 | 61 | if (!skillID) { 62 | res.status(400).send({ message: 'Skill ID is undefined.' }); 63 | return; 64 | } 65 | 66 | const playerId = npcId ? npcId : player.id; 67 | 68 | const [skill, dices] = await prisma.$transaction([ 69 | prisma.skill.findUnique({ 70 | where: { id: skillID }, 71 | select: { startValue: true }, 72 | }), 73 | prisma.config.findUnique({ 74 | where: { name: 'dice' }, 75 | select: { value: true }, 76 | }), 77 | ]); 78 | 79 | const playerSkill = await prisma.playerSkill.create({ 80 | data: { 81 | player_id: playerId, 82 | skill_id: skillID, 83 | value: skill?.startValue || 0, 84 | checked: false, 85 | }, 86 | select: { 87 | Skill: { 88 | select: { id: true, name: true, Specialization: { select: { name: true } } }, 89 | }, 90 | value: true, 91 | checked: true, 92 | modifier: true, 93 | }, 94 | }); 95 | 96 | const enableModifiers = ((dices?.value || {}) as DiceConfig)?.skill?.enable_modifiers; 97 | if (!enableModifiers) playerSkill.modifier = null as any; 98 | 99 | res.send({ skill: playerSkill }); 100 | } 101 | 102 | export default sessionAPI(handler); 103 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/spec.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') { 8 | return handlePost(req, res); 9 | } 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | const npcId: number | undefined = req.body.npcId; 16 | 17 | if (!player || (player.admin && !npcId)) { 18 | res.status(401).end(); 19 | return; 20 | } 21 | 22 | const specID: number | undefined = req.body.id; 23 | const value: string | undefined = req.body.value; 24 | 25 | if (!specID || value === undefined) { 26 | res.status(400).send({ message: 'Spec ID or value is undefined.' }); 27 | return; 28 | } 29 | 30 | const playerId = npcId ? npcId : player.id; 31 | 32 | await database.playerSpec.update({ 33 | data: { value }, 34 | where: { player_id_spec_id: { player_id: playerId, spec_id: specID } }, 35 | }); 36 | 37 | res.end(); 38 | 39 | res.socket.server.io?.emit('playerSpecChange', playerId, specID, value); 40 | } 41 | 42 | export default sessionAPI(handler); 43 | -------------------------------------------------------------------------------- /src/pages/api/sheet/player/spell.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import prisma from '../../../../utils/database'; 3 | import { sessionAPI } from '../../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).send({ message: 'Supported methods: POST | PUT | DELETE' }); 10 | } 11 | 12 | async function handlePut(req: NextApiRequest, res: NextApiResponseServerIO) { 13 | const player = req.session.player; 14 | const npcId: number | undefined = req.body.npcId; 15 | 16 | if (!player || (player.admin && !npcId)) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const spellID: number | undefined = req.body.id; 22 | 23 | if (!spellID) { 24 | res.status(400).send({ message: 'spell ID is undefined.' }); 25 | return; 26 | } 27 | 28 | 29 | const playerId = npcId ? npcId : player.id; 30 | 31 | const spell = await prisma.playerSpell.create({ 32 | data: { 33 | player_id: playerId, 34 | spell_id: spellID, 35 | }, 36 | select: { Spell: true }, 37 | }); 38 | 39 | res.socket.server.io?.emit('playerSpellAdd', playerId, spell.Spell); 40 | 41 | res.send({ spell: spell.Spell }); 42 | } 43 | 44 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 45 | const player = req.session.player; 46 | const npcId: number | undefined = req.body.npcId; 47 | 48 | if (!player || (player.admin && !npcId)) { 49 | res.status(401).end(); 50 | return; 51 | } 52 | 53 | const spellID: number | undefined = req.body.id; 54 | 55 | if (!spellID) { 56 | res.status(400).send({ message: 'spell ID is undefined.' }); 57 | return; 58 | } 59 | 60 | 61 | const playerId = npcId ? npcId : player.id; 62 | 63 | await prisma.playerSpell.delete({ 64 | where: { player_id_spell_id: { player_id: playerId, spell_id: spellID } }, 65 | }); 66 | 67 | res.socket.server.io?.emit('playerSpellRemove', playerId, spellID); 68 | 69 | res.end(); 70 | } 71 | 72 | export default sessionAPI(handler); 73 | -------------------------------------------------------------------------------- /src/pages/api/sheet/skill.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | import type { NextApiResponseServerIO } from '../../../utils/socket'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | if (req.method === 'PUT') return handlePut(req, res); 9 | if (req.method === 'DELETE') return handleDelete(req, res); 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponseServerIO) { 14 | const player = req.session.player; 15 | 16 | if (!player || !player.admin) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const id: number | undefined = req.body.id; 22 | const name: string | undefined = req.body.name; 23 | const startValue: number | undefined = req.body.startValue; 24 | const mandatory: boolean | undefined = req.body.mandatory; 25 | let specialization_id: number | null | undefined = req.body.specialization_id; 26 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 27 | 28 | if ( 29 | !id || 30 | !name || 31 | startValue === undefined || 32 | mandatory === undefined || 33 | specialization_id === undefined || 34 | visibleToAdmin === undefined 35 | ) { 36 | res.status(401).send({ 37 | message: 38 | 'ID, nome, valor inicial, obrigatório, ID de especialização ou visível(mestre) da perícia estão em branco.', 39 | }); 40 | return; 41 | } 42 | 43 | if (specialization_id === 0) specialization_id = null; 44 | 45 | const skill = await database.skill.update({ 46 | data: { name, startValue, mandatory, specialization_id, visibleToAdmin }, 47 | where: { id }, 48 | select: { name: true, Specialization: { select: { name: true } } }, 49 | }); 50 | 51 | res.end(); 52 | 53 | res.socket.server.io?.emit( 54 | 'skillChange', 55 | id, 56 | skill.name, 57 | skill.Specialization?.name || null 58 | ); 59 | } 60 | 61 | async function handlePut(req: NextApiRequest, res: NextApiResponseServerIO) { 62 | const player = req.session.player; 63 | 64 | if (!player || !player.admin) { 65 | res.status(401).end(); 66 | return; 67 | } 68 | 69 | const name: string | undefined = req.body.name; 70 | const startValue: number | undefined = req.body.startValue; 71 | const mandatory: boolean | undefined = req.body.mandatory; 72 | let specialization_id: number | null | undefined = req.body.specialization_id; 73 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 74 | 75 | if ( 76 | !name || 77 | startValue === undefined || 78 | mandatory === undefined || 79 | specialization_id === undefined || 80 | visibleToAdmin === undefined 81 | ) { 82 | res.status(401).send({ 83 | message: 84 | 'Nome, valor inicial, obrigatório, ID de especialização ou visível(mestre) da perícia estão em branco.', 85 | }); 86 | return; 87 | } 88 | 89 | if (specialization_id === 0) specialization_id = null; 90 | 91 | const skill = await database.skill.create({ 92 | data: { name, startValue, mandatory, specialization_id, visibleToAdmin }, 93 | select: { 94 | id: true, 95 | name: true, 96 | Specialization: { 97 | select: { 98 | name: true, 99 | }, 100 | }, 101 | }, 102 | }); 103 | 104 | res.send({ id: skill.id }); 105 | 106 | res.socket.server.io?.emit( 107 | 'skillAdd', 108 | skill.id, 109 | skill.name, 110 | skill.Specialization?.name || null 111 | ); 112 | } 113 | 114 | async function handleDelete(req: NextApiRequest, res: NextApiResponseServerIO) { 115 | const player = req.session.player; 116 | 117 | if (!player || !player.admin) { 118 | res.status(401).end(); 119 | return; 120 | } 121 | 122 | const id: number | undefined = req.body.id; 123 | 124 | if (!id) { 125 | res.status(400).send({ message: 'ID da perícia está em branco.' }); 126 | return; 127 | } 128 | 129 | await database.skill.delete({ where: { id } }); 130 | 131 | res.end(); 132 | 133 | res.socket.server.io?.emit('skillRemove', id); 134 | } 135 | 136 | export default sessionAPI(handler); 137 | -------------------------------------------------------------------------------- /src/pages/api/sheet/spec.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import database from '../../../utils/database'; 3 | import { sessionAPI } from '../../../utils/session'; 4 | 5 | function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method === 'POST') return handlePost(req, res); 7 | if (req.method === 'PUT') return handlePut(req, res); 8 | if (req.method === 'DELETE') return handleDelete(req, res); 9 | res.status(404).end(); 10 | } 11 | 12 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 13 | const player = req.session.player; 14 | 15 | if (!player || !player.admin) { 16 | res.status(401).end(); 17 | return; 18 | } 19 | 20 | const id: number | undefined = req.body.id; 21 | const name: string | undefined = req.body.name; 22 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 23 | 24 | if (!id || !name || visibleToAdmin === undefined) { 25 | res 26 | .status(401) 27 | .send({ message: 'ID, nome ou visível(mestre) da especificação estão em branco.' }); 28 | return; 29 | } 30 | 31 | await database.spec.update({ data: { name, visibleToAdmin }, where: { id } }); 32 | 33 | res.end(); 34 | } 35 | 36 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 37 | const player = req.session.player; 38 | 39 | if (!player || !player.admin) { 40 | res.status(401).end(); 41 | return; 42 | } 43 | 44 | const name: string | undefined = req.body.name; 45 | const visibleToAdmin: boolean | undefined = req.body.visibleToAdmin; 46 | 47 | if (!name || visibleToAdmin === undefined) { 48 | res 49 | .status(401) 50 | .send({ message: 'Nome ou visível(mestre) da especificação está em branco.' }); 51 | return; 52 | } 53 | 54 | const [spec, players] = await database.$transaction([ 55 | database.spec.create({ data: { name, visibleToAdmin }, select: { id: true } }), 56 | database.player.findMany({ 57 | where: { role: { in: ['PLAYER', 'NPC'] } }, 58 | select: { id: true }, 59 | }), 60 | ]); 61 | 62 | if (players.length > 0) { 63 | await database.playerSpec.createMany({ 64 | data: players.map((player) => { 65 | return { 66 | spec_id: spec.id, 67 | player_id: player.id, 68 | value: '0', 69 | }; 70 | }), 71 | }); 72 | } 73 | 74 | res.send({ id: spec.id }); 75 | } 76 | 77 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 78 | const player = req.session.player; 79 | 80 | if (!player || !player.admin) { 81 | res.status(401).end(); 82 | return; 83 | } 84 | 85 | const id: number | null = req.body.id; 86 | 87 | if (!id) { 88 | res.status(401).send({ message: 'ID da especificação está em branco.' }); 89 | return; 90 | } 91 | 92 | await database.spec.delete({ where: { id } }); 93 | 94 | res.end(); 95 | } 96 | 97 | export default sessionAPI(handler); 98 | -------------------------------------------------------------------------------- /src/pages/api/sheet/specialization.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '@prisma/client'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import database from '../../../utils/database'; 4 | import { sessionAPI } from '../../../utils/session'; 5 | 6 | function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method === 'POST') return handlePost(req, res); 8 | if (req.method === 'PUT') return handlePut(req, res); 9 | if (req.method === 'DELETE') return handleDelete(req, res); 10 | res.status(404).end(); 11 | } 12 | 13 | async function handlePost(req: NextApiRequest, res: NextApiResponse) { 14 | const player = req.session.player; 15 | 16 | if (!player || !player.admin) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const id: number | undefined = req.body.id; 22 | const name: string | undefined = req.body.name; 23 | 24 | if (!id || !name) { 25 | res.status(401).send({ message: 'ID ou nome da especialização estão em branco.' }); 26 | return; 27 | } 28 | 29 | await database.specialization.update({ data: { name }, where: { id: id } }); 30 | 31 | res.end(); 32 | } 33 | 34 | async function handlePut(req: NextApiRequest, res: NextApiResponse) { 35 | const player = req.session.player; 36 | 37 | if (!player || !player.admin) { 38 | res.status(401).end(); 39 | return; 40 | } 41 | 42 | const name: string | undefined = req.body.name; 43 | 44 | if (!name) { 45 | res.status(401).send({ message: 'Nome da especialização está em branco.' }); 46 | return; 47 | } 48 | 49 | const specialization = await database.specialization.create({ data: { name } }); 50 | 51 | res.send({ id: specialization.id }); 52 | } 53 | 54 | async function handleDelete(req: NextApiRequest, res: NextApiResponse) { 55 | const player = req.session.player; 56 | 57 | if (!player || !player.admin) { 58 | res.status(401).end(); 59 | return; 60 | } 61 | 62 | const id: number | undefined = req.body.id; 63 | 64 | if (!id) { 65 | res.status(401).send({ message: 'ID da especialização está em branco.' }); 66 | return; 67 | } 68 | 69 | try { 70 | await database.specialization.delete({ where: { id } }); 71 | res.end(); 72 | } catch (err) { 73 | if (err instanceof Prisma.PrismaClientKnownRequestError) { 74 | if (err.code === 'P2003') { 75 | res.status(400).send({ 76 | message: 77 | 'Não foi possível remover essa especialização pois ainda há alguma perícia usando-a.', 78 | }); 79 | return; 80 | } 81 | res.status(500).end(); 82 | } 83 | } 84 | } 85 | 86 | export default sessionAPI(handler); 87 | -------------------------------------------------------------------------------- /src/pages/api/socket.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest } from 'next'; 2 | import { Server } from 'socket.io'; 3 | import type { 4 | ClientToServerEvents, 5 | NextApiResponseServerIO, 6 | ServerToClientEvents 7 | } from '../../utils/socket'; 8 | 9 | export default function handler(req: NextApiRequest, res: NextApiResponseServerIO) { 10 | if (!res.socket.server.io) { 11 | const io = new Server( 12 | res.socket.server 13 | ); 14 | 15 | io.on('connection', (socket) => { 16 | socket.on('roomJoin', (roomName) => socket.join(roomName)); 17 | }); 18 | 19 | res.socket.server.io = io; 20 | } 21 | res.end(); 22 | } 23 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220403144231_playeravatar_relation_cascade/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE `playeravatar` DROP FOREIGN KEY `PlayerAvatar_attribute_status_id_fkey`; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE `PlayerAvatar` ADD CONSTRAINT `PlayerAvatar_attribute_status_id_fkey` FOREIGN KEY (`attribute_status_id`) REFERENCES `AttributeStatus`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220415005955_player_skill_checked_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `playerskill` ADD COLUMN `checked` BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220506183920_skill_start_value_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `skill` ADD COLUMN `startValue` INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220507181703_player_attribute_show_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `playerattribute` ADD COLUMN `show` BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220520225933_player_name_added/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `default` on the `info` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE `info` DROP COLUMN `default`; 9 | 10 | -- AlterTable 11 | ALTER TABLE `player` ADD COLUMN `name` VARCHAR(191) NOT NULL DEFAULT '', 12 | ADD COLUMN `showName` BOOLEAN NOT NULL DEFAULT true; 13 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220530194625_skill_modifier_add/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to alter the column `modifier` on the `playercharacteristic` table. The data in that column could be lost. The data in that column will be cast from `VarChar(191)` to `Int`. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE `playercharacteristic` MODIFY `modifier` INTEGER NOT NULL DEFAULT 0; 9 | 10 | -- AlterTable 11 | ALTER TABLE `playerskill` ADD COLUMN `modifier` INTEGER NOT NULL DEFAULT 0; 12 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220617194737_npc_role_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `player` MODIFY `username` VARCHAR(191) NULL, 3 | MODIFY `password` VARCHAR(191) NULL, 4 | MODIFY `role` ENUM('PLAYER', 'NPC', 'ADMIN') NOT NULL; 5 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220617224442_player_portrait_attribute_enum_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `attribute` ADD COLUMN `portrait` ENUM('PRIMARY', 'SECONDARY') NULL; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220618203220_visible_to_admin_option_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `attribute` ADD COLUMN `visibleToAdmin` BOOLEAN NOT NULL DEFAULT true; 3 | 4 | -- AlterTable 5 | ALTER TABLE `characteristic` ADD COLUMN `visibleToAdmin` BOOLEAN NOT NULL DEFAULT false; 6 | 7 | -- AlterTable 8 | ALTER TABLE `currency` ADD COLUMN `visibleToAdmin` BOOLEAN NOT NULL DEFAULT true; 9 | 10 | -- AlterTable 11 | ALTER TABLE `info` ADD COLUMN `visibleToAdmin` BOOLEAN NOT NULL DEFAULT false; 12 | 13 | -- AlterTable 14 | ALTER TABLE `skill` ADD COLUMN `visibleToAdmin` BOOLEAN NOT NULL DEFAULT false; 15 | 16 | -- AlterTable 17 | ALTER TABLE `spec` ADD COLUMN `visibleToAdmin` BOOLEAN NOT NULL DEFAULT true; 18 | -------------------------------------------------------------------------------- /src/prisma/migrations/20220621212142_trade_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Trade` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `sender_id` INTEGER NOT NULL, 5 | `sender_object_id` INTEGER NOT NULL, 6 | `receiver_id` INTEGER NOT NULL, 7 | `receiver_object_id` INTEGER NULL, 8 | 9 | PRIMARY KEY (`id`) 10 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 11 | 12 | -- AddForeignKey 13 | ALTER TABLE `Trade` ADD CONSTRAINT `Trade_sender_id_fkey` FOREIGN KEY (`sender_id`) REFERENCES `Player`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE `Trade` ADD CONSTRAINT `Trade_receiver_id_fkey` FOREIGN KEY (`receiver_id`) REFERENCES `Player`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /src/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /src/styles/modules/Home.module.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: none; 3 | color: inherit !important; 4 | border-width: 0px 0px 1px 0px; 5 | border-style: solid; 6 | border-color: #ff000070; 7 | &:hover, 8 | &:active { 9 | padding: 5px; 10 | border-width: 0; 11 | border-radius: 5px; 12 | background-color: #ff000070; 13 | } 14 | transition-property: padding, border; 15 | transition-duration: 250ms; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/modules/Portrait.module.scss: -------------------------------------------------------------------------------- 1 | $avatar-width: 420px; 2 | $avatar-height: 600px; 3 | 4 | $dice-container-left: 30px; 5 | 6 | .container { 7 | position: relative; 8 | width: 0px; 9 | } 10 | 11 | .editor { 12 | position: absolute; 13 | top: 650px; 14 | left: 10px; 15 | } 16 | 17 | .avatar { 18 | position: absolute; 19 | width: $avatar-width; 20 | height: $avatar-height; 21 | } 22 | 23 | .sideContainer { 24 | position: absolute; 25 | width: 200px; 26 | height: 120px; 27 | z-index: 100; 28 | 29 | .sideBackground { 30 | filter: blur(10px); 31 | background: radial-gradient(#000000 40%, #00000040 60%, #00000000 70%); 32 | width: 100%; 33 | height: 100%; 34 | } 35 | 36 | .sideContent { 37 | position: absolute; 38 | top: 45%; 39 | left: 50%; 40 | transform: translate(-50%, -50%); 41 | font-size: 68px; 42 | } 43 | } 44 | 45 | .nameContainer { 46 | position: absolute; 47 | width: 100%; 48 | 49 | .name { 50 | width: 6em; 51 | color: white; 52 | text-shadow: 4px 4px 0px rgba(0, 0, 0, 0.75); 53 | 54 | font-size: 96px; 55 | font-style: italic; 56 | text-transform: uppercase; 57 | line-height: 100%; 58 | overflow-wrap: break-word; 59 | padding-right: 1.5rem; 60 | } 61 | } 62 | 63 | .combat { 64 | position: absolute; 65 | width: 100%; 66 | 67 | .attribute { 68 | font-size: 108px; 69 | font-style: italic; 70 | max-width: 250px; 71 | color: white; 72 | line-height: 100%; 73 | padding-right: 1.5rem; 74 | } 75 | } 76 | 77 | .diceContainer { 78 | position: absolute; 79 | left: $dice-container-left; 80 | } 81 | 82 | .dice { 83 | width: $avatar-width - $dice-container-left * 2; 84 | } 85 | 86 | .result { 87 | position: absolute; 88 | top: 49%; 89 | left: 50%; 90 | 91 | font-size: 114px; 92 | color: white; 93 | 94 | transform: translate(-50%, -50%); 95 | } 96 | 97 | .description { 98 | position: absolute; 99 | top: 99%; 100 | left: 50%; 101 | 102 | font-size: 80px; 103 | color: white; 104 | 105 | transform: translate(-50%, -50%); 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: '/api', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { DiceResolverKeyNum } from './dice'; 2 | 3 | export type DiceConfig = { 4 | //Legacy object 5 | base: DiceConfigCell; 6 | 7 | characteristic: DiceConfigCell & { 8 | enable_modifiers: boolean; 9 | }; 10 | skill: DiceConfigCell & { 11 | enable_modifiers: boolean; 12 | }; 13 | attribute: DiceConfigCell; 14 | }; 15 | 16 | export type DiceConfigCell = { 17 | value: DiceResolverKeyNum; 18 | branched: boolean; 19 | }; 20 | 21 | export type PortraitFontConfig = { 22 | name: string; 23 | data: string; 24 | }; 25 | 26 | export type Environment = 'idle' | 'combat'; -------------------------------------------------------------------------------- /src/utils/database.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | let prisma: PrismaClient; 4 | 5 | if (process.env.NOVE_ENV === 'production') { 6 | prisma = new PrismaClient(); 7 | } else { 8 | if (!global.prisma) { 9 | global.prisma = new PrismaClient(); 10 | } 11 | prisma = global.prisma; 12 | } 13 | 14 | export default prisma; 15 | -------------------------------------------------------------------------------- /src/utils/dice.ts: -------------------------------------------------------------------------------- 1 | export type DiceResolverKeyNum = 20 | 100; 2 | export type DiceResolverKey = '20' | '100' | '20b' | '100b'; 3 | 4 | export type DiceRequest = { 5 | num: number; 6 | roll: number; 7 | ref?: number; 8 | }; 9 | 10 | export type DiceResponseResultType = { 11 | description: string; 12 | 13 | //0: normal, < 0: failure, > 0: success 14 | successWeight: number; 15 | }; 16 | 17 | export type DiceResponse = { 18 | roll: number; 19 | resultType?: DiceResponseResultType; 20 | }; 21 | 22 | export function resolveDices(dices: string) { 23 | let formattedDiceString = dices.replace(/\s/g, '').toUpperCase(); 24 | 25 | const options = formattedDiceString.split('|'); 26 | 27 | if (options.length > 1) { 28 | const selected = prompt( 29 | 'Escolha dentre as seguintes opções de rolagem:\n' + 30 | options.map((opt, i) => `${i + 1}: ${opt}`).join('\n') 31 | ); 32 | 33 | if (!selected) return; 34 | 35 | const code = parseInt(selected); 36 | 37 | if (!code || code > options.length) return; 38 | 39 | formattedDiceString = options[code - 1]; 40 | } 41 | 42 | const diceArray = formattedDiceString.split('+'); 43 | const resolvedDices: DiceRequest[] = new Array(diceArray.length); 44 | 45 | for (let i = 0; i < diceArray.length; i++) { 46 | resolvedDices[i] = resolveDice(diceArray[i]); 47 | } 48 | 49 | return resolvedDices; 50 | } 51 | 52 | function resolveDice(dice: string): DiceRequest { 53 | if (dice.includes('DB')) { 54 | const bonusDamageArray = document.getElementsByName( 55 | 'specDano Bônus' 56 | ) as NodeListOf; 57 | 58 | if (bonusDamageArray.length > 0) { 59 | const bonusDamage = bonusDamageArray[0].value.replace(/\s/g, '').toUpperCase(); 60 | 61 | const divider = parseInt(dice.split('/')[1]) || 1; 62 | const split = bonusDamage.split('D'); 63 | 64 | if (split.length === 1) dice = Math.floor(parseInt(split[0]) / divider).toString(); 65 | else dice = `${split[0]}D${Math.floor(parseInt(split[1]) / divider)}`; 66 | } 67 | } else { 68 | const regexResult = dice.match(/[A-Z][A-Z][A-Z]/g); 69 | if (regexResult) { 70 | const charName = regexResult[0]; 71 | const charElementArray = document.getElementsByName(`char${charName}`); 72 | 73 | if (charElementArray.length > 0) { 74 | const charElement = charElementArray[0] as HTMLInputElement; 75 | const divider = parseInt(dice.split('/')[1]) || 1; 76 | 77 | dice = Math.floor(parseInt(charElement.value) / divider).toString(); 78 | } 79 | } 80 | } 81 | 82 | const split = dice.split('D'); 83 | if (split.length === 1) return { num: 0, roll: parseInt(dice) || 0 }; 84 | return { num: parseInt(split[0]), roll: parseInt(split[1]) || 0 }; 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/encryption.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | const saltRounds = 10; 3 | 4 | function hash(plainPassword: string) { 5 | const salt = bcrypt.genSaltSync(saltRounds); 6 | return bcrypt.hashSync(plainPassword, salt); 7 | } 8 | 9 | function compare(plainPassword: string, hashword: string) { 10 | return bcrypt.compareSync(plainPassword, hashword); 11 | } 12 | 13 | export { hash, compare }; 14 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function clamp(num: number, min: number, max: number) { 2 | if (num < min) return min; 3 | if (num > max) return max; 4 | return num; 5 | } 6 | 7 | export function sleep(ms: number): Promise { 8 | return new Promise((res) => setTimeout(res, ms)); 9 | } 10 | 11 | type GetSSRResult = { props: TProps } | { redirect: any } | { notFound: true }; 12 | 13 | type GetSSRFn = (args: any) => Promise>; 14 | 15 | export type InferSSRProps> = TFn extends GetSSRFn 16 | ? NonNullable 17 | : never; 18 | -------------------------------------------------------------------------------- /src/utils/session.ts: -------------------------------------------------------------------------------- 1 | import type { IronSessionOptions } from 'iron-session'; 2 | import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'; 3 | import type { GetServerSidePropsContext, NextApiHandler, NextApiRequest } from 'next'; 4 | import type { NextApiResponseServerIO } from './socket'; 5 | 6 | export const cookieName = 'openrpg_session'; 7 | 8 | declare module 'iron-session' { 9 | interface IronSessionData { 10 | player?: { 11 | id: number; 12 | admin: boolean; 13 | }; 14 | } 15 | } 16 | 17 | const sessionOptions: IronSessionOptions = { 18 | cookieName, 19 | password: process.env.SESSION_SECRET as string, 20 | cookieOptions: { 21 | secure: false, 22 | }, 23 | }; 24 | 25 | type NextApiServerIOHandler = ( 26 | req: NextApiRequest, 27 | res: NextApiResponseServerIO 28 | ) => void | Promise; 29 | 30 | export function sessionAPI(handler: NextApiHandler | NextApiServerIOHandler) { 31 | return withIronSessionApiRoute(handler as NextApiHandler, sessionOptions); 32 | } 33 | 34 | export function sessionSSR( 35 | handler: (context: GetServerSidePropsContext) => Promise 36 | ) { 37 | return withIronSessionSsr(handler, sessionOptions); 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/style.ts: -------------------------------------------------------------------------------- 1 | export function getAttributeStyle(color: string) { 2 | return { 3 | color: 'white', 4 | textShadow: `0 0 10px #${color}, 0 0 30px #${color}, 0 0 50px #${color}`, 5 | }; 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "incremental": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | , "src/utils/socket/server.js" ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------