mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
feat: implementa otimização global de imagens
- Adiciona função utilitária para otimização de imagens - Converte automaticamente para WebP - Implementa redimensionamento contextual - Centraliza lógica de transformação - Melhora performance de carregamento
This commit is contained in:
parent
7087a87ece
commit
02119a62d1
11
CHANGELOG.md
11
CHANGELOG.md
@ -39,6 +39,15 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
- Melhor performance em navegação
|
- Melhor performance em navegação
|
||||||
|
|
||||||
### Modificado
|
### Modificado
|
||||||
|
- Otimização global de imagens
|
||||||
|
- Conversão automática para WebP
|
||||||
|
- Redimensionamento otimizado por contexto
|
||||||
|
- Parâmetros de qualidade personalizados
|
||||||
|
- Função utilitária centralizada
|
||||||
|
- Implementação em todas as rotas
|
||||||
|
- Otimização contextual por uso
|
||||||
|
- Pré-carregamento otimizado
|
||||||
|
|
||||||
- Otimização de imagens de capa
|
- Otimização de imagens de capa
|
||||||
- Uso da primeira página como capa
|
- Uso da primeira página como capa
|
||||||
- Tamanho reduzido para thumbnails
|
- Tamanho reduzido para thumbnails
|
||||||
@ -69,7 +78,7 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
- Melhor tratamento de estados de loading e erro
|
- Melhor tratamento de estados de loading e erro
|
||||||
- Implementação de componente ImageWithLoading
|
- Implementação de componente ImageWithLoading
|
||||||
- Sistema de cache de imagens
|
- Sistema de cache de imagens
|
||||||
- Otimização de URLs de imagem
|
- Otimizaç<EFBFBD><EFBFBD>o de URLs de imagem
|
||||||
|
|
||||||
- Refatoração de componentes para melhor reuso
|
- Refatoração de componentes para melhor reuso
|
||||||
- Separação de lógica de carregamento de imagens
|
- Separação de lógica de carregamento de imagens
|
||||||
|
|||||||
30
src/lib/imageUtils.ts
Normal file
30
src/lib/imageUtils.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
interface ImageOptions {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
quality?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOptimizedImageUrl(url: string, options: ImageOptions = {}): string {
|
||||||
|
const {
|
||||||
|
width = 800,
|
||||||
|
height = undefined,
|
||||||
|
quality = 80
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Se for URL do Supabase Storage
|
||||||
|
if (url.includes('storage.googleapis.com')) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
width: width.toString(),
|
||||||
|
quality: quality.toString(),
|
||||||
|
format: 'webp'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (height) {
|
||||||
|
params.append('height', height.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${url}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { AudioRecorder } from '../../components/story/AudioRecorder';
|
|||||||
import type { Story } from '../../types/database';
|
import type { Story } from '../../types/database';
|
||||||
import { StoryMetrics } from '../../components/story/StoryMetrics';
|
import { StoryMetrics } from '../../components/story/StoryMetrics';
|
||||||
import type { MetricsData } from '../../components/story/StoryMetrics';
|
import type { MetricsData } from '../../components/story/StoryMetrics';
|
||||||
|
import { getOptimizedImageUrl } from '../../lib/imageUtils';
|
||||||
|
|
||||||
interface StoryRecording {
|
interface StoryRecording {
|
||||||
id: string;
|
id: string;
|
||||||
@ -304,7 +305,10 @@ export function StoryPage() {
|
|||||||
const nextImageUrl = story?.content?.pages?.[currentPage + 1]?.image;
|
const nextImageUrl = story?.content?.pages?.[currentPage + 1]?.image;
|
||||||
if (nextImageUrl) {
|
if (nextImageUrl) {
|
||||||
const nextImage = new Image();
|
const nextImage = new Image();
|
||||||
nextImage.src = nextImageUrl;
|
nextImage.src = getOptimizedImageUrl(nextImageUrl, {
|
||||||
|
width: 1200,
|
||||||
|
quality: 85
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentPage, story]);
|
}, [currentPage, story]);
|
||||||
|
|
||||||
@ -398,7 +402,10 @@ export function StoryPage() {
|
|||||||
{/* Imagem da página atual */}
|
{/* Imagem da página atual */}
|
||||||
{story?.content?.pages?.[currentPage]?.image && (
|
{story?.content?.pages?.[currentPage]?.image && (
|
||||||
<ImageWithLoading
|
<ImageWithLoading
|
||||||
src={story.content.pages[currentPage].image}
|
src={getOptimizedImageUrl(story.content.pages[currentPage].image, {
|
||||||
|
width: 1200,
|
||||||
|
quality: 85
|
||||||
|
})}
|
||||||
alt={`Página ${currentPage + 1}`}
|
alt={`Página ${currentPage + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Plus, BookOpen, Clock, TrendingUp, Award } from 'lucide-react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import type { Story, Student } from '../../types/database';
|
import type { Story, Student } from '../../types/database';
|
||||||
|
import { getOptimizedImageUrl } from '../../lib/imageUtils';
|
||||||
|
|
||||||
interface DashboardMetrics {
|
interface DashboardMetrics {
|
||||||
totalStories: number;
|
totalStories: number;
|
||||||
@ -252,7 +253,10 @@ export function StudentDashboardPage() {
|
|||||||
{story.cover && (
|
{story.cover && (
|
||||||
<div className="relative aspect-video">
|
<div className="relative aspect-video">
|
||||||
<img
|
<img
|
||||||
src={`${story.cover.image_url}?width=400&quality=80&format=webp`}
|
src={getOptimizedImageUrl(story.cover.image_url, {
|
||||||
|
width: 400,
|
||||||
|
height: 300
|
||||||
|
})}
|
||||||
alt={story.title}
|
alt={story.title}
|
||||||
className="w-full h-48 object-cover"
|
className="w-full h-48 object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Plus, Search, Filter, BookOpen, ArrowUpDown } from 'lucide-react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import type { Story } from '../../types/database';
|
import type { Story } from '../../types/database';
|
||||||
|
import { getOptimizedImageUrl } from '../../lib/imageUtils';
|
||||||
|
|
||||||
type StoryStatus = 'all' | 'draft' | 'published';
|
type StoryStatus = 'all' | 'draft' | 'published';
|
||||||
type SortOption = 'recent' | 'oldest' | 'title' | 'performance';
|
type SortOption = 'recent' | 'oldest' | 'title' | 'performance';
|
||||||
@ -197,10 +198,14 @@ export function StudentStoriesPage() {
|
|||||||
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden cursor-pointer hover:shadow-md transition"
|
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden cursor-pointer hover:shadow-md transition"
|
||||||
onClick={() => navigate(`/aluno/historias/${story.id}`)}
|
onClick={() => navigate(`/aluno/historias/${story.id}`)}
|
||||||
>
|
>
|
||||||
{story.content?.pages?.[0]?.image && (
|
{story.cover && (
|
||||||
<div className="relative aspect-video">
|
<div className="relative aspect-video">
|
||||||
<img
|
<img
|
||||||
src={story.content.pages[0].image}
|
src={getOptimizedImageUrl(story.cover.image_url, {
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
quality: 80
|
||||||
|
})}
|
||||||
alt={story.title}
|
alt={story.title}
|
||||||
className="w-full h-48 object-cover"
|
className="w-full h-48 object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user