diff --git a/src/components/certificate/Certificate.tsx b/src/components/certificate/Certificate.tsx index 45bdac2..3b4d0c6 100644 --- a/src/components/certificate/Certificate.tsx +++ b/src/components/certificate/Certificate.tsx @@ -82,19 +82,40 @@ const Certificate: React.FC = () => { const maxScroll = totalWidth - (typeof window !== "undefined" ? window.innerWidth * 0.7 : 800); const handleNext = useCallback(() => { + if (isMobile && scrollRef.current) { + const el = scrollRef.current; + const step = cardWidth + GAP; + const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 10; + if (atEnd) { + el.scrollTo({ left: 0, behavior: "smooth" }); + } else { + el.scrollBy({ left: step, behavior: "smooth" }); + } + return; + } setScrollX((prev) => { const next = prev + cardWidth + GAP; if (next > maxScroll) return 0; return next; }); - }, [maxScroll, cardWidth]); + }, [isMobile, maxScroll, cardWidth]); const handlePrev = useCallback(() => { + if (isMobile && scrollRef.current) { + const el = scrollRef.current; + const step = cardWidth + GAP; + if (el.scrollLeft <= 10) { + el.scrollTo({ left: el.scrollWidth, behavior: "smooth" }); + } else { + el.scrollBy({ left: -step, behavior: "smooth" }); + } + return; + } setScrollX((prev) => { if (prev <= 0) return maxScroll > 0 ? maxScroll : 0; return prev - cardWidth - GAP; }); - }, [maxScroll, cardWidth]); + }, [isMobile, maxScroll, cardWidth]); const openModal = useCallback((cert: CertificateItem) => { setSelectedCertificate(cert); @@ -107,10 +128,10 @@ const Certificate: React.FC = () => { }, []); useEffect(() => { - if (modalOpen) return; + if (modalOpen || isMobile) return; const interval = setInterval(handleNext, 4000); return () => clearInterval(interval); - }, [modalOpen, handleNext]); + }, [modalOpen, handleNext, isMobile]); // Build SVG connector paths const connectorPaths = certificates.slice(0, -1).map((_, i) => { @@ -160,8 +181,11 @@ const Certificate: React.FC = () => { {/* Navigation */} { { - {/* Scrollable zigzag */} + {/* Scrollable container — translateX on desktop, native horizontal scroll on mobile */} {/* SVG connectors overlay */} @@ -242,8 +281,8 @@ const Certificate: React.FC = () => { left: { xs: "auto", md: left }, top: { xs: "auto", md: isDown ? OFFSET_Y : 0 }, width: cardWidth, - mb: { xs: 3, md: 0 }, - display: { xs: index < 4 ? "block" : "none", md: "block" }, + flexShrink: 0, + scrollSnapAlign: { xs: "center", md: "none" }, zIndex: 2 }} > @@ -340,8 +379,9 @@ const Certificate: React.FC = () => { - {/* Progress bar */} + {/* Progress bar - desktop only */} diff --git a/src/components/contact/Contact.tsx b/src/components/contact/Contact.tsx index de54888..55f3461 100644 --- a/src/components/contact/Contact.tsx +++ b/src/components/contact/Contact.tsx @@ -150,7 +150,7 @@ const Contact: React.FC = () => { { backdropFilter: "blur(20px)", width: "100%" }} - styles={{ body: { padding: 32 } }} + className="contact-form-card" > <Title level={3} style={{ marginBottom: 24, color: "#e6edf3" }}> Send Me a Message diff --git a/src/components/contact/contact.css b/src/components/contact/contact.css index 0d42198..0b7983b 100644 --- a/src/components/contact/contact.css +++ b/src/components/contact/contact.css @@ -560,3 +560,13 @@ font-size: var(--fs-xs); } } + + +.contact-form-card .ant-card-body { + padding: 32px; +} +@media (max-width: 600px) { + .contact-form-card .ant-card-body { + padding: 16px; + } +} diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index fc869fc..7e89f87 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -47,7 +47,7 @@ const Footer: React.FC = () => { window.scrollTo({ top: 0, behavior: "smooth" }); }; - const currentYear: number = new Date().getFullYear(); + const currentYear: number = 2025; const quickLinks: QuickLink[] = [ { name: "About", href: "#about" }, @@ -140,7 +140,7 @@ const Footer: React.FC = () => { {/* Main Footer Content */} <Grid container spacing={4} sx={{ mb: 6 }}> {/* Brand Section */} - <Grid size={{ xs: 12, md: 4 }}> + <Grid size={{ xs: 12, md: 4 }} sx={{ textAlign: { xs: "center", md: "left" } }}> <Typography variant="h4" sx={{ @@ -174,7 +174,7 @@ const Footer: React.FC = () => { </Typography> {/* Social Links */} - <Box sx={{ display: "flex", gap: 1 }}> + <Box sx={{ display: "flex", gap: 1, justifyContent: { xs: "center", md: "flex-start" } }}> {socialLinks.map((social: SocialLink) => ( <IconButton key={social.name} @@ -200,7 +200,7 @@ const Footer: React.FC = () => { </Grid> {/* Quick Links */} - <Grid size={{ xs: 12, sm: 6, md: 4 }}> + <Grid size={{ xs: 12, sm: 6, md: 4 }} sx={{ textAlign: { xs: "center", sm: "left" } }}> <Typography variant="h6" sx={{ @@ -208,11 +208,13 @@ const Footer: React.FC = () => { mb: 3, color: "white", position: "relative", + display: "inline-block", "&::after": { content: '""', position: "absolute", bottom: -8, - left: 0, + left: { xs: "50%", sm: 0 }, + transform: { xs: "translateX(-50%)", sm: "none" }, width: 50, height: 3, background: "#0eaddf" @@ -221,7 +223,7 @@ const Footer: React.FC = () => { > Quick Links </Typography> - <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}> + <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5, alignItems: { xs: "center", sm: "flex-start" } }}> {quickLinks.map((link: QuickLink) => ( <Link key={link.name} @@ -258,7 +260,7 @@ const Footer: React.FC = () => { </Grid> {/* Contact Info */} - <Grid size={{ xs: 12, sm: 6, md: 4 }}> + <Grid size={{ xs: 12, sm: 6, md: 4 }} sx={{ textAlign: { xs: "center", sm: "left" } }}> <Typography variant="h6" sx={{ @@ -266,11 +268,13 @@ const Footer: React.FC = () => { mb: 3, color: "white", position: "relative", + display: "inline-block", "&::after": { content: '""', position: "absolute", bottom: -8, - left: 0, + left: { xs: "50%", sm: 0 }, + transform: { xs: "translateX(-50%)", sm: "none" }, width: 50, height: 3, background: "#0eaddf" @@ -279,7 +283,7 @@ const Footer: React.FC = () => { > Contact Info </Typography> - <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> + <Box sx={{ display: "flex", flexDirection: "column", gap: 2, alignItems: { xs: "center", sm: "flex-start" } }}> {contactInfo.map((info: ContactInfo, index: number) => ( <Box key={index} diff --git a/src/components/github/GitHubActivity.tsx b/src/components/github/GitHubActivity.tsx index 9fda7cd..fb5f30c 100644 --- a/src/components/github/GitHubActivity.tsx +++ b/src/components/github/GitHubActivity.tsx @@ -1,8 +1,11 @@ import React from "react"; -import { Box, Container, Typography, Paper } from "@mui/material"; +import { Box, Container, Typography, Paper, useTheme, useMediaQuery } from "@mui/material"; import { GitHubCalendar } from "react-github-calendar"; const GitHubActivity: React.FC = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + return ( <Box component="section" @@ -36,37 +39,55 @@ const GitHubActivity: React.FC = () => { </Typography> </Box> - <Paper - elevation={0} - sx={{ - p: { xs: 2, md: 4 }, - borderRadius: 3, - background: "#0d1117", - border: "1px solid rgba(14, 173, 223, 0.15)", - overflow: "auto", - maxWidth: 900, - mx: "auto", - "&::-webkit-scrollbar": { - height: 6 - }, - "&::-webkit-scrollbar-thumb": { + <Box sx={{ position: "relative", maxWidth: 900, mx: "auto" }}> + <Paper + elevation={0} + sx={{ + p: { xs: 2, md: 4 }, borderRadius: 3, - background: "rgba(255,255,255,0.2)" - } - }} - > - <GitHubCalendar - username="anhvuFE" - colorScheme="dark" - fontSize={14} - blockSize={14} - blockMargin={4} - style={{ color: "#e6edf3", width: "100%" }} - labels={{ - totalCount: "{{count}} contributions in the last year" + background: "#0d1117", + border: "1px solid rgba(14, 173, 223, 0.15)", + overflowX: "auto", + overflowY: "hidden", + WebkitOverflowScrolling: "touch", + "&::-webkit-scrollbar": { + height: 6 + }, + "&::-webkit-scrollbar-thumb": { + borderRadius: 3, + background: "rgba(255,255,255,0.2)" + } + }} + > + <GitHubCalendar + username="anhvuFE" + colorScheme="dark" + fontSize={isMobile ? 11 : 14} + blockSize={isMobile ? 10 : 14} + blockMargin={isMobile ? 3 : 4} + style={{ color: "#e6edf3" }} + labels={{ + totalCount: "{{count}} contributions in the last year" + }} + /> + </Paper> + {/* Right-edge fade hint for horizontal scroll on mobile */} + <Box + aria-hidden + sx={{ + display: { xs: "block", md: "none" }, + position: "absolute", + top: 1, + right: 1, + bottom: 8, + width: 24, + borderTopRightRadius: 12, + borderBottomRightRadius: 12, + pointerEvents: "none", + background: "linear-gradient(to right, rgba(13,17,23,0) 0%, rgba(13,17,23,0.95) 100%)" }} /> - </Paper> + </Box> </Container> </Box> ); diff --git a/src/components/qualification/Qualification.tsx b/src/components/qualification/Qualification.tsx index 6f46941..9ca7f69 100644 --- a/src/components/qualification/Qualification.tsx +++ b/src/components/qualification/Qualification.tsx @@ -10,7 +10,9 @@ import { Chip, Paper, Grid, - Avatar + Avatar, + useTheme, + useMediaQuery } from "@mui/material"; import { Timeline, @@ -141,6 +143,8 @@ const getTypeColor = (type: string): string => { const Qualification: React.FC = () => { const [value, setValue] = useState<number>(0); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const handleChange = useCallback((_event: React.SyntheticEvent, newValue: number): void => { setValue(newValue); @@ -223,10 +227,10 @@ const Qualification: React.FC = () => { sx={{ textTransform: "none", fontWeight: value === 0 ? 600 : 500, - fontSize: "1rem", + fontSize: { xs: "0.9rem", md: "1rem" }, color: value === 0 ? "#0eaddf" : "#8b949e", minHeight: 64, - px: 4 + px: { xs: 2, md: 4 } }} /> <Tab @@ -236,17 +240,25 @@ const Qualification: React.FC = () => { sx={{ textTransform: "none", fontWeight: value === 1 ? 600 : 500, - fontSize: "1rem", + fontSize: { xs: "0.9rem", md: "1rem" }, color: value === 1 ? "#0eaddf" : "#8b949e", minHeight: 64, - px: 4 + px: { xs: 2, md: 4 } }} /> </Tabs> <Box sx={{ p: { xs: 2, md: 4 } }}> <TabPanel value={value} index={0}> - <Timeline position="alternate"> + <Timeline + position={isMobile ? "right" : "alternate"} + sx={{ + ...(isMobile && { + pl: 0, + "& .MuiTimelineItem-root::before": { flex: 0, padding: 0 } + }) + }} + > {education.map((item: EducationItem, index: number) => ( <TimelineItem key={index}> <TimelineOppositeContent