feat: frontend shadcn

This commit is contained in:
2026-01-29 13:38:29 -03:00
parent 7627a00303
commit 5125bbf4d4
54 changed files with 6990 additions and 329 deletions

31
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment
.env
.env.local
# Logs
*.log
npm-debug.log*
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Uploads (user content)
uploads/*
!uploads/.gitkeep
# Database
*.sqlite
*.db

View File

@@ -6,10 +6,7 @@ import { YouTubeService } from './youtube.service';
import { YouTubeCache } from '../../entities/youtube-cache.entity';
@Module({
imports: [
TypeOrmModule.forFeature([YouTubeCache]),
HttpModule,
],
imports: [TypeOrmModule.forFeature([YouTubeCache]), HttpModule],
controllers: [YouTubeController],
providers: [YouTubeService],
})

View File

@@ -55,7 +55,10 @@ export class YouTubeService {
this.apiKey = this.configService.get<string>('YOUTUBE_API_KEY', '');
}
async search(query: string, maxResults = 10): Promise<YouTubeVideoResponse[]> {
async search(
query: string,
maxResults = 10,
): Promise<YouTubeVideoResponse[]> {
// Check cache first
const cached = await this.getFromCache(query);
if (cached) {
@@ -76,7 +79,9 @@ export class YouTubeService {
return results;
}
private async getFromCache(query: string): Promise<YouTubeVideoResponse[] | null> {
private async getFromCache(
query: string,
): Promise<YouTubeVideoResponse[] | null> {
const cached = await this.cacheRepository.findOne({
where: {
searchQuery: query.toLowerCase(),
@@ -87,7 +92,10 @@ export class YouTubeService {
return cached ? cached.results : null;
}
private async saveToCache(query: string, results: YouTubeVideoResponse[]): Promise<void> {
private async saveToCache(
query: string,
results: YouTubeVideoResponse[],
): Promise<void> {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + this.cacheHours);
@@ -100,7 +108,10 @@ export class YouTubeService {
await this.cacheRepository.save(cache);
}
private async fetchFromYouTube(query: string, maxResults: number): Promise<YouTubeVideoResponse[]> {
private async fetchFromYouTube(
query: string,
maxResults: number,
): Promise<YouTubeVideoResponse[]> {
const searchUrl = 'https://www.googleapis.com/youtube/v3/search';
const { data } = await firstValueFrom(
@@ -115,7 +126,9 @@ export class YouTubeService {
}),
);
const videoIds = data.items.map((item: YouTubeSearchItem) => item.id.videoId).join(',');
const videoIds = data.items
.map((item: YouTubeSearchItem) => item.id.videoId)
.join(',');
// Get view counts
const statsUrl = 'https://www.googleapis.com/youtube/v3/videos';
@@ -138,20 +151,26 @@ export class YouTubeService {
videoId: item.id.videoId,
title: item.snippet.title,
channelTitle: item.snippet.channelTitle,
thumbnailUrl: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.medium.url,
thumbnailUrl:
item.snippet.thumbnails.high?.url || item.snippet.thumbnails.medium.url,
publishedAt: item.snippet.publishedAt,
viewCount: viewCounts.get(item.id.videoId),
}));
}
private getMockResults(query: string, maxResults: number): YouTubeVideoResponse[] {
private getMockResults(
query: string,
maxResults: number,
): YouTubeVideoResponse[] {
const mockVideos: YouTubeVideoResponse[] = [
{
videoId: 'mock1',
title: `${query} - Complete Tutorial for Beginners`,
channelTitle: 'Tech Academy',
thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg',
publishedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
publishedAt: new Date(
Date.now() - 7 * 24 * 60 * 60 * 1000,
).toISOString(),
viewCount: '1250000',
},
{
@@ -159,7 +178,9 @@ export class YouTubeService {
title: `Learn ${query} in 30 Minutes`,
channelTitle: 'Code Master',
thumbnailUrl: 'https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg',
publishedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
publishedAt: new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000,
).toISOString(),
viewCount: '890000',
},
{
@@ -167,7 +188,9 @@ export class YouTubeService {
title: `${query} Crash Course 2025`,
channelTitle: 'Dev Tutorial',
thumbnailUrl: 'https://i.ytimg.com/vi/kJQP7kiw5Fk/hqdefault.jpg',
publishedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
publishedAt: new Date(
Date.now() - 14 * 24 * 60 * 60 * 1000,
).toISOString(),
viewCount: '2100000',
},
{
@@ -175,7 +198,9 @@ export class YouTubeService {
title: `Why ${query} is Amazing`,
channelTitle: 'Tech Reviews',
thumbnailUrl: 'https://i.ytimg.com/vi/RgKAFK5djSk/hqdefault.jpg',
publishedAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(),
publishedAt: new Date(
Date.now() - 60 * 24 * 60 * 60 * 1000,
).toISOString(),
viewCount: '450000',
},
{
@@ -183,7 +208,9 @@ export class YouTubeService {
title: `${query} Tips and Tricks`,
channelTitle: 'Pro Tips',
thumbnailUrl: 'https://i.ytimg.com/vi/fJ9rUzIMcZQ/hqdefault.jpg',
publishedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
publishedAt: new Date(
Date.now() - 3 * 24 * 60 * 60 * 1000,
).toISOString(),
viewCount: '320000',
},
{
@@ -191,7 +218,9 @@ export class YouTubeService {
title: `${query} for Professionals`,
channelTitle: 'Advanced Learning',
thumbnailUrl: 'https://i.ytimg.com/vi/09R8_2nJtjg/hqdefault.jpg',
publishedAt: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000).toISOString(),
publishedAt: new Date(
Date.now() - 45 * 24 * 60 * 60 * 1000,
).toISOString(),
viewCount: '780000',
},
];