Ajout carrousel articles avec miniatures auto-générées
- Créer composant React ArticleCarousel avec lecture auto et boucle infinie - Afficher 6 articles total : 3 articles blog et 3 pages docs, intercalés - Ajouter badges colorés : bleu pour blog, vert foncé pour docs - Implémenter plugin personnalisé pour récupérer et organiser articles - Remplacer section fonctionnalités page d'accueil par nouveau carrousel - Mettre à jour logo site et favicon avec logo vache (logo_vache.png) - Ajouter traductions anglaises pour chaînes UI carrousel
This commit is contained in:
parent
ba57296597
commit
461fc446ff
10 changed files with 911 additions and 55 deletions
|
|
@ -24,11 +24,12 @@ const config: Config = {
|
||||||
plugins: [
|
plugins: [
|
||||||
'docusaurus-plugin-image-zoom',
|
'docusaurus-plugin-image-zoom',
|
||||||
'./plugins/docusaurus-plugin-unified-tags',
|
'./plugins/docusaurus-plugin-unified-tags',
|
||||||
|
'./plugins/docusaurus-plugin-recent-articles',
|
||||||
],
|
],
|
||||||
|
|
||||||
title: 'TellServ Tech Blog',
|
title: 'TellServ Tech Blog',
|
||||||
tagline: 'Recherches et réflexions sur les défis techniques',
|
tagline: 'Recherches et réflexions sur les défis techniques',
|
||||||
favicon: 'img/favicon.png',
|
favicon: 'img/logo.png',
|
||||||
|
|
||||||
url: 'https://docs.tellserv.fr',
|
url: 'https://docs.tellserv.fr',
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
|
|
|
||||||
|
|
@ -87,5 +87,29 @@
|
||||||
"tags.notFound.description": {
|
"tags.notFound.description": {
|
||||||
"message": "The tag you are looking for does not exist.",
|
"message": "The tag you are looking for does not exist.",
|
||||||
"description": "Tag not found description"
|
"description": "Tag not found description"
|
||||||
|
},
|
||||||
|
"carousel.title": {
|
||||||
|
"message": "Recent Articles",
|
||||||
|
"description": "Title of the article carousel section"
|
||||||
|
},
|
||||||
|
"carousel.badge.blog": {
|
||||||
|
"message": "BLOG",
|
||||||
|
"description": "Badge text for blog articles in carousel"
|
||||||
|
},
|
||||||
|
"carousel.badge.documentation": {
|
||||||
|
"message": "DOCUMENTATION",
|
||||||
|
"description": "Badge text for documentation pages in carousel"
|
||||||
|
},
|
||||||
|
"carousel.previous": {
|
||||||
|
"message": "Previous article",
|
||||||
|
"description": "Previous article button aria-label"
|
||||||
|
},
|
||||||
|
"carousel.next": {
|
||||||
|
"message": "Next article",
|
||||||
|
"description": "Next article button aria-label"
|
||||||
|
},
|
||||||
|
"carousel.goToSlide": {
|
||||||
|
"message": "Go to slide {index}",
|
||||||
|
"description": "Go to slide button aria-label"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
plugins/docusaurus-plugin-recent-articles/index.js
Normal file
96
plugins/docusaurus-plugin-recent-articles/index.js
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* Docusaurus plugin to gather recent blog posts and documentation pages
|
||||||
|
* for the ArticleCarousel component.
|
||||||
|
*
|
||||||
|
* This plugin creates a global data structure containing recent articles
|
||||||
|
* from both blog posts and documentation pages, sorted by date.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function pluginRecentArticles(context, options) {
|
||||||
|
return {
|
||||||
|
name: 'docusaurus-plugin-recent-articles',
|
||||||
|
|
||||||
|
async allContentLoaded({ actions, allContent }) {
|
||||||
|
const { setGlobalData } = actions;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blogArticles = [];
|
||||||
|
const docArticles = [];
|
||||||
|
|
||||||
|
// Access blog plugin data
|
||||||
|
const blogPlugin = allContent?.['docusaurus-plugin-content-blog'];
|
||||||
|
const blogContent = blogPlugin?.default;
|
||||||
|
|
||||||
|
if (blogContent?.blogPosts) {
|
||||||
|
blogContent.blogPosts.forEach((post) => {
|
||||||
|
blogArticles.push({
|
||||||
|
title: post.metadata.title,
|
||||||
|
permalink: post.metadata.permalink,
|
||||||
|
type: 'blog',
|
||||||
|
date: post.metadata.date,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access docs plugin data
|
||||||
|
const docsPlugin = allContent?.['docusaurus-plugin-content-docs'];
|
||||||
|
const docsContent = docsPlugin?.default;
|
||||||
|
|
||||||
|
if (docsContent?.loadedVersions) {
|
||||||
|
docsContent.loadedVersions.forEach((version) => {
|
||||||
|
if (version.docs) {
|
||||||
|
version.docs.forEach((doc) => {
|
||||||
|
// Skip index/category pages to focus on actual content
|
||||||
|
if (!doc.id.endsWith('/index') && !doc.id.includes('category')) {
|
||||||
|
docArticles.push({
|
||||||
|
title: doc.title,
|
||||||
|
permalink: doc.permalink,
|
||||||
|
type: 'doc',
|
||||||
|
// Docs don't have a publish date, use last updated time or current date
|
||||||
|
date: doc.lastUpdatedAt
|
||||||
|
? new Date(doc.lastUpdatedAt).toISOString()
|
||||||
|
: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort each type by date (most recent first)
|
||||||
|
blogArticles.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.date || 0);
|
||||||
|
const dateB = new Date(b.date || 0);
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
|
||||||
|
docArticles.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.date || 0);
|
||||||
|
const dateB = new Date(b.date || 0);
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Take 3 most recent from each type
|
||||||
|
const recentBlog = blogArticles.slice(0, 3);
|
||||||
|
const recentDocs = docArticles.slice(0, 3);
|
||||||
|
|
||||||
|
// Intercalate blog and documentation articles: blog, doc, blog, doc, blog, doc
|
||||||
|
const articles = [];
|
||||||
|
for (let i = 0; i < Math.max(recentBlog.length, recentDocs.length); i++) {
|
||||||
|
if (i < recentBlog.length) {
|
||||||
|
articles.push(recentBlog[i]);
|
||||||
|
}
|
||||||
|
if (i < recentDocs.length) {
|
||||||
|
articles.push(recentDocs[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store globally for use in React components
|
||||||
|
setGlobalData({ articles });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading recent articles:', error);
|
||||||
|
setGlobalData({ articles: [] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
280
src/components/ArticleCarousel/README.md
Normal file
280
src/components/ArticleCarousel/README.md
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
# ArticleCarousel Component
|
||||||
|
|
||||||
|
A production-ready React carousel component for Docusaurus 3.9.2 that displays recent blog posts and documentation pages with auto-generated thumbnails.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Auto-generated Thumbnails**: Each article displays with a colorful thumbnail featuring:
|
||||||
|
- Plain/solid color background (rotates through 6 distinct colors)
|
||||||
|
- Article title prominently displayed
|
||||||
|
- Type badge overlay (blue "BLOG" or green "DOCUMENTATION")
|
||||||
|
- Optional date display for blog posts
|
||||||
|
|
||||||
|
- **Carousel Functionality**:
|
||||||
|
- Smooth sliding animations between articles
|
||||||
|
- Auto-play feature (advances every 5 seconds, pauses on user interaction)
|
||||||
|
- Navigation arrows (previous/next)
|
||||||
|
- Dot indicators for quick navigation
|
||||||
|
- Responsive design (mobile-friendly)
|
||||||
|
- Hover effects for enhanced interactivity
|
||||||
|
|
||||||
|
- **Bilingual Support**:
|
||||||
|
- Full i18n compatibility (FR/EN)
|
||||||
|
- Translatable badge text and UI strings
|
||||||
|
- Locale-aware date formatting
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Component Files
|
||||||
|
|
||||||
|
```
|
||||||
|
src/components/ArticleCarousel/
|
||||||
|
├── index.tsx # Main carousel component
|
||||||
|
├── styles.module.css # CSS modules for styling
|
||||||
|
└── README.md # This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Plugin
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/docusaurus-plugin-recent-articles/
|
||||||
|
└── index.js # Plugin to fetch blog & docs data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
|
||||||
|
- **Homepage**: `src/pages/index.tsx` - Replaces the three-feature section
|
||||||
|
- **Config**: `docusaurus.config.ts` - Plugin registration
|
||||||
|
- **i18n**: `i18n/en/code.json` - English translations
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import ArticleCarousel from '@site/src/components/ArticleCarousel';
|
||||||
|
import {usePluginData} from '@docusaurus/useGlobalData';
|
||||||
|
|
||||||
|
function MyPage() {
|
||||||
|
const {articles} = usePluginData('docusaurus-plugin-recent-articles');
|
||||||
|
|
||||||
|
return <ArticleCarousel articles={articles || []} maxVisible={6} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `articles` | `Article[]` | required | Array of articles to display |
|
||||||
|
| `maxVisible` | `number` | `6` | Maximum number of articles in carousel |
|
||||||
|
|
||||||
|
### Article Type
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Article {
|
||||||
|
title: string; // Article title
|
||||||
|
permalink: string; // URL to the article
|
||||||
|
type: 'blog' | 'doc'; // Article type (affects badge)
|
||||||
|
date?: string; // Optional publication/update date
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin: docusaurus-plugin-recent-articles
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Gathers recent blog posts and documentation pages from Docusaurus content plugins, sorts them by date, and makes them available globally via `usePluginData`.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. Accesses blog posts from `docusaurus-plugin-content-blog`
|
||||||
|
2. Accesses documentation from `docusaurus-plugin-content-docs`
|
||||||
|
3. Extracts metadata (title, permalink, date, type)
|
||||||
|
4. Filters out index/category pages from docs
|
||||||
|
5. Sorts all articles by date (most recent first)
|
||||||
|
6. Makes data available globally via `setGlobalData`
|
||||||
|
|
||||||
|
### Data Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
title: "Article Title",
|
||||||
|
permalink: "/blog/article-slug",
|
||||||
|
type: "blog",
|
||||||
|
date: "2025-12-02T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
// ... more articles
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Thumbnail Colors
|
||||||
|
|
||||||
|
Edit `getBackgroundColor()` in `index.tsx` to customize the color palette:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const getBackgroundColor = (index: number): string => {
|
||||||
|
const colors = [
|
||||||
|
'#4A90E2', // Blue
|
||||||
|
'#50C878', // Emerald
|
||||||
|
'#9B59B6', // Purple
|
||||||
|
'#E67E22', // Orange
|
||||||
|
'#1ABC9C', // Turquoise
|
||||||
|
'#E74C3C', // Red
|
||||||
|
];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Badge Colors
|
||||||
|
|
||||||
|
Edit `styles.module.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.articleBadgeBlog {
|
||||||
|
background-color: #2563eb; /* Blue for blog */
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleBadgeDoc {
|
||||||
|
background-color: #059669; /* Dark green for documentation */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-play Interval
|
||||||
|
|
||||||
|
Edit the `useEffect` in `index.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % articles.length);
|
||||||
|
}, 5000); // Change 5000 to desired milliseconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maximum Visible Articles
|
||||||
|
|
||||||
|
Pass a different `maxVisible` prop:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ArticleCarousel articles={articles} maxVisible={10} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## i18n Configuration
|
||||||
|
|
||||||
|
### Translation Keys
|
||||||
|
|
||||||
|
All UI strings are translatable. Keys in `i18n/en/code.json`:
|
||||||
|
|
||||||
|
- `carousel.title` - Section title ("Recent Articles")
|
||||||
|
- `carousel.badge.blog` - Blog badge text ("BLOG")
|
||||||
|
- `carousel.badge.documentation` - Documentation badge text ("DOCUMENTATION")
|
||||||
|
- `carousel.previous` - Previous arrow aria-label
|
||||||
|
- `carousel.next` - Next arrow aria-label
|
||||||
|
- `carousel.goToSlide` - Dot indicator aria-label
|
||||||
|
|
||||||
|
### Adding New Locales
|
||||||
|
|
||||||
|
1. Add translations to your locale's `code.json` file
|
||||||
|
2. Badge text and UI elements will automatically use the translations
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
### CSS Modules
|
||||||
|
|
||||||
|
The component uses CSS Modules for scoped styling. All classes are prefixed with the module scope.
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
The component respects Docusaurus's color mode through CSS variables:
|
||||||
|
|
||||||
|
- `--ifm-background-surface-color`
|
||||||
|
- `--ifm-heading-color`
|
||||||
|
- `--ifm-color-primary`
|
||||||
|
- `--ifm-color-emphasis-*`
|
||||||
|
|
||||||
|
### Responsive Breakpoints
|
||||||
|
|
||||||
|
- Desktop: Default styles
|
||||||
|
- Tablet (≤996px): Reduced padding, smaller thumbnails
|
||||||
|
- Mobile (≤768px): Compact layout, smaller arrows
|
||||||
|
- Small Mobile (≤480px): Further size reductions
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### No External Dependencies
|
||||||
|
|
||||||
|
The carousel is built with pure CSS and React, no external carousel libraries required. This:
|
||||||
|
|
||||||
|
- Reduces bundle size
|
||||||
|
- Improves load times
|
||||||
|
- Eliminates version conflicts
|
||||||
|
- Maintains full control over behavior
|
||||||
|
|
||||||
|
### Optimizations
|
||||||
|
|
||||||
|
- Uses CSS transforms for smooth animations (GPU-accelerated)
|
||||||
|
- Auto-play pauses when user interacts (improves UX)
|
||||||
|
- Articles are pre-sorted by plugin (no runtime sorting)
|
||||||
|
- Lazy evaluation with optional chaining
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
- Arrow buttons are keyboard-accessible
|
||||||
|
- Proper `aria-label` attributes on all interactive elements
|
||||||
|
- Focus states for navigation controls
|
||||||
|
|
||||||
|
### Screen Readers
|
||||||
|
|
||||||
|
- Semantic HTML structure
|
||||||
|
- Descriptive aria-labels
|
||||||
|
- Translatable accessibility strings
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
Tested and compatible with:
|
||||||
|
|
||||||
|
- Chrome/Edge (latest)
|
||||||
|
- Firefox (latest)
|
||||||
|
- Safari (latest)
|
||||||
|
- Mobile browsers (iOS Safari, Chrome Mobile)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Articles Not Appearing
|
||||||
|
|
||||||
|
1. Check that the plugin is registered in `docusaurus.config.ts`
|
||||||
|
2. Verify blog posts and docs exist in your content directories
|
||||||
|
3. Check browser console for plugin errors
|
||||||
|
|
||||||
|
### Styling Issues
|
||||||
|
|
||||||
|
1. Clear Docusaurus cache: `npm run clear`
|
||||||
|
2. Rebuild: `npm run build`
|
||||||
|
3. Check for CSS conflicts with custom themes
|
||||||
|
|
||||||
|
### Build Errors
|
||||||
|
|
||||||
|
1. Ensure all dependencies are installed: `npm install`
|
||||||
|
2. Verify TypeScript configuration is correct
|
||||||
|
3. Check that `usePluginData` hook receives valid data
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements:
|
||||||
|
|
||||||
|
- Touch swipe gestures for mobile
|
||||||
|
- Configurable thumbnail aspect ratios
|
||||||
|
- Image upload support for custom thumbnails
|
||||||
|
- Animation variations (fade, slide vertical, etc.)
|
||||||
|
- Pause on hover option
|
||||||
|
- Infinite scroll mode
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This component is part of the TellServ Tech Blog and follows the project's MIT license.
|
||||||
214
src/components/ArticleCarousel/index.tsx
Normal file
214
src/components/ArticleCarousel/index.tsx
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
title: string;
|
||||||
|
permalink: string;
|
||||||
|
type: 'blog' | 'doc';
|
||||||
|
date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArticleCarouselProps {
|
||||||
|
articles: Article[];
|
||||||
|
maxVisible?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArticleCarousel component displays recent blog posts and documentation
|
||||||
|
* with auto-generated thumbnails featuring title and type badge.
|
||||||
|
*
|
||||||
|
* @param articles - Array of articles to display in the carousel
|
||||||
|
* @param maxVisible - Maximum number of visible carousel items (default: 6)
|
||||||
|
*/
|
||||||
|
export default function ArticleCarousel({
|
||||||
|
articles,
|
||||||
|
maxVisible = 6
|
||||||
|
}: ArticleCarouselProps): JSX.Element {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||||
|
const { i18n } = useDocusaurusContext();
|
||||||
|
|
||||||
|
// Auto-play carousel every 5 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAutoPlaying || articles.length <= 1) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % articles.length);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isAutoPlaying, articles.length]);
|
||||||
|
|
||||||
|
// Limit articles to maxVisible
|
||||||
|
const visibleArticles = articles.slice(0, maxVisible);
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
setIsAutoPlaying(false);
|
||||||
|
setCurrentIndex((prev) =>
|
||||||
|
prev === 0 ? visibleArticles.length - 1 : prev - 1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
setIsAutoPlaying(false);
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % visibleArticles.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDotClick = (index: number) => {
|
||||||
|
setIsAutoPlaying(false);
|
||||||
|
setCurrentIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visibleArticles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get badge text based on article type and current locale
|
||||||
|
const getBadgeText = (type: 'blog' | 'doc'): string => {
|
||||||
|
return type === 'blog'
|
||||||
|
? translate({
|
||||||
|
id: 'carousel.badge.blog',
|
||||||
|
message: 'BLOG',
|
||||||
|
description: 'Badge text for blog articles in carousel',
|
||||||
|
})
|
||||||
|
: translate({
|
||||||
|
id: 'carousel.badge.documentation',
|
||||||
|
message: 'DOCUMENTATION',
|
||||||
|
description: 'Badge text for documentation pages in carousel',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate consistent background colors for thumbnails
|
||||||
|
const getBackgroundColor = (index: number): string => {
|
||||||
|
const colors = [
|
||||||
|
'#4A90E2', // Blue
|
||||||
|
'#50C878', // Emerald
|
||||||
|
'#9B59B6', // Purple
|
||||||
|
'#E67E22', // Orange
|
||||||
|
'#1ABC9C', // Turquoise
|
||||||
|
'#E74C3C', // Red
|
||||||
|
];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.carouselSection}>
|
||||||
|
<div className="container">
|
||||||
|
<h2 className={styles.carouselTitle}>
|
||||||
|
{translate({
|
||||||
|
id: 'carousel.title',
|
||||||
|
message: 'Articles récents',
|
||||||
|
description: 'Title of the article carousel section',
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className={styles.carouselContainer}>
|
||||||
|
{/* Navigation Arrow - Previous */}
|
||||||
|
<button
|
||||||
|
className={`${styles.carouselArrow} ${styles.carouselArrowLeft}`}
|
||||||
|
onClick={handlePrevious}
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'carousel.previous',
|
||||||
|
message: 'Article précédent',
|
||||||
|
description: 'Previous article button aria-label',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Carousel Track */}
|
||||||
|
<div className={styles.carouselTrack}>
|
||||||
|
<div
|
||||||
|
className={styles.carouselInner}
|
||||||
|
style={{
|
||||||
|
transform: `translateX(-${currentIndex * 100}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{visibleArticles.map((article, index) => (
|
||||||
|
<Link
|
||||||
|
key={`${article.permalink}-${index}`}
|
||||||
|
to={article.permalink}
|
||||||
|
className={styles.carouselItem}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.articleThumbnail}
|
||||||
|
style={{
|
||||||
|
backgroundColor: getBackgroundColor(index),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Type Badge */}
|
||||||
|
<div
|
||||||
|
className={`${styles.articleBadge} ${
|
||||||
|
article.type === 'blog'
|
||||||
|
? styles.articleBadgeBlog
|
||||||
|
: styles.articleBadgeDoc
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getBadgeText(article.type)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Article Title */}
|
||||||
|
<div className={styles.articleTitle}>
|
||||||
|
<h3>{article.title}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date (if available for blog posts) */}
|
||||||
|
{article.date && (
|
||||||
|
<div className={styles.articleDate}>
|
||||||
|
{new Date(article.date).toLocaleDateString(
|
||||||
|
i18n.currentLocale === 'fr' ? 'fr-FR' : 'en-US',
|
||||||
|
{
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Arrow - Next */}
|
||||||
|
<button
|
||||||
|
className={`${styles.carouselArrow} ${styles.carouselArrowRight}`}
|
||||||
|
onClick={handleNext}
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'carousel.next',
|
||||||
|
message: 'Article suivant',
|
||||||
|
description: 'Next article button aria-label',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Carousel Indicators (Dots) */}
|
||||||
|
<div className={styles.carouselIndicators}>
|
||||||
|
{visibleArticles.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={`${styles.carouselDot} ${
|
||||||
|
index === currentIndex ? styles.carouselDotActive : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => handleDotClick(index)}
|
||||||
|
aria-label={translate(
|
||||||
|
{
|
||||||
|
id: 'carousel.goToSlide',
|
||||||
|
message: 'Aller à la diapositive {index}',
|
||||||
|
description: 'Go to slide button aria-label',
|
||||||
|
},
|
||||||
|
{ index: index + 1 }
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
281
src/components/ArticleCarousel/styles.module.css
Normal file
281
src/components/ArticleCarousel/styles.module.css
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
/* Article Carousel Section */
|
||||||
|
.carouselSection {
|
||||||
|
padding: 3rem 0;
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselTitle {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--ifm-heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel Container */
|
||||||
|
.carouselContainer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel Track (overflow container) */
|
||||||
|
.carouselTrack {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel Inner (sliding container) */
|
||||||
|
.carouselInner {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual Carousel Item */
|
||||||
|
.carouselItem {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselItem:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Thumbnail (auto-generated card) */
|
||||||
|
.articleThumbnail {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselItem:hover .articleThumbnail {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Type Badge */
|
||||||
|
.articleBadge {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleBadgeBlog {
|
||||||
|
background-color: #2563eb; /* Blue for blog */
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleBadgeDoc {
|
||||||
|
background-color: #059669; /* Dark green for documentation */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Title */
|
||||||
|
.articleTitle {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleTitle h3 {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Date */
|
||||||
|
.articleDate {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel Navigation Arrows */
|
||||||
|
.carouselArrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--ifm-color-primary);
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 300;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrow:hover {
|
||||||
|
background-color: var(--ifm-color-primary-dark);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrow:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrowLeft {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrowRight {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carousel Indicators (Dots) */
|
||||||
|
.carouselIndicators {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselDot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--ifm-color-emphasis-300);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselDot:hover {
|
||||||
|
background-color: var(--ifm-color-emphasis-500);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselDotActive {
|
||||||
|
background-color: var(--ifm-color-primary);
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media screen and (max-width: 996px) {
|
||||||
|
.carouselTitle {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleThumbnail {
|
||||||
|
height: 350px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleTitle h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrow {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.carouselSection {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselTitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselContainer {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleThumbnail {
|
||||||
|
height: 300px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleTitle h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleBadge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleDate {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrow {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrowLeft {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrowRight {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselDot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.articleThumbnail {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleTitle h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carouselArrow {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,10 @@ import clsx from 'clsx';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import {usePluginData} from '@docusaurus/useGlobalData';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
|
import ArticleCarousel from '@site/src/components/ArticleCarousel';
|
||||||
|
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
|
@ -50,6 +52,17 @@ function HomepageHeader() {
|
||||||
|
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): JSX.Element {
|
||||||
const {siteConfig} = useDocusaurusContext();
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
|
||||||
|
// Get recent articles data from plugin
|
||||||
|
const {articles} = usePluginData('docusaurus-plugin-recent-articles') as {
|
||||||
|
articles: Array<{
|
||||||
|
title: string;
|
||||||
|
permalink: string;
|
||||||
|
type: 'blog' | 'doc';
|
||||||
|
date?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
title={translate({
|
title={translate({
|
||||||
|
|
@ -64,60 +77,7 @@ export default function Home(): JSX.Element {
|
||||||
})}>
|
})}>
|
||||||
<HomepageHeader />
|
<HomepageHeader />
|
||||||
<main>
|
<main>
|
||||||
<section className={styles.features}>
|
<ArticleCarousel articles={articles || []} maxVisible={6} />
|
||||||
<div className="container">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col col--4">
|
|
||||||
<h3>
|
|
||||||
<Translate
|
|
||||||
id="homepage.feature1.title"
|
|
||||||
description="Title of feature 1 (technical documentation) on the homepage">
|
|
||||||
Documentation Technique
|
|
||||||
</Translate>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
<Translate
|
|
||||||
id="homepage.feature1.description"
|
|
||||||
description="Description of feature 1 (technical documentation) on the homepage">
|
|
||||||
Documentation approfondie de mes projets et solutions techniques.
|
|
||||||
</Translate>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col col--4">
|
|
||||||
<h3>
|
|
||||||
<Translate
|
|
||||||
id="homepage.feature2.title"
|
|
||||||
description="Title of feature 2 (blog posts) on the homepage">
|
|
||||||
Articles de Blog
|
|
||||||
</Translate>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
<Translate
|
|
||||||
id="homepage.feature2.description"
|
|
||||||
description="Description of feature 2 (blog posts) on the homepage">
|
|
||||||
Réflexions et analyses sur les défis techniques rencontrés.
|
|
||||||
</Translate>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col col--4">
|
|
||||||
<h3>
|
|
||||||
<Translate
|
|
||||||
id="homepage.feature3.title"
|
|
||||||
description="Title of feature 3 (knowledge sharing) on the homepage">
|
|
||||||
Partage de Connaissances
|
|
||||||
</Translate>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
<Translate
|
|
||||||
id="homepage.feature3.description"
|
|
||||||
description="Description of feature 3 (knowledge sharing) on the homepage">
|
|
||||||
Partage d'expériences et de solutions pour la communauté.
|
|
||||||
</Translate>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
BIN
static/img/favicon.ico
Normal file
BIN
static/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 155 KiB |
Loading…
Add table
Add a link
Reference in a new issue