Adicionando itens na interface principal
Depois de todo esse trabalho podemos finalmente terminar o nosso jogo, para isso precisamos tornar os detalhes funcionais na nossa interface principal.
Esse processo é simples mas o trabalho depende de como você prefere que seu design seja, muita gente não gosta de trabalhar com Modal (dialog) que são aquelas janelas que aparecem no meio da tela, e se você gosta de organizar seu código eu recomendo que você evite usar porque acaba dando mais trabalho gerenciar esses componentes.
Eu vou reduzir um pouco o escopo do projeto para que esse tutorial não fique muito grande, mas você pode adicionar mais detalhes se quiser.
Adicionando o ShadCN
Se você nunca ouviu falar o ShadCN é uma biblioteca de componentes que é mantida pela Vercel para ser usada com varios frameworks, como ela não instala dependencias no projeto vamos usar ela pois deixa o nosso projeto mais leve.
Para instalar o ShadCN basta rodar o comando:
Antes recomendo que você faça o backup do arquivo styles/globals.css
pois ele vai ser substituido, ou você pode copiar o meu arquivo caso esteja seguindo a risca esse tutorial.
npx shadcn-ui@latest init
Caso não saiba como criar os componentes do shadcn basta olhar no site deles que tem a documentação de como criar os componentes.
Depois eu criei um Dialog para ser usado como modal, você pode ver o codigo dele no arquivo components/ui/Dialog.tsx
.
npx shadcn-ui@latest add dialog
globals.css
@import url("https://fonts.googleapis.com/css2?family=Poor+Story&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
body > div {
height: 100vh;
}
@layer utilities {
.medieval-font {
font-family: "Poor Story", system-ui;
}
.golden-gradient {
background: radial-gradient(
ellipse farthest-corner at right bottom,
#fedb37 0%,
#fdb931 8%,
#9f7928 30%,
#8a6e2f 40%,
transparent 80%
), radial-gradient(ellipse farthest-corner at left top, #ffffff 0%, #ffffac
8%, #d1b464 25%, #5d4a1f 62.5%, #5d4a1f 100%);
}
.medieval-button {
padding: 10px 20px;
background-color: #795649;
color: #f4e5c3;
border: 2px solid #c0a080;
font-family: "Goudy Bookletter 1911", serif;
font-size: 18px;
text-shadow: 1px 1px 0px #a67c52;
box-shadow: 2px 2px 0px #a67c52;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.medieval-button:hover {
background-color: #8a6e2f;
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.3);
}
.medieval-button:active {
box-shadow: inset 1px 1px 2px 0px rgba(0, 0, 0, 0.5);
transform: translateY(2px);
}
.medieval-menu {
background-color: #162825;
list-style: none;
}
.medieval-menu li {
margin-bottom: 10px;
border-radius: 4px;
transition: background-color 0.3s ease, box-shadow 0.2s ease;
box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3);
}
.medieval-menu a {
text-decoration: none;
color: #af9567;
display: block;
padding: 10px 20px;
transition: color 0.3s ease;
font-family: "Poor Story", system-ui;
font-size: 16px;
}
.medieval-menu li:hover {
background-color: #4f4334;
}
.medieval-menu li:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.2);
}
.medieval-menu li:hover a {
color: #af9567;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;
Criando um personagem
Primeiro eu adicionei no topo do index.tsx
uma interface para o personagem chamada Char.
interface Char {
id: number;
name: string;
level: number;
user?: unknown;
class?: {
id: number;
name: string;
emoji: string;
image: string;
};
}
Depois adicionei o import dos componentes de Dialog que criamos usando o ShadCN.
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
Fiz algumas alterações no componente Home
do index.tsx
que são as seguintes:
- State para os dados que precisam de reatividade
- Inicializacao do useQuery e useMutation para buscar os personagens e criar um novo personagem
- useEffect para atualizar os personagens
- Funcao para recarregar os personagens do banco
- Funcao para selecionar um personagem da lista
- Funcao para criar um novo personagem
const [collapsedSkills, setCollapsedSkills] = useState(true);
const [chars, setChars] = useState<Char[]>([]);
const [selectedChar, setSelectedChar] = useState<Char | undefined>(undefined);
const [newCharacter, setNewCharacter] = useState(false);
const [selectedClass, setSelectedClass] = useState(-1);
const [charName, setCharName] = useState("");
useSessionGuard();
const characters = api.character.getChars.useQuery();
const createCharMutation = api.character.create.useMutation();
useEffect(() => {
if (characters?.data) {
const updatedChars = characters.data as Char[];
setChars(updatedChars);
if (!selectedChar && updatedChars.length > 0) {
setSelectedChar(updatedChars[0]);
}
}
}, [characters, selectedChar]);
const refreshChars = () => {
void characters.refetch();
};
const selectChar = (char: Char) => {
setSelectedChar(char);
};
const createChar = async () => {
try {
await createCharMutation.mutateAsync({
name: charName,
classId: selectedClass,
});
refreshChars();
} catch (error) {
console.error("Error creating character:", error);
} finally {
setNewCharacter(false);
}
};
E por ultimo fiz o bind nos elementos que tinhamos na tela claro adicionando o Dialog no topo do dom.
return (
<>
<Dialog open={newCharacter} onOpenChange={setNewCharacter}>
<DialogContent>
<DialogHeader>
<DialogTitle>Criar novo personagem?</DialogTitle>
<DialogDescription>
Escolha entre as classes disponivel para o seu personagem
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2">
<div className="grid flex-1 gap-2">
{createCharMutation.error && (
<p>{createCharMutation.error.message}</p>
)}
<div>
<label className="text-white">Nome</label>
<input
type="text"
className="input input-bordered w-full"
placeholder="Nome do personagem"
value={charName}
onChange={(e) => setCharName(e.target.value)}
/>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 1 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(1)}
>
<Image
src="/warrior.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Guerreiro</div>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 2 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(2)}
>
<Image
src="/rouge.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Ladino</div>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 3 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(3)}
>
<Image
src="/mage.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Mago</div>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 4 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(4)}
>
<Image
src="/druid.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Druida</div>
</div>
</div>
</div>
<DialogFooter className="flex h-full w-full items-end justify-end">
<button
className="btn btn-primary flex h-fit items-center gap-1 rounded-lg bg-green-500 p-2 text-white"
onClick={() => createChar()}
>
<span role="img" aria-label="Plus">
➕
</span>
Criar
</button>
<DialogClose asChild>
<button className="btn btn-secondary h-fit gap-1 rounded-lg bg-red-500 p-2 text-white">
<span role="img" aria-label="Trash Bin">
🗑️
</span>
Cancelar
</button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<main className="flex h-full w-full gap-2 bg-[#152724]">
<div className="flex h-full w-full gap-2 bg-[#152724] ">
<div className="golden-gradient hidden w-fit p-2 sm:block">
<Image
src={selectedChar?.class?.image ?? "/rouge.png"}
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-full w-full object-cover"
/>
</div>
<div className="flex w-full flex-col justify-between bg-[#1D2F2C]">
<div className="golden-gradient w-full rounded-sm p-1">
<div className="bg-[#2C534B]">
<h1
className="medieval-font p-2 text-2xl font-bold text-white"
onClick={() => setCollapsedSkills(!collapsedSkills)}
>
Personagens
</h1>
<ul
className={`medieval-menu w-full overflow-y-auto ${collapsedSkills ? "" : "hidden"}`}
>
<li>
<a onClick={() => setNewCharacter(true)}>Novo Jogo</a>
</li>
{chars?.map((char: Char) => (
<li key={char.id} onClick={() => selectChar(char)}>
<a
className={`${
selectedChar?.id === char.id
? "bg-[#4f4334] text-white"
: ""
}`}
>
{char.class?.emoji} {char.name} Lv: {char.level}
</a>
</li>
))}
</ul>
</div>
</div>
index.tsx
import Image from "next/image";
import { useEffect, useState } from "react";
import { useSessionGuard } from "~/utils/useSessionGuard";
import { api } from "~/utils/api";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
interface Char {
id: number;
name: string;
level: number;
user?: unknown;
class?: {
id: number;
name: string;
emoji: string;
image: string;
};
}
export default function Home() {
const [collapsedSkills, setCollapsedSkills] = useState(true);
const [chars, setChars] = useState<Char[]>([]);
const [selectedChar, setSelectedChar] = useState<Char | undefined>(undefined);
const [newCharacter, setNewCharacter] = useState(false);
const [selectedClass, setSelectedClass] = useState(-1);
const [charName, setCharName] = useState("");
useSessionGuard();
const characters = api.character.getChars.useQuery();
const createCharMutation = api.character.create.useMutation();
useEffect(() => {
if (characters?.data) {
const updatedChars = characters.data as Char[];
setChars(updatedChars);
if (!selectedChar && updatedChars.length > 0) {
setSelectedChar(updatedChars[0]);
}
}
}, [characters, selectedChar]);
const refreshChars = () => {
void characters.refetch();
};
const selectChar = (char: Char) => {
setSelectedChar(char);
};
const createChar = async () => {
try {
await createCharMutation.mutateAsync({
name: charName,
classId: selectedClass,
});
refreshChars();
} catch (error) {
console.error("Error creating character:", error);
} finally {
setNewCharacter(false);
}
};
return (
<>
<Dialog open={newCharacter} onOpenChange={setNewCharacter}>
<DialogContent>
<DialogHeader>
<DialogTitle>Criar novo personagem?</DialogTitle>
<DialogDescription>
Escolha entre as classes disponivel para o seu personagem
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2">
<div className="grid flex-1 gap-2">
{createCharMutation.error && (
<p>{createCharMutation.error.message}</p>
)}
<div>
<label className="text-white">Nome</label>
<input
type="text"
className="input input-bordered w-full"
placeholder="Nome do personagem"
value={charName}
onChange={(e) => setCharName(e.target.value)}
/>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 1 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(1)}
>
<Image
src="/warrior.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Guerreiro</div>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 2 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(2)}
>
<Image
src="/rouge.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Ladino</div>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 3 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(3)}
>
<Image
src="/mage.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Mago</div>
</div>
<div
className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 4 ? "border-2 border-red-500 shadow-lg" : ""}`}
onClick={() => setSelectedClass(4)}
>
<Image
src="/druid.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-32 w-32 object-cover"
/>
<div className="text-center text-white">Druida</div>
</div>
</div>
</div>
<DialogFooter className="flex h-full w-full items-end justify-end">
<button
className="btn btn-primary flex h-fit items-center gap-1 rounded-lg bg-green-500 p-2 text-white"
onClick={() => createChar()}
>
<span role="img" aria-label="Plus">
➕
</span>
Criar
</button>
<DialogClose asChild>
<button className="btn btn-secondary h-fit gap-1 rounded-lg bg-red-500 p-2 text-white">
<span role="img" aria-label="Trash Bin">
🗑️
</span>
Cancelar
</button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<main className="flex h-full w-full gap-2 bg-[#152724]">
<div className="flex h-full w-full gap-2 bg-[#152724] ">
<div className="golden-gradient hidden w-fit p-2 sm:block">
<Image
src={selectedChar?.class?.image ?? "/rouge.png"}
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-full w-full object-cover"
/>
</div>
<div className="flex w-full flex-col justify-between bg-[#1D2F2C]">
<div className="golden-gradient w-full rounded-sm p-1">
<div className="bg-[#2C534B]">
<h1
className="medieval-font p-2 text-2xl font-bold text-white"
onClick={() => setCollapsedSkills(!collapsedSkills)}
>
Personagens
</h1>
<ul
className={`medieval-menu w-full overflow-y-auto ${collapsedSkills ? "" : "hidden"}`}
>
<li>
<a onClick={() => setNewCharacter(true)}>Novo Jogo</a>
</li>
{chars?.map((char: Char) => (
<li key={char.id} onClick={() => selectChar(char)}>
<a
className={`${
selectedChar?.id === char.id
? "bg-[#4f4334] text-white"
: ""
}`}
>
{char.class?.emoji} {char.name} Lv: {char.level}
</a>
</li>
))}
</ul>
</div>
</div>
<div className="golden-gradient w-full bg-[#2C534B] p-1">
<div className="bg-[#2C534B]">
<h1 className="medieval-font p-2 text-2xl font-bold text-white">
Ações
</h1>
<ul className="medieval-menu w-full overflow-y-auto">
<li>
<a>Personagem</a>
</li>
<li>
<a>Inventario</a>
</li>
<li>
<a>
<div className="medieval-menu__item__content">Caçar</div>
</a>
</li>
<li>
<a>
<div className="medieval-menu__item__content">Atacar</div>
</a>
</li>
<li>
<a>
<div className="medieval-menu__item__content">Viajar</div>
</a>
</li>
</ul>
</div>
</div>
<div className="golden-gradient w-full bg-[#2C534B] p-1">
<div className="bg-[#2C534B]">
<h1
className="medieval-font p-2 text-2xl font-bold text-white"
onClick={() => setCollapsedSkills(!collapsedSkills)}
>
Skills
</h1>
<ul
className={`medieval-menu w-full overflow-y-auto ${collapsedSkills ? "hidden" : ""}`}
>
<li>
<a>Apunhalhar 🗡️ [1] </a>
</li>
<li>
<a>Saraivada de flechas 🏹 [2]</a>
</li>
<li>
<a>Punhal venenoso 🗡️ [3]</a>
</li>
<li>
<a>Furtividade 🗡️ [4]</a>
</li>
</ul>
</div>
</div>
<div className="flex w-full grow flex-col justify-end gap-1 bg-[#152724] pb-4">
<h1 className="medieval-font bg-[#2C534B] p-2 text-2xl font-bold text-white">
Dados
</h1>
<div className="text-md medieval-font rounded-md bg-red-600 text-center text-white">
100/100
</div>
<div className="text-md medieval-font rounded-md bg-blue-600 text-center text-white">
100/100
</div>
<div className="text-md medieval-font rounded-md bg-orange-300 text-center text-white">
100/100
</div>
</div>
</div>
</div>
<div className="flex w-full flex-col gap-2 bg-[#152724]">
<div className="golden-gradient w-full rounded-lg">
<Image
src="/Valdheim.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-full w-full rounded-xl object-cover p-2"
/>
</div>
<div className="flex h-full w-full flex-row rounded-lg">
<div className="h-full w-full border-r-2 border-[#af9567]">
<h2 className="medieval-font p-2 text-2xl text-[#af9567]">
História
</h2>
<ul className="medieval-menu w-full overflow-y-auto">
<li>
<a>
<div className="flex h-full w-full justify-around">
<Image
src="/coin.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-10 w-10 object-cover"
/>
<div className="m-auto">
Encontre o Lobo cinzento [4h]
</div>
</div>
</a>
</li>
</ul>
</div>
<div className="h-full w-full">
<h2 className="medieval-font p-2 text-2xl text-[#af9567]">
Caçada
</h2>
<ul className="medieval-menu w-full overflow-y-auto">
<li>
<a>
<div className="flex h-full w-full justify-around">
<Image
src="/coin-1.png"
alt="Logo"
width={0}
height={0}
sizes="100vw"
className="h-10 w-10 object-cover"
/>
<div className="m-auto">Mate 20 gremilins</div>
</div>
</a>
</li>
</ul>
</div>
</div>
</div>
</main>
</>
);
}
Para fazer isso funcionar você precisa criar algumas coisas no banco de dados antes, como as classes e o Mapa principal, com isso você ja consegue criar um personagem e selecionar ele.
Agora é com você, crie o seu primeiro personagem.