feat: integra sistema de idiomas com banco de dados
Some checks failed
Docker Build and Push / build (push) Has been cancelled

- Integra completamente com a tabela languages
- Adiciona suporte para ícones de bandeira e instruções
- Remove LANGUAGE_OPTIONS hard coded
- Usa DEFAULT_LANGUAGE do type
- Melhora validações e UX do seletor de idiomas
- Atualiza CHANGELOG.md para versão 1.4.0
This commit is contained in:
Lucas Santana 2025-02-02 08:20:33 -03:00
parent ba93f3ef29
commit abe4ce86d4
7 changed files with 146 additions and 45 deletions

View File

@ -5,6 +5,7 @@ import { useSession } from '../../hooks/useSession';
import { useStoryCategories } from '../../hooks/useStoryCategories';
import { Wand2, ArrowLeft, Globe } from 'lucide-react';
import { useStudentTracking } from '../../hooks/useStudentTracking';
import { useLanguages } from '../../hooks/useLanguages';
interface Category {
id: string;
@ -44,12 +45,6 @@ interface StoryGeneratorProps {
setChoices: React.Dispatch<React.SetStateAction<StoryChoices>>;
}
const LANGUAGE_OPTIONS = [
{ value: 'pt-BR', label: 'Português (Brasil)' },
{ value: 'en-US', label: 'Inglês (EUA)' },
{ value: 'es-ES', label: 'Espanhol (Espanha)' }
] as const;
export function StoryGenerator({
initialContext = '',
onContextChange,
@ -62,7 +57,8 @@ export function StoryGenerator({
choices,
setChoices
}: StoryGeneratorProps) {
const { themes, subjects, characters, settings, isLoading } = useStoryCategories();
const { themes, subjects, characters, settings, isLoading: isCategoriesLoading } = useStoryCategories();
const { languages, supportedLanguages, isLoading: isLanguagesLoading } = useLanguages();
// Definir steps com os dados obtidos
const steps: StoryStep[] = [
@ -173,7 +169,8 @@ export function StoryGenerator({
const handleLanguageSelect = (language: string) => {
console.log('Selecionando idioma:', language);
if (!LANGUAGE_OPTIONS.some(opt => opt.value === language)) {
const selectedLanguage = languages.find(lang => lang.code === language);
if (!selectedLanguage) {
setError('Idioma inválido selecionado');
return;
}
@ -252,7 +249,7 @@ export function StoryGenerator({
}
// Validar idioma
if (!choices.language_type || !LANGUAGE_OPTIONS.some(opt => opt.value === choices.language_type)) {
if (!choices.language_type || !languages.some(lang => lang.code === choices.language_type)) {
setError('Idioma não selecionado ou inválido');
return;
}
@ -473,7 +470,7 @@ export function StoryGenerator({
}
};
if (isLoading) {
if (isCategoriesLoading || isLanguagesLoading) {
return (
<div className="animate-pulse space-y-8">
<div className="h-2 bg-gray-200 rounded-full" />
@ -506,27 +503,38 @@ export function StoryGenerator({
{currentStep.isLanguageStep ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{LANGUAGE_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => handleLanguageSelect(option.value)}
className={`p-6 rounded-xl border-2 transition-all text-left ${
choices.language_type === option.value
? 'border-purple-500 bg-purple-50'
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-3">
<Globe className="h-6 w-6 text-purple-600" />
<div>
<h3 className="font-medium text-gray-900">{option.label}</h3>
<p className="text-sm text-gray-600">
Escreva sua história em {option.label}
</p>
{supportedLanguages.map((option) => {
const languageDetails = languages.find(lang => lang.code === option.value);
return (
<button
key={option.value}
onClick={() => handleLanguageSelect(option.value)}
className={`p-6 rounded-xl border-2 transition-all text-left ${
choices.language_type === option.value
? 'border-purple-500 bg-purple-50'
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-3">
{languageDetails?.flag_icon ? (
<img
src={languageDetails.flag_icon}
alt={`Bandeira ${option.label}`}
className="h-6 w-6 object-cover rounded-full"
/>
) : (
<Globe className="h-6 w-6 text-purple-600" />
)}
<div>
<h3 className="font-medium text-gray-900">{option.label}</h3>
<p className="text-sm text-gray-600">
{`Escreva sua história em ${option.label}`}
</p>
</div>
</div>
</div>
</button>
))}
</button>
);
})}
</div>
) : currentStep.isContextStep ? (
<div className="space-y-4">

44
src/hooks/useLanguages.ts Normal file
View File

@ -0,0 +1,44 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '../lib/supabase';
import type { Language, SupportedLanguageCode, LanguageOption } from '../types/language';
interface UseLanguagesReturn {
languages: Language[];
isLoading: boolean;
error: Error | null;
getLanguageLabel: (code: SupportedLanguageCode) => string;
supportedLanguages: LanguageOption[];
}
export function useLanguages(): UseLanguagesReturn {
const { data: languages = [], isLoading, error } = useQuery<Language[]>({
queryKey: ['languages'],
queryFn: async () => {
const { data, error } = await supabase
.from('languages')
.select('*')
.order('name');
if (error) throw error;
return data;
}
});
const supportedLanguages: LanguageOption[] = languages.map(lang => ({
value: lang.code as SupportedLanguageCode,
label: lang.name
}));
const getLanguageLabel = (code: SupportedLanguageCode): string => {
const language = languages.find(lang => lang.code === code);
return language?.name || code;
};
return {
languages,
isLoading,
error: error as Error | null,
getLanguageLabel,
supportedLanguages
};
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { ArrowLeft, Sparkles, Globe } from 'lucide-react';
import { ArrowLeft, Sparkles } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { StoryGenerator } from '../../components/story/StoryGenerator';
import { useSession } from '../../hooks/useSession';
@ -9,12 +9,7 @@ import { useUppercasePreference } from '../../hooks/useUppercasePreference';
import { useSpeechRecognition } from '@/features/voice-commands/hooks/useSpeechRecognition';
import { VoiceCommandButton } from '@/features/voice-commands/components/VoiceCommandButton';
import type { StoryChoices } from '@/components/story/StoryGenerator';
const LANGUAGE_OPTIONS = [
{ value: 'pt-BR', label: 'Português (Brasil)' },
{ value: 'en-US', label: 'Inglês (EUA)' },
{ value: 'es-ES', label: 'Espanhol (Espanha)' }
] as const;
import { DEFAULT_LANGUAGE } from '@/types/language';
export function CreateStoryPage() {
const navigate = useNavigate();
@ -45,7 +40,7 @@ export function CreateStoryPage() {
character_id: null,
setting_id: null,
context: '',
language_type: 'pt-BR'
language_type: DEFAULT_LANGUAGE
});
// Manipuladores para gravação de voz

38
src/types/language.ts Normal file
View File

@ -0,0 +1,38 @@
import { supabase } from "@/lib/supabase";
export interface Language {
id: string;
name: string;
code: string;
instructions: string | null;
flag_icon: string | null;
created_at: string;
updated_at: string;
}
// Busca as opções de idioma do banco de dados
export const getLanguageOptions = async () => {
const { data: languages, error } = await supabase
.from('languages')
.select('code, name')
.order('name');
if (error) {
console.error('Erro ao buscar idiomas:', error);
return [];
}
return languages.map(lang => ({
value: lang.code,
label: lang.name
}));
};
export interface LanguageOption {
value: string;
label: string;
}
export type SupportedLanguageCode = string;
export const DEFAULT_LANGUAGE = 'pt-BR';

View File

@ -87,6 +87,8 @@
| public | student_phonics_achievements | student_id | FOREIGN KEY |
| public | phonics_word_audio | id | PRIMARY KEY |
| public | phonics_word_audio | word | UNIQUE |
| public | languages | code | UNIQUE |
| public | languages | id | PRIMARY KEY |
| public | story_recordings | story_id | FOREIGN KEY |
| public | story_recordings | story_id | FOREIGN KEY |
| public | story_recordings | | CHECK |
@ -291,6 +293,11 @@
| public | phonics_word_audio | | CHECK |
| public | phonics_word_audio | | CHECK |
| public | phonics_word_audio | | CHECK |
| public | languages | | CHECK |
| public | languages | | CHECK |
| public | languages | | CHECK |
| public | languages | | CHECK |
| public | languages | | CHECK |
| public | story_recordings | | CHECK |
| public | story_recordings | | CHECK |
| public | story_recordings | | CHECK |

View File

@ -72,8 +72,8 @@
| realtime | subscription | claims | jsonb | NO | |
| realtime | messages | inserted_at | timestamp without time zone | NO | now() |
| public | phonics_categories | level | integer | NO | |
| storage | objects | bucket_id | text | YES | |
| public | story_characters | slug | text | NO | |
| storage | objects | bucket_id | text | YES | |
| public | students | birth_date | date | YES | |
| auth | refresh_tokens | revoked | boolean | YES | |
| storage | s3_multipart_uploads | owner_id | text | YES | |
@ -89,6 +89,7 @@
| public | phonics_words | created_at | timestamp with time zone | YES | CURRENT_TIMESTAMP |
| public | teacher_invites | expires_at | timestamp with time zone | NO | |
| auth | identities | provider_id | text | NO | |
| public | languages | name | character varying | NO | |
| net | _http_response | headers | jsonb | YES | |
| auth | mfa_challenges | otp_code | text | YES | |
| public | stories | character_id | uuid | YES | |
@ -259,8 +260,8 @@
| public | story_settings | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| public | story_subjects | description | text | NO | |
| public | stories | content | jsonb | NO | |
| auth | flow_state | provider_access_token | text | YES | |
| vault | secrets | secret | text | NO | |
| auth | flow_state | provider_access_token | text | YES | |
| public | interests | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| public | teachers | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| storage | s3_multipart_uploads | created_at | timestamp with time zone | NO | now() |
@ -325,12 +326,13 @@
| extensions | pg_stat_statements | blk_read_time | double precision | YES | |
| public | phonics_exercises | title | character varying | NO | |
| auth | saml_providers | id | uuid | NO | |
| public | languages | flag_icon | character varying | YES | |
| auth | sessions | user_agent | text | YES | |
| public | story_recordings | processed_at | timestamp with time zone | YES | |
| storage | s3_multipart_uploads_parts | bucket_id | text | NO | |
| pgsodium | decrypted_key | name | text | YES | |
| public | phonics_words | word | character varying | NO | |
| public | teachers | name | text | NO | |
| public | phonics_words | word | character varying | NO | |
| auth | sso_providers | created_at | timestamp with time zone | YES | |
| storage | buckets | file_size_limit | bigint | YES | |
| auth | sso_domains | sso_provider_id | uuid | NO | |
@ -406,6 +408,7 @@
| storage | s3_multipart_uploads_parts | key | text | NO | |
| public | story_details | updated_at | timestamp with time zone | YES | |
| auth | mfa_challenges | factor_id | uuid | NO | |
| public | languages | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| pgsodium | masking_rule | attrelid | oid | YES | |
| public | students | name | text | NO | |
| storage | s3_multipart_uploads_parts | owner_id | text | YES | |
@ -415,8 +418,8 @@
| public | story_details | subject_icon | text | YES | |
| auth | schema_migrations | version | character varying | NO | |
| public | phonics_word_audio | audio_path | text | NO | |
| auth | one_time_tokens | token_hash | text | NO | |
| public | story_settings | slug | text | NO | |
| auth | one_time_tokens | token_hash | text | NO | |
| public | story_recordings | transcription | text | YES | |
| storage | objects | metadata | jsonb | YES | |
| public | story_characters | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
@ -436,8 +439,8 @@
| public | story_details | content | jsonb | YES | |
| public | teachers | class_ids | ARRAY | YES | |
| extensions | pg_stat_statements | shared_blks_hit | bigint | YES | |
| public | teacher_classes | class_id | uuid | NO | |
| public | story_generations | id | uuid | NO | uuid_generate_v4() |
| public | teacher_classes | class_id | uuid | NO | |
| auth | flow_state | auth_code_issued_at | timestamp with time zone | YES | |
| public | media_types | id | uuid | NO | uuid_generate_v4() |
| public | students | avatar_url | text | YES | |
@ -473,8 +476,8 @@
| auth | mfa_factors | user_id | uuid | NO | |
| public | phonics_word_audio | id | uuid | NO | uuid_generate_v4() |
| public | students | guardian_phone | text | YES | |
| pgsodium | masking_rule | relname | name | YES | |
| storage | s3_multipart_uploads | key | text | NO | |
| pgsodium | masking_rule | relname | name | YES | |
| auth | sessions | ip | inet | YES | |
| auth | refresh_tokens | updated_at | timestamp with time zone | YES | |
| public | story_recordings | pronunciation_score | integer | YES | |
@ -486,8 +489,8 @@
| pgsodium | key | key_context | bytea | YES | '\x7067736f6469756d'::bytea |
| public | story_settings | icon | text | NO | |
| public | story_generations | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| storage | objects | name | text | YES | |
| public | story_characters | title | text | NO | |
| storage | objects | name | text | YES | |
| public | student_achievements | id | uuid | NO | uuid_generate_v4() |
| public | story_exercise_words | syllable_pattern | text | YES | |
| auth | flow_state | code_challenge | text | NO | |
@ -501,6 +504,7 @@
| public | student_phonics_achievements | student_id | uuid | YES | |
| public | teachers | phone | text | YES | |
| vault | decrypted_secrets | name | text | YES | |
| public | languages | code | character varying | NO | |
| auth | instances | id | uuid | NO | |
| public | story_pages | text | text | NO | |
| pgsodium | key | associated_data | text | YES | 'associated'::text |
@ -551,6 +555,7 @@
| pgsodium | decrypted_key | decrypted_raw_key | bytea | YES | |
| auth | instances | created_at | timestamp with time zone | YES | |
| storage | migrations | hash | character varying | NO | |
| public | languages | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| public | story_recordings | error_message | text | YES | |
| public | story_pages | id | uuid | NO | uuid_generate_v4() |
| public | student_phonics_attempts | created_at | timestamp with time zone | YES | CURRENT_TIMESTAMP |
@ -582,6 +587,7 @@
| public | story_pages | page_number | integer | NO | |
| extensions | pg_stat_statements | temp_blk_read_time | double precision | YES | |
| net | _http_response | status_code | integer | YES | |
| public | languages | instructions | text | YES | |
| public | phonics_exercises | description | text | YES | |
| auth | saml_relay_states | request_id | text | NO | |
| storage | s3_multipart_uploads_parts | part_number | integer | NO | |
@ -603,6 +609,7 @@
| pgsodium | masking_rule | key_id_column | text | YES | |
| storage | s3_multipart_uploads_parts | id | uuid | NO | gen_random_uuid() |
| auth | identities | user_id | uuid | NO | |
| public | languages | id | uuid | NO | uuid_generate_v4() |
| public | story_pages | image_path | text | YES | |
| auth | saml_providers | metadata_url | text | YES | |
| auth | instances | raw_base_config | text | YES | |

View File

@ -9,6 +9,8 @@
| 65876 | public | interests | Students can insert their own interests | a | | (auth.uid() = student_id) |
| 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) |
| 65875 | public | interests | Students can view their own interests | r | (auth.uid() = student_id) | |
| 104599 | public | languages | Allow insert/update for admins only | * | ((auth.jwt() ->> 'role'::text) = 'admin'::text) | ((auth.jwt() ->> 'role'::text) = 'admin'::text) |
| 104598 | public | languages | Allow read access for all authenticated users | r | true | |
| 79931 | public | phonics_categories | Permitir leitura de categorias fonéticas para usuários autent | r | true | |
| 79932 | public | phonics_exercise_types | Permitir leitura de tipos de exercícios fonéticos para usuár | r | true | |
| 79934 | public | phonics_exercise_words | Permitir leitura de relações exercício-palavra para usuário | r | true | |