mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 13:27:52 +00:00
First Commit
This commit is contained in:
commit
d2b959fe73
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"template": "bolt-vite-react-ts"
|
||||||
|
}
|
||||||
8
.bolt/prompt
Normal file
8
.bolt/prompt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
|
||||||
|
|
||||||
|
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
|
||||||
|
|
||||||
|
Use icons from lucide-react for logos.
|
||||||
|
|
||||||
|
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.
|
||||||
|
|
||||||
108
.cursorrules
Normal file
108
.cursorrules
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "Padrões de Código",
|
||||||
|
"description": "Regras gerais para manter consistência no código",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "naming-conventions",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9]*$",
|
||||||
|
"message": "Use camelCase para nomes de variáveis e funções"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "component-naming",
|
||||||
|
"pattern": "^[A-Z][a-zA-Z0-9]*$",
|
||||||
|
"message": "Componentes React devem começar com letra maiúscula"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Segurança",
|
||||||
|
"description": "Regras para garantir segurança da aplicação",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "no-sensitive-data",
|
||||||
|
"pattern": "(password|senha|token|key|secret)",
|
||||||
|
"message": "Não exponha dados sensíveis no código"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child-safety",
|
||||||
|
"pattern": "(idade|age).*(>12|<6)",
|
||||||
|
"message": "Verifique restrições de idade (6-12 anos)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acessibilidade",
|
||||||
|
"description": "Regras para garantir acessibilidade",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "alt-text",
|
||||||
|
"pattern": "<img(?!.*alt=)[^>]*>",
|
||||||
|
"message": "Imagens devem ter texto alternativo (alt)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aria-labels",
|
||||||
|
"pattern": "<button(?!.*aria-label)[^>]*>",
|
||||||
|
"message": "Botões devem ter aria-label quando não têm texto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Performance",
|
||||||
|
"description": "Regras para otimização de performance",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "large-images",
|
||||||
|
"pattern": "unsplash.com/.*w=(\\d{4,})",
|
||||||
|
"message": "Evite imagens muito grandes (max 1200px)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "memo-check",
|
||||||
|
"pattern": "React.memo\\(",
|
||||||
|
"message": "Verifique se o uso de memo é necessário"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Estilo",
|
||||||
|
"description": "Regras de estilo e formatação",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "tailwind-classes",
|
||||||
|
"pattern": "className=\"[^\"]{150,}\"",
|
||||||
|
"message": "Considere extrair classes Tailwind longas para componentes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "color-consistency",
|
||||||
|
"pattern": "text-(purple|green|orange|blue|yellow)",
|
||||||
|
"message": "Use cores consistentes com a paleta definida"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Conteúdo",
|
||||||
|
"description": "Regras para conteúdo infantil",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"id": "child-friendly",
|
||||||
|
"pattern": "(violência|morte|guerra|violento)",
|
||||||
|
"message": "Evite conteúdo inadequado para crianças"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "educational-content",
|
||||||
|
"pattern": "(educativo|educacional|aprendizado|ensino)",
|
||||||
|
"message": "Priorize conteúdo educacional e construtivo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignoreFiles": [
|
||||||
|
"node_modules/**",
|
||||||
|
"dist/**",
|
||||||
|
"build/**",
|
||||||
|
".git/**",
|
||||||
|
"*.test.*",
|
||||||
|
"*.spec.*"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Histórias Mágicas 🌟
|
||||||
|
|
||||||
|
Uma plataforma educacional interativa que oferece histórias personalizadas para crianças entre 6 e 12 anos, com foco na cultura brasileira e educação.
|
||||||
|
|
||||||
|
## 🎯 Sobre o Projeto
|
||||||
|
|
||||||
|
Histórias Mágicas é uma aplicação web desenvolvida em React que permite que crianças explorem histórias educativas de forma interativa e personalizada. O projeto tem como objetivo:
|
||||||
|
|
||||||
|
- Promover a educação através de narrativas envolventes
|
||||||
|
- Valorizar a diversidade cultural brasileira
|
||||||
|
- Incentivar a consciência ambiental
|
||||||
|
- Oferecer uma experiência de aprendizado segura e divertida
|
||||||
|
|
||||||
|
## ✨ Funcionalidades
|
||||||
|
|
||||||
|
- 📚 Biblioteca de histórias interativas
|
||||||
|
- 👤 Personalização de avatares (culturas baiana e amazônica)
|
||||||
|
- 🎨 Temas educacionais diversos
|
||||||
|
- 🔊 Narração de histórias
|
||||||
|
- 📱 Design responsivo
|
||||||
|
- 🔒 Ambiente seguro e protegido
|
||||||
|
|
||||||
|
## 🛠️ Tecnologias Utilizadas
|
||||||
|
|
||||||
|
- React 18
|
||||||
|
- TypeScript
|
||||||
|
- Tailwind CSS
|
||||||
|
- Lucide React (ícones)
|
||||||
|
- Vite
|
||||||
|
|
||||||
|
## 🚀 Como Executar
|
||||||
|
|
||||||
|
1. Clone o repositório:
|
||||||
|
|
||||||
|
## 🚀 Deploy
|
||||||
|
|
||||||
|
### Opções Recomendadas
|
||||||
|
|
||||||
|
#### 1. Vercel (Recomendação Principal)
|
||||||
|
- Ideal para aplicações React/Next.js
|
||||||
|
- Deploy automático integrado com GitHub
|
||||||
|
- SSL gratuito
|
||||||
|
- CDN global
|
||||||
|
- Excelente performance
|
||||||
|
- Plano gratuito generoso
|
||||||
|
|
||||||
|
#### 2. Netlify
|
||||||
|
- Também oferece deploy automático
|
||||||
|
- Funções serverless incluídas
|
||||||
|
- SSL gratuito
|
||||||
|
- CDN global
|
||||||
|
- Interface amigável
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4051
package-lock.json
generated
Normal file
4051
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-react-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.344.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "^8.3.0",
|
||||||
|
"vite": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
105
src/App.tsx
Normal file
105
src/App.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { WelcomePage } from './components/WelcomePage';
|
||||||
|
import { LoginForm } from './components/auth/LoginForm';
|
||||||
|
import { RegistrationForm } from './components/RegistrationForm';
|
||||||
|
import { AvatarSelector } from './components/AvatarSelector';
|
||||||
|
import { ThemeSelector } from './components/ThemeSelector';
|
||||||
|
import { StoryViewer } from './components/StoryViewer';
|
||||||
|
import { StoryLibrary } from './components/library/StoryLibrary';
|
||||||
|
import { AuthUser, SavedStory } from './types/auth';
|
||||||
|
import { User, Theme } from './types';
|
||||||
|
import { AuthProvider } from './contexts/AuthContext'
|
||||||
|
|
||||||
|
type AppStep =
|
||||||
|
| 'welcome'
|
||||||
|
| 'login'
|
||||||
|
| 'register'
|
||||||
|
| 'avatar'
|
||||||
|
| 'theme'
|
||||||
|
| 'story'
|
||||||
|
| 'library';
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [step, setStep] = useState<AppStep>('welcome');
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [authUser, setAuthUser] = useState<AuthUser | null>(null);
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState<Theme | null>(null);
|
||||||
|
const [savedStories] = useState<SavedStory[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'A Aventura na Floresta Amazônica',
|
||||||
|
theme: 'Natureza e Meio Ambiente',
|
||||||
|
createdAt: '2024-03-15T10:00:00Z',
|
||||||
|
lastReadPage: 2,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleLogin = (email: string, password: string) => {
|
||||||
|
// In production: Implement actual authentication
|
||||||
|
setAuthUser({ id: '1', email, name: 'Usuário' });
|
||||||
|
setStep('library');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegistrationComplete = (userData: User) => {
|
||||||
|
setUser(userData);
|
||||||
|
setStep('avatar');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarComplete = (avatarId: string) => {
|
||||||
|
if (user) {
|
||||||
|
setUser({ ...user, avatarId });
|
||||||
|
setStep('theme');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThemeSelect = (theme: Theme) => {
|
||||||
|
setSelectedTheme(theme);
|
||||||
|
setStep('story');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStorySelect = (storyId: string) => {
|
||||||
|
// In production: Load the selected story
|
||||||
|
const story = savedStories.find(s => s.id === storyId);
|
||||||
|
if (story) {
|
||||||
|
// Navigate to the story viewer
|
||||||
|
setStep('story');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
||||||
|
{step === 'welcome' && (
|
||||||
|
<WelcomePage
|
||||||
|
onLoginClick={() => setStep('login')}
|
||||||
|
onRegisterClick={() => setStep('register')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 'login' && (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-6">
|
||||||
|
<LoginForm
|
||||||
|
onLogin={handleLogin}
|
||||||
|
onRegisterClick={() => setStep('register')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 'register' && (
|
||||||
|
<RegistrationForm onComplete={handleRegistrationComplete} />
|
||||||
|
)}
|
||||||
|
{step === 'avatar' && user && (
|
||||||
|
<AvatarSelector user={user} onComplete={handleAvatarComplete} />
|
||||||
|
)}
|
||||||
|
{step === 'theme' && <ThemeSelector onSelect={handleThemeSelect} />}
|
||||||
|
{step === 'story' && user && selectedTheme && (
|
||||||
|
<StoryViewer theme={selectedTheme} user={user} />
|
||||||
|
)}
|
||||||
|
{step === 'library' && authUser && (
|
||||||
|
<StoryLibrary
|
||||||
|
stories={savedStories}
|
||||||
|
onStorySelect={handleStorySelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
src/components/AvatarSelector.tsx
Normal file
141
src/components/AvatarSelector.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { User, Avatar } from '../types';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
const AVATARS: Avatar[] = [
|
||||||
|
// Bahia Culture
|
||||||
|
{
|
||||||
|
id: 'capoeira',
|
||||||
|
name: 'Capoeirista',
|
||||||
|
category: 'bahia',
|
||||||
|
description: 'Um jovem capoeirista cheio de energia',
|
||||||
|
baseColor: '#FCD34D',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baiana',
|
||||||
|
name: 'Baiana',
|
||||||
|
category: 'bahia',
|
||||||
|
description: 'Uma alegre baiana do acarajé',
|
||||||
|
baseColor: '#F87171',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pescador',
|
||||||
|
name: 'Pescador',
|
||||||
|
category: 'bahia',
|
||||||
|
description: 'Um pescador tradicional da Bahia',
|
||||||
|
baseColor: '#60A5FA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sambista',
|
||||||
|
name: 'Sambista',
|
||||||
|
category: 'bahia',
|
||||||
|
description: 'Um animado sambista',
|
||||||
|
baseColor: '#34D399',
|
||||||
|
},
|
||||||
|
// Amazon Culture
|
||||||
|
{
|
||||||
|
id: 'indigenous',
|
||||||
|
name: 'Indígena',
|
||||||
|
category: 'amazon',
|
||||||
|
description: 'Um guardião da floresta',
|
||||||
|
baseColor: '#A78BFA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ribeirinho',
|
||||||
|
name: 'Ribeirinho',
|
||||||
|
category: 'amazon',
|
||||||
|
description: 'Um conhecedor dos rios',
|
||||||
|
baseColor: '#4ADE80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'seringueira',
|
||||||
|
name: 'Seringueira',
|
||||||
|
category: 'amazon',
|
||||||
|
description: 'Uma protetora da floresta',
|
||||||
|
baseColor: '#F472B6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'artesao',
|
||||||
|
name: 'Artesão',
|
||||||
|
category: 'amazon',
|
||||||
|
description: 'Um artesão da Amazônia',
|
||||||
|
baseColor: '#FB923C',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
onComplete: (avatarId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AvatarSelector({ user, onComplete }: Props) {
|
||||||
|
const [selectedAvatar, setSelectedAvatar] = React.useState(AVATARS[0]);
|
||||||
|
const [category, setCategory] = React.useState<'bahia' | 'amazon'>('bahia');
|
||||||
|
|
||||||
|
const filteredAvatars = AVATARS.filter((avatar) => avatar.category === category);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-green-100 to-blue-100 p-6">
|
||||||
|
<div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-xl p-8">
|
||||||
|
<h1 className="text-3xl font-bold text-center text-green-600 mb-8">
|
||||||
|
Escolha seu Personagem, {user.name}!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-4 mb-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setCategory('bahia')}
|
||||||
|
className={`px-6 py-3 rounded-full text-lg font-medium transition ${
|
||||||
|
category === 'bahia'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-green-100 text-green-600 hover:bg-green-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Cultura Baiana
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCategory('amazon')}
|
||||||
|
className={`px-6 py-3 rounded-full text-lg font-medium transition ${
|
||||||
|
category === 'amazon'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-green-100 text-green-600 hover:bg-green-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Cultura Amazônica
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{filteredAvatars.map((avatar) => (
|
||||||
|
<button
|
||||||
|
key={avatar.id}
|
||||||
|
onClick={() => setSelectedAvatar(avatar)}
|
||||||
|
className={`p-4 rounded-xl transition ${
|
||||||
|
selectedAvatar.id === avatar.id
|
||||||
|
? 'bg-green-100 border-2 border-green-500'
|
||||||
|
: 'bg-gray-50 border-2 border-transparent hover:bg-green-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-32 h-32 mx-auto rounded-full mb-4"
|
||||||
|
style={{ backgroundColor: avatar.baseColor }}
|
||||||
|
/>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
{avatar.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">{avatar.description}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => onComplete(selectedAvatar.id)}
|
||||||
|
className="px-8 py-3 text-lg font-semibold text-white bg-green-600 rounded-xl hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
Continuar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
src/components/RegistrationForm.tsx
Normal file
130
src/components/RegistrationForm.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { User } from '../types';
|
||||||
|
import { Heart, User2, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onComplete: (user: User) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegistrationForm({ onComplete }: Props) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [age, setAge] = useState(8);
|
||||||
|
const [gender, setGender] = useState<'boy' | 'girl' | 'other'>('other');
|
||||||
|
const [showPrivacy, setShowPrivacy] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (name.trim()) {
|
||||||
|
onComplete({
|
||||||
|
name: name.trim(),
|
||||||
|
age,
|
||||||
|
gender,
|
||||||
|
avatarId: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-purple-100 p-6">
|
||||||
|
<div className="max-w-md mx-auto bg-white rounded-2xl shadow-xl p-8">
|
||||||
|
<h1 className="text-3xl font-bold text-center text-purple-600 mb-8">
|
||||||
|
Bem-vindo à Aventura!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-lg font-medium text-gray-700 mb-2">
|
||||||
|
Como você se chama?
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 rounded-lg border-2 border-purple-200 focus:border-purple-500 focus:ring focus:ring-purple-200 transition"
|
||||||
|
required
|
||||||
|
maxLength={20}
|
||||||
|
placeholder="Seu primeiro nome"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-lg font-medium text-gray-700 mb-2">
|
||||||
|
Quantos anos você tem?
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
value={age}
|
||||||
|
onChange={(e) => setAge(Number(e.target.value))}
|
||||||
|
className="w-full h-2 bg-purple-200 rounded-lg appearance-none cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div className="text-center text-2xl font-bold text-purple-600 mt-2">
|
||||||
|
{age} anos
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-lg font-medium text-gray-700 mb-4">
|
||||||
|
Como você se identifica?
|
||||||
|
</label>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
{[
|
||||||
|
{ value: 'boy', label: 'Menino', icon: User2 },
|
||||||
|
{ value: 'girl', label: 'Menina', icon: Heart },
|
||||||
|
{ value: 'other', label: 'Outro', icon: User2 },
|
||||||
|
].map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setGender(option.value as any)}
|
||||||
|
className={`flex flex-col items-center p-4 rounded-xl transition ${
|
||||||
|
gender === option.value
|
||||||
|
? 'bg-purple-100 border-2 border-purple-500'
|
||||||
|
: 'bg-gray-50 border-2 border-transparent hover:bg-purple-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<option.icon
|
||||||
|
className={`w-8 h-8 ${
|
||||||
|
gender === option.value ? 'text-purple-600' : 'text-gray-600'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="mt-2 text-sm font-medium">{option.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPrivacy(!showPrivacy)}
|
||||||
|
className="flex items-center text-sm text-purple-600 hover:text-purple-700"
|
||||||
|
>
|
||||||
|
<Info className="w-4 h-4 mr-1" />
|
||||||
|
Política de Privacidade
|
||||||
|
</button>
|
||||||
|
{showPrivacy && (
|
||||||
|
<div className="mt-2 p-4 bg-purple-50 rounded-lg text-sm">
|
||||||
|
<p>
|
||||||
|
Olá! Nós guardamos apenas seu primeiro nome para tornar suas
|
||||||
|
histórias mais divertidas. Não compartilhamos suas informações
|
||||||
|
com ninguém. Seus pais podem solicitar a exclusão dos dados a
|
||||||
|
qualquer momento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!name.trim()}
|
||||||
|
className="w-full py-3 px-6 text-lg font-semibold text-white bg-purple-600 rounded-xl hover:bg-purple-700 transition disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Começar Aventura!
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/components/StoryViewer.tsx
Normal file
61
src/components/StoryViewer.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Theme, User } from '../types';
|
||||||
|
import { StoryContent } from './story/StoryContent';
|
||||||
|
import { StoryImage } from './story/StoryImage';
|
||||||
|
import { StoryControls } from './story/StoryControls';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
theme: Theme;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoryViewer({ theme, user }: Props) {
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
|
||||||
|
const story = {
|
||||||
|
title: `${user.name} e a Aventura ${theme.title}`,
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
text: `Era uma vez, em um lugar muito especial do Brasil, vivia ${user.name}, uma criança muito curiosa de ${user.age} anos. Um dia, enquanto explorava ${theme.category === 'environment' ? 'a natureza' : 'sua cultura'}, algo mágico aconteceu...`,
|
||||||
|
image: `https://images.unsplash.com/photo-1472162072942-cd5147eb3902?auto=format&fit=crop&q=80&w=800&h=600`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "A jornada estava apenas começando, e muitas descobertas incríveis aguardavam...",
|
||||||
|
image: `https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&q=80&w=800&h=600`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAudio = () => {
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
// In production: implement actual audio playback
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-indigo-100 to-purple-100 p-6">
|
||||||
|
<div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||||
|
<div className="p-6 bg-indigo-600 text-white">
|
||||||
|
<h1 className="text-3xl font-bold text-center">{story.title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StoryImage
|
||||||
|
src={story.pages[currentPage].image}
|
||||||
|
onNext={() => setCurrentPage(currentPage + 1)}
|
||||||
|
onPrev={() => setCurrentPage(currentPage - 1)}
|
||||||
|
hasNext={currentPage < story.pages.length - 1}
|
||||||
|
hasPrev={currentPage > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-8">
|
||||||
|
<StoryContent page={story.pages[currentPage]} />
|
||||||
|
<StoryControls
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onToggleAudio={toggleAudio}
|
||||||
|
title={story.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
src/components/ThemeSelector.tsx
Normal file
152
src/components/ThemeSelector.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Theme } from '../types';
|
||||||
|
import {
|
||||||
|
Leaf,
|
||||||
|
Users,
|
||||||
|
PartyPopper,
|
||||||
|
History,
|
||||||
|
Droplets,
|
||||||
|
Dog,
|
||||||
|
Recycle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const THEMES: Theme[] = [
|
||||||
|
{
|
||||||
|
id: 'nature',
|
||||||
|
title: 'Natureza e Meio Ambiente',
|
||||||
|
description: 'Explore as maravilhas do nosso planeta',
|
||||||
|
icon: 'Leaf',
|
||||||
|
sdsGoal: 13,
|
||||||
|
category: 'environment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'indigenous',
|
||||||
|
title: 'Povos Originários do Brasil',
|
||||||
|
description: 'Conheça as culturas indígenas',
|
||||||
|
icon: 'Users',
|
||||||
|
sdsGoal: 10,
|
||||||
|
category: 'culture',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'traditions',
|
||||||
|
title: 'Festas e Tradições Brasileiras',
|
||||||
|
description: 'Celebre nossa cultura',
|
||||||
|
icon: 'PartyPopper',
|
||||||
|
sdsGoal: 11,
|
||||||
|
category: 'culture',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quilombola',
|
||||||
|
title: 'Histórias Quilombolas',
|
||||||
|
description: 'Descubra nossa história',
|
||||||
|
icon: 'History',
|
||||||
|
sdsGoal: 10,
|
||||||
|
category: 'culture',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'water',
|
||||||
|
title: 'Água é Vida',
|
||||||
|
description: 'Aprenda sobre esse recurso precioso',
|
||||||
|
icon: 'Droplets',
|
||||||
|
sdsGoal: 6,
|
||||||
|
category: 'environment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'animals',
|
||||||
|
title: 'Animais Brasileiros',
|
||||||
|
description: 'Conheça nossa fauna',
|
||||||
|
icon: 'Dog',
|
||||||
|
sdsGoal: 15,
|
||||||
|
category: 'nature',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sustainable',
|
||||||
|
title: 'Vivendo Sustentável',
|
||||||
|
description: 'Cuide do nosso futuro',
|
||||||
|
icon: 'Recycle',
|
||||||
|
sdsGoal: 12,
|
||||||
|
category: 'environment',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const IconMap = {
|
||||||
|
Leaf,
|
||||||
|
Users,
|
||||||
|
PartyPopper,
|
||||||
|
History,
|
||||||
|
Droplets,
|
||||||
|
Dog,
|
||||||
|
Recycle,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSelect: (theme: Theme) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeSelector({ onSelect }: Props) {
|
||||||
|
const [selectedCategory, setSelectedCategory] = React.useState('all');
|
||||||
|
|
||||||
|
const filteredThemes =
|
||||||
|
selectedCategory === 'all'
|
||||||
|
? THEMES
|
||||||
|
: THEMES.filter((theme) => theme.category === selectedCategory);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-yellow-100 to-orange-100 p-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold text-center text-orange-600 mb-8">
|
||||||
|
Escolha sua Aventura!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-4 mb-8">
|
||||||
|
{[
|
||||||
|
{ id: 'all', label: 'Todos' },
|
||||||
|
{ id: 'environment', label: 'Meio Ambiente' },
|
||||||
|
{ id: 'culture', label: 'Cultura' },
|
||||||
|
{ id: 'nature', label: 'Natureza' },
|
||||||
|
].map((category) => (
|
||||||
|
<button
|
||||||
|
key={category.id}
|
||||||
|
onClick={() => setSelectedCategory(category.id)}
|
||||||
|
className={`px-6 py-3 rounded-full text-lg font-medium transition ${
|
||||||
|
selectedCategory === category.id
|
||||||
|
? 'bg-orange-600 text-white'
|
||||||
|
: 'bg-orange-100 text-orange-600 hover:bg-orange-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{filteredThemes.map((theme) => {
|
||||||
|
const Icon = IconMap[theme.icon as keyof typeof IconMap];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={theme.id}
|
||||||
|
onClick={() => onSelect(theme)}
|
||||||
|
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="p-3 rounded-full bg-orange-100 group-hover:bg-orange-200 transition">
|
||||||
|
<Icon className="w-8 h-8 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800">
|
||||||
|
{theme.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">{theme.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-orange-600 font-medium">
|
||||||
|
ODS {theme.sdsGoal}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
src/components/WelcomePage.tsx
Normal file
65
src/components/WelcomePage.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Book, Sparkles, Shield } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onLoginClick: () => void;
|
||||||
|
onRegisterClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WelcomePage({ onLoginClick, onRegisterClick }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
||||||
|
<div className="max-w-6xl mx-auto px-6 py-16">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h1 className="text-5xl font-bold text-purple-600 mb-4">
|
||||||
|
Histórias Mágicas
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600">
|
||||||
|
Embarque em uma jornada de aprendizado e diversão!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg text-center">
|
||||||
|
<Book className="w-12 h-12 text-purple-600 mx-auto mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Histórias Educativas</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Aprenda sobre cultura, meio ambiente e muito mais
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg text-center">
|
||||||
|
<Sparkles className="w-12 h-12 text-purple-600 mx-auto mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Personalize sua Jornada</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Crie seu próprio personagem e escolha suas aventuras
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-xl shadow-lg text-center">
|
||||||
|
<Shield className="w-12 h-12 text-purple-600 mx-auto mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Ambiente Seguro</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Conteúdo adequado e proteção de dados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={onLoginClick}
|
||||||
|
className="px-8 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onRegisterClick}
|
||||||
|
className="px-8 py-3 bg-white text-purple-600 rounded-lg hover:bg-purple-50 transition"
|
||||||
|
>
|
||||||
|
Criar Conta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/components/auth/LoginForm.tsx
Normal file
98
src/components/auth/LoginForm.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { LogIn } from 'lucide-react';
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onLogin: (email: string, password: string) => void;
|
||||||
|
onRegisterClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginForm({ onLogin, onRegisterClick }: Props) {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const { signIn } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { user } = await signIn(email, password);
|
||||||
|
if (user) {
|
||||||
|
onLogin(email, password);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao fazer login. Verifique suas credenciais.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-3xl font-bold text-center text-purple-600">
|
||||||
|
Bem-vindo de volta!
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-gray-600">
|
||||||
|
Continue sua jornada de histórias mágicas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||||
|
Senha
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-lg shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<LogIn className="w-5 h-5" />
|
||||||
|
Entrar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRegisterClick}
|
||||||
|
className="text-purple-600 hover:text-purple-500"
|
||||||
|
>
|
||||||
|
Criar uma nova conta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/components/library/StoryLibrary.tsx
Normal file
48
src/components/library/StoryLibrary.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Book, Clock } from 'lucide-react';
|
||||||
|
import { SavedStory } from '../../types/auth';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stories: SavedStory[];
|
||||||
|
onStorySelect: (storyId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoryLibrary({ stories, onStorySelect }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto p-6">
|
||||||
|
<h1 className="text-3xl font-bold text-purple-600 mb-8">
|
||||||
|
Minha Biblioteca de Histórias
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{stories.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Book className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Você ainda não tem histórias salvas.
|
||||||
|
<br />
|
||||||
|
Comece uma nova aventura agora!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{stories.map((story) => (
|
||||||
|
<button
|
||||||
|
key={story.id}
|
||||||
|
onClick={() => onStorySelect(story.id)}
|
||||||
|
className="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition text-left"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-2">
|
||||||
|
{story.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">Tema: {story.theme}</p>
|
||||||
|
<div className="flex items-center text-sm text-gray-500">
|
||||||
|
<Clock className="w-4 h-4 mr-1" />
|
||||||
|
{new Date(story.createdAt).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/components/story/StoryContent.tsx
Normal file
14
src/components/story/StoryContent.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { StoryPage } from '../../types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: StoryPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoryContent({ page }: Props) {
|
||||||
|
return (
|
||||||
|
<p className="text-xl leading-relaxed text-gray-700 mb-8">
|
||||||
|
{page.text}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/components/story/StoryControls.tsx
Normal file
55
src/components/story/StoryControls.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Share2, Download, Volume2, VolumeX } from 'lucide-react';
|
||||||
|
import { useShare } from '../../hooks/useShare';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isPlaying: boolean;
|
||||||
|
onToggleAudio: () => void;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoryControls({ isPlaying, onToggleAudio, title }: Props) {
|
||||||
|
const { share, isShareSupported } = useShare();
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
await share({
|
||||||
|
title,
|
||||||
|
text: 'Confira esta história incrível!',
|
||||||
|
url: window.location.href,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={onToggleAudio}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-indigo-100 text-indigo-600 rounded-full hover:bg-indigo-200 transition"
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<VolumeX className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<Volume2 className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
{isPlaying ? 'Pausar Áudio' : 'Ouvir História'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isShareSupported && (
|
||||||
|
<button
|
||||||
|
onClick={handleShare}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-indigo-100 text-indigo-600 rounded-full hover:bg-indigo-200 transition"
|
||||||
|
>
|
||||||
|
<Share2 className="w-5 h-5" />
|
||||||
|
Compartilhar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {/* Implement PDF download */}}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-indigo-100 text-indigo-600 rounded-full hover:bg-indigo-200 transition"
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
Salvar PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/story/StoryImage.tsx
Normal file
40
src/components/story/StoryImage.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src: string;
|
||||||
|
onNext: () => void;
|
||||||
|
onPrev: () => void;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoryImage({ src, onNext, onPrev, hasNext, hasPrev }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="relative aspect-video">
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt="Ilustração da história"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasPrev && (
|
||||||
|
<button
|
||||||
|
onClick={onPrev}
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 p-2 bg-white/80 rounded-full hover:bg-white transition"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-6 h-6 text-indigo-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasNext && (
|
||||||
|
<button
|
||||||
|
onClick={onNext}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 bg-white/80 rounded-full hover:bg-white transition"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-6 h-6 text-indigo-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/contexts/AuthContext.tsx
Normal file
24
src/contexts/AuthContext.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createContext, useContext, ReactNode } from 'react'
|
||||||
|
import { useAuth } from '../hooks/useAuth'
|
||||||
|
|
||||||
|
type AuthContextType = ReturnType<typeof useAuth>
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const auth = useAuth()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={auth}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuthContext() {
|
||||||
|
const context = useContext(AuthContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuthContext deve ser usado dentro de um AuthProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
74
src/hooks/useAuth.ts
Normal file
74
src/hooks/useAuth.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { supabase } from '../lib/supabase'
|
||||||
|
import { User } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Verificar sessão atual
|
||||||
|
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||||
|
setUser(session?.user ?? null)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Escutar mudanças de autenticação
|
||||||
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
|
setUser(session?.user ?? null)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const signIn = async (email: string, password: string) => {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao fazer login:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signUp = async (email: string, password: string) => {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.auth.signUp({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
options: {
|
||||||
|
emailRedirectTo: `${window.location.origin}/auth/callback`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar conta:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
try {
|
||||||
|
const { error } = await supabase.auth.signOut()
|
||||||
|
if (error) throw error
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao fazer logout:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
loading,
|
||||||
|
signIn,
|
||||||
|
signUp,
|
||||||
|
signOut
|
||||||
|
}
|
||||||
|
}
|
||||||
207
src/hooks/useDatabase.ts
Normal file
207
src/hooks/useDatabase.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
import { School, Teacher, Class, Student, TeacherClass, Story, StoryPage } from '../types/database';
|
||||||
|
|
||||||
|
export function useDatabase() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Funções para escolas
|
||||||
|
const getSchools = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('schools')
|
||||||
|
.select('*');
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao buscar escolas');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funções para professores
|
||||||
|
const getTeachersBySchool = async (schoolId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('teachers')
|
||||||
|
.select('*')
|
||||||
|
.eq('school_id', schoolId);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao buscar professores');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funções para turmas
|
||||||
|
const getClassesBySchool = async (schoolId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('classes')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
students (*)
|
||||||
|
`)
|
||||||
|
.eq('school_id', schoolId);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao buscar turmas');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funções para alunos
|
||||||
|
const getStudentsByClass = async (classId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('students')
|
||||||
|
.select('*')
|
||||||
|
.eq('class_id', classId);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao buscar alunos');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funções para histórias
|
||||||
|
const getStoriesByStudent = async (studentId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.select('*')
|
||||||
|
.eq('student_id', studentId)
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao buscar histórias');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStory = async (studentId: string, story: Omit<Story, 'id' | 'created_at' | 'updated_at'>) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.insert([
|
||||||
|
{
|
||||||
|
student_id: studentId,
|
||||||
|
title: story.title,
|
||||||
|
theme: story.theme,
|
||||||
|
content: story.content,
|
||||||
|
status: story.status
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao criar história');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStory = async (storyId: string, updates: Partial<Story>) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.update(updates)
|
||||||
|
.eq('id', storyId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao atualizar história');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPageToStory = async (storyId: string, page: StoryPage) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data: story, error: fetchError } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.select('content')
|
||||||
|
.eq('id', storyId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError) throw fetchError;
|
||||||
|
|
||||||
|
const updatedContent = {
|
||||||
|
...story.content,
|
||||||
|
pages: [...story.content.pages, page]
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.update({ content: updatedContent })
|
||||||
|
.eq('id', storyId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
setError('Erro ao adicionar página');
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
getSchools,
|
||||||
|
getTeachersBySchool,
|
||||||
|
getClassesBySchool,
|
||||||
|
getStudentsByClass,
|
||||||
|
getStoriesByStudent,
|
||||||
|
createStory,
|
||||||
|
updateStory,
|
||||||
|
addPageToStory
|
||||||
|
};
|
||||||
|
}
|
||||||
25
src/hooks/useShare.ts
Normal file
25
src/hooks/useShare.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
interface ShareData {
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShare() {
|
||||||
|
const isShareSupported = typeof navigator !== 'undefined' && !!navigator.share;
|
||||||
|
|
||||||
|
const share = async (data: ShareData) => {
|
||||||
|
if (!isShareSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.share(data);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name !== 'AbortError') {
|
||||||
|
console.error('Error sharing:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { share, isShareSupported };
|
||||||
|
}
|
||||||
3
src/index.css
Normal file
3
src/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
6
src/lib/supabase.ts
Normal file
6
src/lib/supabase.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const supabaseUrl = 'https://bsjlbnyslxzsdwxvkaap.supabase.co'
|
||||||
|
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJzamxibnlzbHh6c2R3eHZrYWFwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ2MzUzNzYsImV4cCI6MjA1MDIxMTM3Nn0.ygEUrAu2ZnCkfgS4-k4Puvk7ywkn3U7Bnzh7BSOQWFo'
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
13
src/types/auth.ts
Normal file
13
src/types/auth.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export interface AuthUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedStory {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
theme: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastReadPage: number;
|
||||||
|
}
|
||||||
106
src/types/database.ts
Normal file
106
src/types/database.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
export interface School {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
address?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Teacher {
|
||||||
|
id: string;
|
||||||
|
school_id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone?: string;
|
||||||
|
subject?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Class {
|
||||||
|
id: string;
|
||||||
|
school_id: string;
|
||||||
|
name: string;
|
||||||
|
grade: string;
|
||||||
|
year: number;
|
||||||
|
period?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Student {
|
||||||
|
id: string;
|
||||||
|
class_id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
birth_date?: string;
|
||||||
|
guardian_name?: string;
|
||||||
|
guardian_phone?: string;
|
||||||
|
guardian_email?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeacherClass {
|
||||||
|
id: string;
|
||||||
|
teacher_id: string;
|
||||||
|
class_id: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tipos para relacionamentos
|
||||||
|
export interface ClassWithTeachers extends Class {
|
||||||
|
teachers: Teacher[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeacherWithClasses extends Teacher {
|
||||||
|
classes: Class[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassWithStudents extends Class {
|
||||||
|
students: Student[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SchoolWithRelations extends School {
|
||||||
|
teachers: Teacher[];
|
||||||
|
classes: ClassWithStudents[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoryPage {
|
||||||
|
text: string;
|
||||||
|
image?: string;
|
||||||
|
audio?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Story {
|
||||||
|
id: string;
|
||||||
|
student_id: string;
|
||||||
|
title: string;
|
||||||
|
theme: string;
|
||||||
|
content: {
|
||||||
|
pages: StoryPage[];
|
||||||
|
currentPage?: number;
|
||||||
|
lastModified?: string;
|
||||||
|
};
|
||||||
|
status: 'draft' | 'published' | 'archived';
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizando a interface Student para incluir histórias
|
||||||
|
export interface StudentWithStories extends Student {
|
||||||
|
stories: Story[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizando ClassWithStudents para incluir histórias dos alunos
|
||||||
|
export interface ClassWithStudentsAndStories extends Class {
|
||||||
|
students: StudentWithStories[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizando SchoolWithRelations
|
||||||
|
export interface SchoolWithRelations extends School {
|
||||||
|
teachers: Teacher[];
|
||||||
|
classes: ClassWithStudentsAndStories[];
|
||||||
|
}
|
||||||
28
src/types/index.ts
Normal file
28
src/types/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export interface User {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
gender: 'boy' | 'girl' | 'other';
|
||||||
|
avatarId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Avatar {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: 'bahia' | 'amazon';
|
||||||
|
description: string;
|
||||||
|
baseColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Theme {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
sdsGoal: number;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoryPage {
|
||||||
|
text: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
24
tsconfig.app.json
Normal file
24
tsconfig.app.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['lucide-react'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user