Skip to content

Commit 17de3cf

Browse files
committed
Added carousel to showcase TeXlyre ecosystem
1 parent 5bcbc42 commit 17de3cf

File tree

11 files changed

+455
-0
lines changed

11 files changed

+455
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import type { ReactNode } from 'react';
2+
import { useState, useEffect, useCallback } from 'react';
3+
import clsx from 'clsx';
4+
import Heading from '@theme/Heading';
5+
import styles from './styles.module.css';
6+
7+
type Project = {
8+
title: string;
9+
description: string;
10+
image: string;
11+
link: string;
12+
};
13+
14+
const projects: Project[] = [
15+
{
16+
title: 'CodeMirror LaTeX Language',
17+
description: 'Syntax highlighting and language support for LaTeX in CodeMirror 6',
18+
image: '/img/codemirror-lang-latex.png',
19+
link: 'https://texlyre.github.io/codemirror-lang-latex/',
20+
},
21+
{
22+
title: 'CodeMirror BibTeX Language',
23+
description: 'Syntax highlighting and language support for BibTeX in CodeMirror 6',
24+
image: '/img/codemirror-lang-bib.png',
25+
link: 'https://texlyre.github.io/codemirror-lang-bib/',
26+
},
27+
{
28+
title: 'WASM LaTeX Tools',
29+
description: 'WebAssembly-powered LaTeX utilities for browser-based compilation',
30+
image: '/img/wasm-latex-tools.png',
31+
link: 'https://texlyre.github.io/wasm-latex-tools/',
32+
},
33+
{
34+
title: 'CodeMirror LaTeX Visual',
35+
description: 'Visual editing enhancements for LaTeX in CodeMirror',
36+
image: '/img/codemirror-latex-visual.png',
37+
link: 'https://texlyre.github.io/codemirror-latex-visual/',
38+
},
39+
{
40+
title: 'Vector PDF Converter',
41+
description: 'Convert between vector PDF formats in the browser',
42+
image: '/img/vector-pdf-converter.png',
43+
link: 'https://texlyre.github.io/vector-pdf-converter/',
44+
},
45+
{
46+
title: 'FilePizza Client',
47+
description: 'Peer-to-peer file transfer directly in your browser',
48+
image: '/img/filepizza-client.png',
49+
link: 'https://texlyre.github.io/filepizza-client/',
50+
},
51+
{
52+
title: 'TeXlyre Templates',
53+
description: 'Collection of LaTeX and Typst templates for various use cases',
54+
image: '/img/texlyre-templates.png',
55+
link: 'https://texlyre.github.io/texlyre-templates/',
56+
},
57+
];
58+
59+
export default function ProjectCarousel(): ReactNode {
60+
const [currentIndex, setCurrentIndex] = useState(0);
61+
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
62+
63+
const goToSlide = useCallback((index: number) => {
64+
setCurrentIndex(index);
65+
}, []);
66+
67+
const goToPrevious = useCallback(() => {
68+
setCurrentIndex((prev) => (prev === 0 ? projects.length - 1 : prev - 1));
69+
}, []);
70+
71+
const goToNext = useCallback(() => {
72+
setCurrentIndex((prev) => (prev === projects.length - 1 ? 0 : prev + 1));
73+
}, []);
74+
75+
useEffect(() => {
76+
if (!isAutoPlaying) return;
77+
78+
const interval = setInterval(() => {
79+
goToNext();
80+
}, 5000);
81+
82+
return () => clearInterval(interval);
83+
}, [isAutoPlaying, goToNext]);
84+
85+
const handleMouseEnter = useCallback(() => {
86+
setIsAutoPlaying(false);
87+
}, []);
88+
89+
const handleMouseLeave = useCallback(() => {
90+
setIsAutoPlaying(true);
91+
}, []);
92+
93+
return (
94+
<section className={styles.carousel}>
95+
<div className="container">
96+
<Heading as="h2" className={styles.title}>TeXlyre Ecosystem</Heading>
97+
<p className={styles.subtitle}>
98+
Explore the open-source projects that power TeXlyre
99+
</p>
100+
101+
<div
102+
className={styles.carouselContainer}
103+
onMouseEnter={handleMouseEnter}
104+
onMouseLeave={handleMouseLeave}
105+
>
106+
<button
107+
className={clsx(styles.navButton, styles.navButtonPrev)}
108+
onClick={goToPrevious}
109+
aria-label="Previous project"
110+
>
111+
112+
</button>
113+
114+
<div className={styles.carouselContent}>
115+
{projects.map((project, index) => (
116+
<div
117+
key={index}
118+
className={clsx(styles.slide, {
119+
[styles.slideActive]: index === currentIndex,
120+
})}
121+
>
122+
<a
123+
href={project.link}
124+
target="_blank"
125+
rel="noopener noreferrer"
126+
className={styles.slideLink}
127+
>
128+
<img
129+
src={project.image}
130+
alt={project.title}
131+
className={styles.slideImage}
132+
/>
133+
<div className={styles.slideInfo}>
134+
<h3 className={styles.slideTitle}>{project.title}</h3>
135+
<p className={styles.slideDescription}>{project.description}</p>
136+
</div>
137+
</a>
138+
</div>
139+
))}
140+
</div>
141+
142+
<button
143+
className={clsx(styles.navButton, styles.navButtonNext)}
144+
onClick={goToNext}
145+
aria-label="Next project"
146+
>
147+
148+
</button>
149+
150+
<div className={styles.indicators}>
151+
{projects.map((_, index) => (
152+
<button
153+
key={index}
154+
className={clsx(styles.indicator, {
155+
[styles.indicatorActive]: index === currentIndex,
156+
})}
157+
onClick={() => goToSlide(index)}
158+
aria-label={`Go to project ${index + 1}`}
159+
/>
160+
))}
161+
</div>
162+
</div>
163+
</div>
164+
</section>
165+
);
166+
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
.carousel {
2+
padding: 4rem 0;
3+
background: linear-gradient(180deg, #ffffff 0%, #f7fafc 100%);
4+
}
5+
6+
[data-theme='dark'] .carousel {
7+
background: linear-gradient(180deg, #1a202c 0%, #2d3748 100%);
8+
}
9+
10+
.title {
11+
text-align: center;
12+
font-size: 2.5rem;
13+
margin-bottom: 1rem;
14+
color: var(--ifm-heading-color);
15+
}
16+
17+
.subtitle {
18+
text-align: center;
19+
font-size: 1.2rem;
20+
color: var(--ifm-color-content);
21+
margin-bottom: 3rem;
22+
}
23+
24+
.carouselContainer {
25+
position: relative;
26+
max-width: 1100px;
27+
margin: 0 auto;
28+
}
29+
30+
.carouselContent {
31+
position: relative;
32+
width: 100%;
33+
aspect-ratio: 1320 / 1080;
34+
overflow: hidden;
35+
border-radius: 12px;
36+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
37+
}
38+
39+
[data-theme='dark'] .carouselContent {
40+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
41+
}
42+
43+
.slide {
44+
position: absolute;
45+
top: 0;
46+
left: 0;
47+
width: 100%;
48+
height: 100%;
49+
opacity: 0;
50+
transform: translateX(100%);
51+
transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
52+
}
53+
54+
.slideActive {
55+
opacity: 1;
56+
transform: translateX(0);
57+
}
58+
59+
.slideLink {
60+
display: block;
61+
width: 100%;
62+
height: 100%;
63+
position: relative;
64+
text-decoration: none;
65+
}
66+
67+
.slideImage {
68+
width: 100%;
69+
height: 100%;
70+
object-fit: cover;
71+
display: block;
72+
}
73+
74+
.slideInfo {
75+
position: absolute;
76+
bottom: 0;
77+
left: 0;
78+
right: 0;
79+
background: linear-gradient(to top, rgba(0, 0, 0, 0.9), transparent);
80+
color: white;
81+
padding: 3rem 2rem 2rem;
82+
transform: translateY(100%);
83+
transition: transform 0.3s ease-in-out;
84+
}
85+
86+
.slideLink:hover .slideInfo {
87+
transform: translateY(0);
88+
}
89+
90+
.slideTitle {
91+
font-size: 1.5rem;
92+
font-weight: bold;
93+
margin-bottom: 0.5rem;
94+
color: white;
95+
}
96+
97+
.slideDescription {
98+
font-size: 1rem;
99+
color: rgba(255, 255, 255, 0.9);
100+
margin: 0;
101+
}
102+
103+
.navButton {
104+
position: absolute;
105+
top: 50%;
106+
transform: translateY(-50%);
107+
background: rgba(255, 255, 255, 0.9);
108+
border: none;
109+
width: 50px;
110+
height: 50px;
111+
border-radius: 50%;
112+
font-size: 2rem;
113+
cursor: pointer;
114+
z-index: 10;
115+
display: flex;
116+
align-items: center;
117+
justify-content: center;
118+
color: var(--ifm-color-primary);
119+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
120+
transition: all 0.2s ease;
121+
}
122+
123+
[data-theme='dark'] .navButton {
124+
background: rgba(45, 55, 72, 0.9);
125+
color: var(--ifm-color-primary-light);
126+
}
127+
128+
.navButton:hover {
129+
background: var(--ifm-color-primary);
130+
color: white;
131+
transform: translateY(-50%) scale(1.1);
132+
}
133+
134+
.navButtonPrev {
135+
left: -25px;
136+
}
137+
138+
.navButtonNext {
139+
right: -25px;
140+
}
141+
142+
.indicators {
143+
display: flex;
144+
justify-content: center;
145+
gap: 0.75rem;
146+
margin-top: 2rem;
147+
}
148+
149+
.indicator {
150+
width: 12px;
151+
height: 12px;
152+
border-radius: 50%;
153+
border: 2px solid var(--ifm-color-primary);
154+
background: transparent;
155+
cursor: pointer;
156+
transition: all 0.3s ease;
157+
padding: 0;
158+
}
159+
160+
.indicator:hover {
161+
transform: scale(1.2);
162+
}
163+
164+
.indicatorActive {
165+
background: var(--ifm-color-primary);
166+
}
167+
168+
@media (max-width: 996px) {
169+
.title {
170+
font-size: 2rem;
171+
}
172+
173+
.subtitle {
174+
font-size: 1rem;
175+
}
176+
177+
.navButtonPrev {
178+
left: 10px;
179+
}
180+
181+
.navButtonNext {
182+
right: 10px;
183+
}
184+
185+
.slideInfo {
186+
padding: 2rem 1rem 1rem;
187+
}
188+
189+
.slideTitle {
190+
font-size: 1.2rem;
191+
}
192+
193+
.slideDescription {
194+
font-size: 0.9rem;
195+
}
196+
}
197+
198+
@media (max-width: 768px) {
199+
.navButton {
200+
width: 40px;
201+
height: 40px;
202+
font-size: 1.5rem;
203+
}
204+
205+
.slideInfo {
206+
position: static;
207+
transform: none;
208+
background: rgba(0, 0, 0, 0.85);
209+
padding: 1.5rem 1rem;
210+
}
211+
}
212+
213+
@media (prefers-reduced-motion: reduce) {
214+
.slide {
215+
transition: none;
216+
}
217+
218+
.slideInfo {
219+
transition: none;
220+
}
221+
222+
.navButton {
223+
transition: none;
224+
}
225+
}

0 commit comments

Comments
 (0)