Implement achraf's portfolio
This commit is contained in:
227
components/Skills.tsx
Normal file
227
components/Skills.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface SkillsProps {
|
||||
skills: string[];
|
||||
languages: { name: string; level: string }[];
|
||||
interests: string[];
|
||||
}
|
||||
|
||||
export default function Skills({ skills, languages, interests }: SkillsProps) {
|
||||
const headingRef = useRef<HTMLDivElement>(null);
|
||||
const skillsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const els = [headingRef.current, skillsRef.current].filter(Boolean);
|
||||
const obs = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("visible");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
els.forEach((el) => el && obs.observe(el));
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
// Give skills a pseudo-random "weight" based on their index for visual variety
|
||||
const weights = [1.3, 1, 1.5, 1, 1.2, 0.9, 1.4, 1, 1.1, 0.95, 1.3, 1, 1.2, 0.9, 1.4, 1, 1.1, 1.3];
|
||||
|
||||
const levelColor = (level: string) => {
|
||||
if (level === "Native") return "#c8a96e";
|
||||
if (level === "C2") return "#3a7a5a";
|
||||
if (level === "C1") return "#6b7a50";
|
||||
return "#4a5060";
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="skills"
|
||||
style={{ padding: "8rem 2rem", background: "#0a0b0e" }}
|
||||
>
|
||||
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
|
||||
<div ref={headingRef} className="reveal" style={{ marginBottom: "4rem" }}>
|
||||
<div className="section-label" style={{ marginBottom: "0.75rem" }}>Expertise</div>
|
||||
<h2
|
||||
className="font-display"
|
||||
style={{
|
||||
fontFamily: "var(--font-bebas), sans-serif",
|
||||
fontSize: "clamp(2.5rem, 6vw, 5rem)",
|
||||
letterSpacing: "0.04em",
|
||||
color: "#e2e4e9",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
Skills & Profile
|
||||
</h2>
|
||||
<div style={{ width: "48px", height: "1px", background: "#c8a96e", marginTop: "1rem" }} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 340px), 1fr))",
|
||||
gap: "2rem",
|
||||
}}
|
||||
>
|
||||
{/* Tech skills */}
|
||||
<div ref={skillsRef} className="reveal">
|
||||
<div
|
||||
className="font-mono"
|
||||
style={{
|
||||
fontFamily: "var(--font-jetbrains), monospace",
|
||||
fontSize: "0.62rem",
|
||||
letterSpacing: "0.18em",
|
||||
color: "#4a5060",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: "1.5rem",
|
||||
paddingBottom: "0.75rem",
|
||||
borderBottom: "1px solid #1c1f26",
|
||||
}}
|
||||
>
|
||||
Technical Stack
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem", alignItems: "center" }}>
|
||||
{skills.map((skill, i) => (
|
||||
<span
|
||||
key={skill}
|
||||
style={{
|
||||
fontFamily: "var(--font-jetbrains), monospace",
|
||||
fontSize: `${0.68 * (weights[i % weights.length] ?? 1)}rem`,
|
||||
padding: "4px 12px",
|
||||
border: "1px solid #1c1f26",
|
||||
color: "#6b7280",
|
||||
background: "#0e1014",
|
||||
letterSpacing: "0.06em",
|
||||
transition: "all 0.2s",
|
||||
cursor: "default",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
el.style.borderColor = "#c8a96e";
|
||||
el.style.color = "#c8a96e";
|
||||
el.style.background = "rgba(200,169,110,0.05)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
el.style.borderColor = "#1c1f26";
|
||||
el.style.color = "#6b7280";
|
||||
el.style.background = "#0e1014";
|
||||
}}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Languages + Interests stacked */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "2rem" }}>
|
||||
{/* Languages */}
|
||||
<div>
|
||||
<div
|
||||
className="font-mono"
|
||||
style={{
|
||||
fontFamily: "var(--font-jetbrains), monospace",
|
||||
fontSize: "0.62rem",
|
||||
letterSpacing: "0.18em",
|
||||
color: "#4a5060",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: "1.5rem",
|
||||
paddingBottom: "0.75rem",
|
||||
borderBottom: "1px solid #1c1f26",
|
||||
}}
|
||||
>
|
||||
Languages
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
|
||||
{languages.map((lang) => (
|
||||
<div
|
||||
key={lang.name}
|
||||
style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "var(--font-lora), serif",
|
||||
fontSize: "0.95rem",
|
||||
color: "#e2e4e9",
|
||||
}}
|
||||
>
|
||||
{lang.name}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono"
|
||||
style={{
|
||||
fontFamily: "var(--font-jetbrains), monospace",
|
||||
fontSize: "0.62rem",
|
||||
letterSpacing: "0.12em",
|
||||
padding: "2px 8px",
|
||||
border: `1px solid ${levelColor(lang.level)}`,
|
||||
color: levelColor(lang.level),
|
||||
}}
|
||||
>
|
||||
{lang.level}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interests */}
|
||||
<div>
|
||||
<div
|
||||
className="font-mono"
|
||||
style={{
|
||||
fontFamily: "var(--font-jetbrains), monospace",
|
||||
fontSize: "0.62rem",
|
||||
letterSpacing: "0.18em",
|
||||
color: "#4a5060",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: "1.5rem",
|
||||
paddingBottom: "0.75rem",
|
||||
borderBottom: "1px solid #1c1f26",
|
||||
}}
|
||||
>
|
||||
Interests
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
|
||||
{interests.map((interest) => (
|
||||
<span
|
||||
key={interest}
|
||||
style={{
|
||||
fontFamily: "var(--font-lora), serif",
|
||||
fontSize: "0.85rem",
|
||||
padding: "4px 14px",
|
||||
border: "1px solid #1c1f26",
|
||||
color: "#6b7280",
|
||||
background: "#0e1014",
|
||||
fontStyle: "italic",
|
||||
transition: "all 0.2s",
|
||||
cursor: "default",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
el.style.borderColor = "#6b5730";
|
||||
el.style.color = "#c8a96e";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
el.style.borderColor = "#1c1f26";
|
||||
el.style.color = "#6b7280";
|
||||
}}
|
||||
>
|
||||
{interest}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user