mirror of
https://github.com/XFox111/my-website.git
synced 2026-04-22 07:28:01 +03:00
init: First version
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
@import "../theme.scss";
|
||||
|
||||
.section
|
||||
{
|
||||
@include centerTwo;
|
||||
@include body2($fontFamilyBaseAlt);
|
||||
color: $colorNeutralForeground2;
|
||||
align-items: center;
|
||||
|
||||
> div:first-child
|
||||
{
|
||||
@include flex(column);
|
||||
gap: $spacingM;
|
||||
}
|
||||
|
||||
> img
|
||||
{
|
||||
height: auto;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
justify-self: center;
|
||||
|
||||
border-radius: $borderRadiusMedium;
|
||||
box-shadow: $shadow2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { aboutPicture } from "@/_assets/illustrations";
|
||||
import bio from "@/_data/bio";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import cls from "./AboutSection.module.scss";
|
||||
|
||||
const AboutSection: React.FC = () => (
|
||||
<section id="about" className={ cls.section }>
|
||||
<div>
|
||||
<h2>About me</h2>
|
||||
|
||||
{ bio.map((i, index) =>
|
||||
<p key={ index }>{ i }</p>
|
||||
) }
|
||||
</div>
|
||||
|
||||
<Image src={ aboutPicture.src } alt={ aboutPicture.alt } />
|
||||
</section>
|
||||
);
|
||||
|
||||
export default AboutSection;
|
||||
@@ -0,0 +1,91 @@
|
||||
@import "../theme.scss";
|
||||
|
||||
.section
|
||||
{
|
||||
@include flex(column);
|
||||
gap: $spacingXXXL;
|
||||
|
||||
@include body1;
|
||||
|
||||
h2
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content
|
||||
{
|
||||
@include centerTwo;
|
||||
|
||||
.container
|
||||
{
|
||||
@include flex(column);
|
||||
gap: $spacingM;
|
||||
}
|
||||
|
||||
.contacts
|
||||
{
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.textarea
|
||||
{
|
||||
min-width: 100%;
|
||||
min-height: 160px;
|
||||
max-width: 60vw;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.formToolbar
|
||||
{
|
||||
@include flex(row);
|
||||
justify-content: flex-end;
|
||||
|
||||
@media screen and (max-width: 460px)
|
||||
{
|
||||
flex-flow: column;
|
||||
row-gap: $spacingM;
|
||||
align-items: flex-end;
|
||||
|
||||
.status > span
|
||||
{
|
||||
@include body1($fontFamilyBaseAlt);
|
||||
}
|
||||
}
|
||||
|
||||
.status
|
||||
{
|
||||
@include flex(row);
|
||||
@include align(center, flex-end);
|
||||
@include body2($fontFamilyBaseAlt);
|
||||
|
||||
height: 40px;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
> span
|
||||
{
|
||||
margin: $spacingS $spacingM;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
color: $colorNeutralForegroundStaticInverted;
|
||||
background-color: $colorStatusDangerBackground3;
|
||||
|
||||
transition-property: width;
|
||||
transition-duration: $durationNormal;
|
||||
transition-timing-function: $curveEasyEaseMax;
|
||||
|
||||
&:is(.error, .success)
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.success
|
||||
{
|
||||
background-color: $colorStatusSuccessBackground3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/_components/Button";
|
||||
import contacts from "@/_data/contacts";
|
||||
import FormStatusTracker from "@/_utils/FormStatusTracker";
|
||||
import React, { InputHTMLAttributes, useMemo, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import sendInquiry, { FormStatus } from "../_utils/sendInquiry";
|
||||
import cls from "./ContactSection.module.scss";
|
||||
|
||||
const defaultState: FormStatus = { status: "idle" };
|
||||
|
||||
const ContactSection: React.FC = () =>
|
||||
{
|
||||
const [pending, setPending] = useState<boolean>(false);
|
||||
const [{ status, message }, formAction] = useFormState<FormStatus, FormData>(sendInquiry, defaultState);
|
||||
const { telephone: phone, email, socials } = contacts;
|
||||
|
||||
const sharedProps: InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> = useMemo(() => ({
|
||||
required: true,
|
||||
disabled: pending,
|
||||
readOnly: status === "success"
|
||||
}), [status, pending]);
|
||||
|
||||
return (
|
||||
<section id="contacts" className={ cls.section }>
|
||||
<h2>Let's get in touch</h2>
|
||||
|
||||
<div className={ cls.content }>
|
||||
|
||||
<div className={ cls.container }>
|
||||
<h3>Inquiries, requests or proposals</h3>
|
||||
|
||||
<form className={ cls.container } action={ formAction }>
|
||||
<FormStatusTracker onPendingChanged={ setPending } />
|
||||
|
||||
<input name="email" type="email" { ...sharedProps }
|
||||
autoComplete="email" spellCheck="false"
|
||||
maxLength={ 60 }
|
||||
placeholder="Email" />
|
||||
|
||||
<input name="subject" type="text" { ...sharedProps }
|
||||
autoComplete="off" spellCheck="true"
|
||||
maxLength={ 120 }
|
||||
placeholder="Subject" />
|
||||
|
||||
<textarea name="message" { ...sharedProps } className={ cls.textarea }
|
||||
autoComplete="off" spellCheck="true"
|
||||
minLength={ 100 } maxLength={ 2000 }
|
||||
placeholder="Message (min 100 characters)" />
|
||||
|
||||
<input name="timezone" type="hidden" readOnly
|
||||
value={ Intl.DateTimeFormat().resolvedOptions().timeZone } />
|
||||
|
||||
<div className={ cls.formToolbar }>
|
||||
<div className={ `${cls.status} ${pending ? "" : cls[status]}` }>
|
||||
{ pending &&
|
||||
<span role="alert" aria-live="assertive">
|
||||
Sending
|
||||
</span>
|
||||
}
|
||||
{ !pending && status === "success" &&
|
||||
<span role="alert" aria-live="assertive">
|
||||
Message successfully sent
|
||||
</span>
|
||||
}
|
||||
{ !pending && status === "error" &&
|
||||
<span role="alert" aria-live="assertive">
|
||||
{ message ?? "Something went wrong" }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
{ status !== "success" &&
|
||||
<Button type="submit" disabled={ pending }>
|
||||
Submit
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className={ `${cls.container} ${cls.contacts}` }>
|
||||
<h3>Direct contacts</h3>
|
||||
|
||||
<p>
|
||||
{ Object.entries(socials).map(([name, i]) =>
|
||||
<span key={ name }>
|
||||
<span aria-hidden>{ name + ": " }</span>
|
||||
<a aria-label={ `${name}: ${i.username}` } href={ i.href } target="_blank">
|
||||
{ i.username }
|
||||
</a>
|
||||
<br aria-hidden />
|
||||
</span>
|
||||
) }
|
||||
|
||||
{ phone &&
|
||||
<span>
|
||||
<span aria-hidden>Telephone: </span>
|
||||
<a aria-label={ `Telephone: ${phone.text} (${phone.country})` } href={ phone.href }>
|
||||
{ phone.text }
|
||||
</a>
|
||||
<span aria-hidden> ({ phone.country })</span>
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
|
||||
<Button href={ email.href } target="_blank">
|
||||
{ email.text }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSection;
|
||||
@@ -0,0 +1,242 @@
|
||||
@import "../theme.scss";
|
||||
|
||||
.section
|
||||
{
|
||||
@include flex(column);
|
||||
gap: $spacingXXXL;
|
||||
|
||||
h2
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Properties shared between horizontal and vertical timelines
|
||||
.timeline
|
||||
{
|
||||
@include maxCenter(1600px);
|
||||
display: grid;
|
||||
position: relative;
|
||||
|
||||
.line
|
||||
{
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
display: grid;
|
||||
|
||||
> *
|
||||
{
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.strip
|
||||
{
|
||||
border-radius: $borderRadiusMedium;
|
||||
}
|
||||
|
||||
.trailHorizontal
|
||||
{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.trailVertical
|
||||
{
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
display: grid;
|
||||
|
||||
.year
|
||||
{
|
||||
@include subtitle1($fontFamilyBaseAlt);
|
||||
}
|
||||
|
||||
> i
|
||||
{
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
||||
border: $strokeWidthThickest solid $colorNeutralForeground1;
|
||||
border-radius: $borderRadiusCircular;
|
||||
background-color: $colorNeutralForeground1;
|
||||
box-shadow: inset 0 0 0 16px $colorNeutralBackground1;
|
||||
}
|
||||
|
||||
.description
|
||||
{
|
||||
p
|
||||
{
|
||||
@include body2($fontFamilyBaseAlt);
|
||||
color: $colorNeutralForeground3;
|
||||
}
|
||||
|
||||
.title
|
||||
{
|
||||
@include subtitle1($fontFamilyBaseAlt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical timeline
|
||||
@media screen and (max-width: 860px)
|
||||
{
|
||||
gap: $spacingXXXL;
|
||||
|
||||
.line
|
||||
{
|
||||
height: 100%;
|
||||
width: 8px;
|
||||
left: 88px;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
|
||||
.strip
|
||||
{
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
background-image:
|
||||
repeating-linear-gradient($colorNeutralForeground1 0 $spacingM, transparent 0 $spacingXXL);
|
||||
}
|
||||
|
||||
.trailHorizontal
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
grid-template-columns: 64px auto 1fr;
|
||||
padding: $spacingXXXL $spacingNone;
|
||||
gap: $spacingM;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal timeline
|
||||
@media screen and (min-width: 861px)
|
||||
{
|
||||
grid-auto-flow: column;
|
||||
|
||||
.line
|
||||
{
|
||||
bottom: 72px;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
|
||||
.strip
|
||||
{
|
||||
height: 8px;
|
||||
background-image:
|
||||
repeating-linear-gradient(90deg, $colorNeutralForeground1 0 $spacingM, transparent 0 $spacingXXL);
|
||||
}
|
||||
|
||||
.trailVertical
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item
|
||||
{
|
||||
grid-template-rows: 128px auto 48px;
|
||||
padding: $spacingNone $spacingM;
|
||||
row-gap: $spacingM;
|
||||
|
||||
.year
|
||||
{
|
||||
grid-row: 3/3;
|
||||
}
|
||||
|
||||
> i
|
||||
{
|
||||
grid-row: 2/2;
|
||||
transition: box-shadow $durationNormal $curveEasyEaseMax;
|
||||
}
|
||||
|
||||
h3,
|
||||
p
|
||||
{
|
||||
transition-property: font-size, line-height, opacity;
|
||||
transition-duration: $durationNormal;
|
||||
transition-timing-function: $curveEasyEaseMax;
|
||||
}
|
||||
|
||||
.description
|
||||
{
|
||||
grid-row: 1/1;
|
||||
align-self: self-end;
|
||||
|
||||
p
|
||||
{
|
||||
font-size: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px)
|
||||
{
|
||||
.title
|
||||
{
|
||||
opacity: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When there's something hovered or focused inside the timeline
|
||||
&:has(:hover, :focus-visible, :focus-within) .item
|
||||
{
|
||||
|
||||
// Item that is being hovered or focused
|
||||
&:is(:hover, :focus-visible, :focus-within)
|
||||
{
|
||||
|
||||
.year,
|
||||
.title
|
||||
{
|
||||
@include title1($fontFamilyBaseAlt);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> i
|
||||
{
|
||||
box-shadow: inset 0 0 0 0 $colorNeutralBackground1;
|
||||
}
|
||||
|
||||
.description > p
|
||||
{
|
||||
@include subtitle2($fontFamilyBaseAlt);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Other not focused items
|
||||
&:not(:hover, :focus-visible, :focus-within)
|
||||
{
|
||||
|
||||
.year,
|
||||
.title
|
||||
{
|
||||
@include body1($fontFamilyBaseAlt);
|
||||
color: $colorNeutralForeground3;
|
||||
|
||||
@media screen and (max-width: 1200px)
|
||||
{
|
||||
opacity: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { experienceBgHorizontal, experienceBgVertical } from "@/_assets/decorations";
|
||||
import experience, { WorkplaceEntry } from "@/_data/experience";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import cls from "./ExperienceSection.module.scss";
|
||||
|
||||
const ExperienceSection: React.FC = () => (
|
||||
<section id="experience" className={ cls.section }>
|
||||
<h2>My work experience</h2>
|
||||
|
||||
<div className={ cls.timeline } role="list" aria-label="My work experience">
|
||||
<div aria-hidden className={ cls.line }>
|
||||
<Image className={ cls.trailHorizontal }
|
||||
alt={ experienceBgHorizontal.alt } src={ experienceBgHorizontal.src } />
|
||||
<Image className={ cls.trailVertical }
|
||||
alt={ experienceBgVertical.alt } src={ experienceBgVertical.src } />
|
||||
|
||||
<i className={ cls.strip } />
|
||||
</div>
|
||||
{ experience.map((i, index) =>
|
||||
<div className={ cls.item } key={ index }
|
||||
tabIndex={ 0 } role="listitem" aria-label={ getAriaLabel(i) }>
|
||||
|
||||
<p aria-hidden className={ cls.year }>{ i.year }</p>
|
||||
<i />
|
||||
<div className={ cls.description }>
|
||||
<p aria-hidden>{ i.place }</p>
|
||||
<h3 aria-hidden className={ cls.title }>{ i.title }</h3>
|
||||
<p aria-hidden={ !!i.tech }>{ i.tech ?? <Link href="#contacts">Contact me</Link> }</p>
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
function getAriaLabel(item: WorkplaceEntry): string
|
||||
{
|
||||
let str: string[] = [];
|
||||
|
||||
if (item.year)
|
||||
str.push(`${item.year} -`);
|
||||
|
||||
str.push(item.title);
|
||||
|
||||
if (item.place)
|
||||
str.push(`at ${item.place}`);
|
||||
|
||||
if (item.tech)
|
||||
return str.join(" ") + `. ${item.tech}`;
|
||||
else
|
||||
return str.join(" ");
|
||||
|
||||
}
|
||||
|
||||
export default ExperienceSection;
|
||||
@@ -0,0 +1,98 @@
|
||||
@import "../theme.scss";
|
||||
|
||||
.section
|
||||
{
|
||||
@include centerTwo;
|
||||
|
||||
.listItem
|
||||
{
|
||||
background-position: right;
|
||||
}
|
||||
|
||||
.descriptions
|
||||
{
|
||||
@include body2($fontFamilyBaseAlt);
|
||||
display: grid;
|
||||
overflow-x: visible;
|
||||
min-height: 760px;
|
||||
|
||||
@media screen and (max-width: 860px)
|
||||
{
|
||||
min-height: unset;
|
||||
padding-top: calc(56px + $spacingXL);
|
||||
}
|
||||
|
||||
img
|
||||
{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.defaultImg
|
||||
{
|
||||
@include slideIn;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.projectItem
|
||||
{
|
||||
@include flex(column);
|
||||
max-width: 600px;
|
||||
justify-self: center;
|
||||
gap: $spacingM;
|
||||
|
||||
@include slideIn;
|
||||
|
||||
@media (prefers-color-scheme: light)
|
||||
{
|
||||
> img[data-theme=dark]
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark)
|
||||
{
|
||||
> img[data-theme=light]
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
p
|
||||
{
|
||||
color: $colorNeutralForeground2;
|
||||
}
|
||||
|
||||
> h4
|
||||
{
|
||||
@include subtitle1($fontFamilyBaseAlt);
|
||||
}
|
||||
|
||||
.stack
|
||||
{
|
||||
@include flex(row, wrap);
|
||||
gap: $spacingL;
|
||||
@include body1;
|
||||
|
||||
.item
|
||||
{
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
gap: $spacingSNudge;
|
||||
|
||||
> svg
|
||||
{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cta
|
||||
{
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { projectsImg } from "@/_assets/illustrations";
|
||||
import Button from "@/_components/Button";
|
||||
import links from "@/_data/links";
|
||||
import projects from "@/_data/projects";
|
||||
import shared from "@/_styles/gallery.module.scss";
|
||||
import { ArrowRight24Regular } from "@fluentui/react-icons";
|
||||
import Image from "next/image";
|
||||
import React, { useState } from "react";
|
||||
import { networkFor } from "react-social-icons";
|
||||
import cls from "./ProjectsSection.module.scss";
|
||||
|
||||
const ProjectsSection: React.FC = () =>
|
||||
{
|
||||
const [selection, setSelection] = useState<number | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<section id="projects" className={ cls.section }>
|
||||
<div className={ shared.list }>
|
||||
<h2>My pet projects</h2>
|
||||
|
||||
{ projects.map((project, index) =>
|
||||
<Button key={ index } type="button"
|
||||
className={ `${shared.listItem} ${cls.listItem}` }
|
||||
appearance={ selection === index ? "primary" : "secondary" }
|
||||
data-selected={ selection === index }
|
||||
onClick={ () => setSelection(selection == index ? undefined : index) }
|
||||
aria-label={ `"${project.title}". ${project.subtitle}` }>
|
||||
|
||||
<div className={ shared.content }>
|
||||
<span className={ shared.title }>{ project.title }</span>
|
||||
<span>{ project.subtitle }</span>
|
||||
</div>
|
||||
</Button>
|
||||
) }
|
||||
|
||||
<Button className={ cls.cta } appearance="secondary" href={ links.github } target="_blank"
|
||||
iconAfter={ <ArrowRight24Regular /> }>
|
||||
|
||||
View GitHub profile
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={ cls.descriptions } aria-live="polite" aria-atomic>
|
||||
{ projects.map((project, index) =>
|
||||
<div key={ index } className={ cls.projectItem } hidden={ selection !== index }>
|
||||
<Image src={ project.image } alt={ project.title } data-theme={ project.imageDark ? "light" : "both" } />
|
||||
{/* This is a workaround since not all images can be theme-adaptive */ }
|
||||
{ project.imageDark &&
|
||||
<Image src={ project.imageDark } alt="" data-theme="dark" />
|
||||
}
|
||||
|
||||
<h3>{ project.title }</h3>
|
||||
|
||||
{ project.description?.map((i, index) =>
|
||||
<p key={ index }>{ i }</p>
|
||||
) }
|
||||
|
||||
<h4>Stack</h4>
|
||||
<div className={ cls.stack }>
|
||||
{ project.stack.map((i, index) =>
|
||||
<div key={ index } className={ cls.item }>
|
||||
<i.icon /> { i.text }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
|
||||
<Button className={ cls.cta } appearance="secondary" href={ project.link } target="_blank"
|
||||
iconAfter={ <ArrowRight24Regular /> }>
|
||||
|
||||
{ networkFor(project.link) === "github" ? "View on GitHub" : "Visit project page" }
|
||||
</Button>
|
||||
</div>
|
||||
) }
|
||||
<Image className={ cls.defaultImg } hidden={ selection !== undefined }
|
||||
src={ projectsImg.src } alt={ projectsImg.alt } />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsSection;
|
||||
@@ -0,0 +1,51 @@
|
||||
@import "../theme.scss";
|
||||
|
||||
.section
|
||||
{
|
||||
@include centerTwo;
|
||||
align-items: center;
|
||||
|
||||
.illustrations
|
||||
{
|
||||
justify-self: center;
|
||||
position: relative;
|
||||
|
||||
img
|
||||
{
|
||||
width: 100%;
|
||||
max-height: 600px;
|
||||
|
||||
@include slideIn;
|
||||
}
|
||||
|
||||
// [SPECIAL]
|
||||
.whatsThis
|
||||
{
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
|
||||
bottom: calc(50% - 20px + 13.5%);
|
||||
left: 40%;
|
||||
width: 20%;
|
||||
height: 40px;
|
||||
|
||||
@media screen and (max-width: 1400px)
|
||||
{
|
||||
bottom: calc(50% - 20px + 6vw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list
|
||||
{
|
||||
.cta
|
||||
{
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 860px)
|
||||
{
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/_components/Button";
|
||||
import skills from "@/_data/skills";
|
||||
import shared from "@/_styles/gallery.module.scss";
|
||||
import { ArrowDownload24Regular } from "@fluentui/react-icons";
|
||||
import Image from "next/image";
|
||||
import React, { useId, useState } from "react";
|
||||
import cls from "./SkillsSection.module.scss";
|
||||
import links from "@/_data/links";
|
||||
|
||||
const SkillsSection: React.FC = () =>
|
||||
{
|
||||
const [selection, setSelection] = useState<number>(0);
|
||||
const illustrations = useId();
|
||||
|
||||
return (
|
||||
<section id="skills" className={ cls.section }>
|
||||
<div id={ illustrations } className={ cls.illustrations } aria-live="polite" aria-atomic>
|
||||
{ skills.map((i, index) =>
|
||||
<Image key={ index }
|
||||
src={ i.image.src } alt={ i.image.alt }
|
||||
hidden={ selection !== index } />
|
||||
) }
|
||||
|
||||
{ selection === 4 &&
|
||||
// [SPECIAL] It's a surprize tool that will help us later
|
||||
<div role="button" aria-label="Click me" className={ cls.whatsThis } onClick={ () => window.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ") } />
|
||||
}
|
||||
</div>
|
||||
<div className={ `${shared.list} ${cls.list}` }>
|
||||
<h2>My skillset</h2>
|
||||
|
||||
{ skills.map((skill, index) =>
|
||||
<Button key={ index } type="button" aria-current={ selection === index } aria-controls={ illustrations }
|
||||
className={ shared.listItem } appearance={ selection === index ? "primary" : "secondary" }
|
||||
data-selected={ selection === index }
|
||||
onClick={ () => setSelection(index) }
|
||||
aria-label={ `${skill.title} skills. Associated stack: ${skill.description}` }
|
||||
icon={ <skill.icon /> } >
|
||||
|
||||
<div className={ shared.content }>
|
||||
<span className={ shared.title }>{ skill.title }</span>
|
||||
<span>{ skill.description }</span>
|
||||
</div>
|
||||
</Button>
|
||||
) }
|
||||
|
||||
<Button appearance="secondary" className={ cls.cta }
|
||||
as="next" href={ links.resume }
|
||||
icon={ <ArrowDownload24Regular /> }>
|
||||
|
||||
Download resume
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsSection;
|
||||
Reference in New Issue
Block a user