1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-05-29 22:37:44 +02:00

refactor: components struture

This commit is contained in:
2024-07-31 11:41:39 +02:00
parent ceeeb2f9c5
commit b5c50728de
72 changed files with 122 additions and 114 deletions

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { About as AboutComponent } from "./About"
const meta = {
title: "Home/About",
component: AboutComponent,
} satisfies Meta<typeof AboutComponent>
export default meta
type Story = StoryObj<typeof meta>
export const About: Story = {
args: {},
}

View File

@@ -0,0 +1,27 @@
import { Section, SectionContent } from "../../Layout/Section/Section"
import { AboutDescription } from "./AboutDescription"
import { AboutIntroduction } from "./AboutIntroduction"
import { AboutList } from "./AboutList/AboutList"
import { AboutLogo } from "./AboutLogo"
import { SocialMediaList } from "./SocialMediaList/SocialMediaList"
export interface AboutProps {}
export const About: React.FC<AboutProps> = () => {
return (
<Section verticalSpacing horizontalSpacing id="about">
<SectionContent shadowContainer>
<div className="flex flex-col items-center justify-center md:flex-row md:pt-10">
<AboutLogo />
<div>
<AboutIntroduction />
<AboutList />
<AboutDescription />
</div>
</div>
<SocialMediaList />
</SectionContent>
</Section>
)
}

View File

@@ -0,0 +1,21 @@
import { useTranslations } from "next-intl"
import { Button } from "../../Design/Button/Button"
import { Typography } from "../../Design/Typography/Typography"
export interface AboutDescriptionProps {}
export const AboutDescription: React.FC<AboutDescriptionProps> = () => {
const t = useTranslations()
return (
<div className="dark:text-gray my-6 max-w-md text-center text-black">
<Typography as="p" variant="text1" className="my-6">
{t.rich("home.about.description")}
</Typography>
<Button href="/curriculum-vitae/index.html" variant="outline">
Curriculum vitæ ({t("locales.fr-FR")})
</Button>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { useTranslations } from "next-intl"
import { Typography } from "../../Design/Typography/Typography"
export interface AboutIntroductionProps {}
export const AboutIntroduction: React.FC<AboutIntroductionProps> = () => {
const t = useTranslations()
return (
<div className="border-b border-black dark:border-white">
<Typography as="h1" variant="h1">
{t("meta.title")}
</Typography>
<Typography as="h2" variant="text1" className="my-3">
{t("meta.description")}
</Typography>
</div>
)
}

View File

@@ -0,0 +1,26 @@
export interface AboutItemProps {
label: string
value: string
link?: string
}
export const AboutItem: React.FC<AboutItemProps> = (props) => {
const { label, value, link } = props
return (
<li className="flex items-center justify-between sm:justify-start">
<strong className="w-24 text-sm text-black lg:w-32 dark:text-white">
{label}
</strong>
<span className="dark:text-gray block text-sm font-normal text-black">
{link != null ? (
<a className="hover:underline" href={link}>
{value}
</a>
) : (
value
)}
</span>
</li>
)
}

View File

@@ -0,0 +1,27 @@
"use client"
import { BIRTH_DATE } from "@repo/utils/constants"
import { getAge, getISODate } from "@repo/utils/dates"
import { useTranslations } from "next-intl"
import { useMemo } from "react"
import { AboutItem } from "./AboutItem"
export interface AboutItemBirthDateProps {}
export const AboutItemBirthDate: React.FC<AboutItemBirthDateProps> = () => {
const t = useTranslations()
const age = useMemo(() => {
return getAge(BIRTH_DATE)
}, [])
return (
<AboutItem
label={t("home.about.birth-date.label")}
value={t("home.about.birth-date.value", {
age,
birthDate: getISODate(BIRTH_DATE),
})}
/>
)
}

View File

@@ -0,0 +1,30 @@
import { useTranslations } from "next-intl"
import { AboutItem } from "./AboutItem"
import { AboutItemBirthDate } from "./AboutItemBirthDate"
export interface AboutListProps {}
export const AboutList: React.FC<AboutListProps> = () => {
const t = useTranslations()
return (
<ul className="my-6 list-none space-y-3">
<AboutItem
label={t("home.about.pronouns.label")}
value={t("home.about.pronouns.value")}
/>
<AboutItemBirthDate />
<AboutItem
label={t("home.about.nationality.label")}
value={t("home.about.nationality.value")}
/>
<AboutItem
label={t("home.about.email.label")}
value={t("home.about.email.value", {
email: "contact@theoludwig.fr",
})}
link="mailto:contact@theoludwig.fr"
/>
</ul>
)
}

View File

@@ -0,0 +1,21 @@
import { useTranslations } from "next-intl"
import Image from "next/image"
export interface AboutLogoProps {}
export const AboutLogo: React.FC<AboutLogoProps> = () => {
const t = useTranslations()
return (
<div className="max-h-[370px] max-w-[370px] px-2 py-6">
<Image
quality={100}
src="/images/logo.webp"
alt={t("meta.title")}
width={800}
height={800}
priority
/>
</div>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>Email</title>
<path d="M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12" />
</Icon>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</Icon>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const GitLabIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>GitLab</title>
<path d="M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z" />
</Icon>
)
}

View File

@@ -0,0 +1,19 @@
import { classNames } from "@repo/config-tailwind/classNames"
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
const { children, className, ...rest } = props
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className={classNames(
"size-8 fill-current text-black dark:text-white",
className,
)}
{...rest}
>
{children}
</svg>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const NPMIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>npm</title>
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
</Icon>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>Twitch</title>
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z" />
</Icon>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>Twitter</title>
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
</Icon>
)
}

View File

@@ -0,0 +1,10 @@
import { Icon } from "./Icon"
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<Icon {...props}>
<title>YouTube</title>
<path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</Icon>
)
}

View File

@@ -0,0 +1,21 @@
export interface SocialMediaItemProps extends React.PropsWithChildren {
link: string
ariaLabel: string
}
export const SocialMediaItem: React.FC<SocialMediaItemProps> = (props) => {
const { link, ariaLabel, children } = props
return (
<li className="mx-4 my-1 inline-block">
<a
href={link}
aria-label={ariaLabel}
target="_blank"
className="relative inline-block bg-transparent transition-all duration-300 ease-in-out hover:scale-110"
>
{children}
</a>
</li>
)
}

View File

@@ -0,0 +1,53 @@
import { EmailIcon } from "./SocialMediaIcons/EmailIcon"
import { GitHubIcon } from "./SocialMediaIcons/GitHubIcon"
import { GitLabIcon } from "./SocialMediaIcons/GitLabIcon"
import { NPMIcon } from "./SocialMediaIcons/NPMIcon"
import { TwitchIcon } from "./SocialMediaIcons/TwitchIcon"
import { TwitterIcon } from "./SocialMediaIcons/TwitterIcon"
import { YouTubeIcon } from "./SocialMediaIcons/YouTubeIcon"
import { SocialMediaItem } from "./SocialMediaItem"
export interface SocialMediaListProps {}
export const SocialMediaList: React.FC<SocialMediaListProps> = () => {
return (
<ul className="mt-6 list-none text-center">
<SocialMediaItem link="https://github.com/theoludwig" ariaLabel="GitHub">
<GitHubIcon />
</SocialMediaItem>
<SocialMediaItem link="https://gitlab.com/theoludwig" ariaLabel="GitLab">
<GitLabIcon />
</SocialMediaItem>
<SocialMediaItem link="https://www.npmjs.com/~theoludwig" ariaLabel="npm">
<NPMIcon />
</SocialMediaItem>
<SocialMediaItem
link="https://twitter.com/theoludwig_"
ariaLabel="Twitter"
>
<TwitterIcon />
</SocialMediaItem>
<SocialMediaItem
link="https://www.youtube.com/@theo_ludwig"
ariaLabel="YouTube"
>
<YouTubeIcon />
</SocialMediaItem>
<SocialMediaItem
link="https://www.twitch.tv/theoludwig"
ariaLabel="Twitch"
>
<TwitchIcon />
</SocialMediaItem>
<SocialMediaItem link="mailto:contact@theoludwig.fr" ariaLabel="Email">
<EmailIcon />
</SocialMediaItem>
</ul>
)
}

View File

@@ -0,0 +1,26 @@
import { Typography } from "../../Design/Typography/Typography"
export interface InterestItemProps {
title: string
description: React.ReactNode
}
export const InterestItem: React.FC<InterestItemProps> = (props) => {
const { title, description } = props
return (
<div className="my-6 text-center">
<Typography as="h3" variant="h4">
{title}
</Typography>
<Typography
as="p"
variant="text1"
className="dark:text-gray my-2 text-black"
>
{description}
</Typography>
</div>
)
}

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { Interests as InterestsComponent } from "./Interests"
const meta = {
title: "Home/Interests",
component: InterestsComponent,
} satisfies Meta<typeof InterestsComponent>
export default meta
type Story = StoryObj<typeof meta>
export const Interests: Story = {
args: {},
}

View File

@@ -0,0 +1,74 @@
import { GIT_REPO_LINK } from "@repo/utils/constants"
import { useTranslations } from "next-intl"
import { FaGit, FaMicrochip } from "react-icons/fa"
import { Link } from "../../Design/Link/Link"
import {
Section,
SectionContent,
SectionTitle,
} from "../../Layout/Section/Section"
import { InterestItem } from "./InterestItem"
export interface InterestsProps {}
export const Interests: React.FC<InterestsProps> = () => {
const t = useTranslations()
const items = [
{
id: "code",
title: t("home.interests.code.title"),
description: t.rich("home.interests.code.description", {
"abbr-ux": (children) => {
return <abbr title="User Experience">{children}</abbr>
},
}),
Icon: FaMicrochip,
},
{
id: "open-source",
title: t("home.interests.open-source.title"),
description: t.rich("home.interests.open-source.description", {
"github-link": (children) => {
return (
<Link href={GIT_REPO_LINK} target="_blank">
{children}
</Link>
)
},
}),
Icon: FaGit,
},
] as const
return (
<Section verticalSpacing horizontalSpacing id="interests">
<SectionTitle>{t("home.interests.title")}</SectionTitle>
<SectionContent shadowContainer>
<div className="max-w-full">
{items.map((item) => {
return (
<InterestItem
key={item.id}
title={item.title}
description={item.description}
/>
)
})}
</div>
<div className="my-4 flex justify-center">
<ul className="m-0 flex w-96 list-none justify-around p-0">
{items.map((item) => {
return (
<li className="m-2 size-8" key={item.id} title={item.title}>
<item.Icon className="text-primary dark:text-primary-dark block size-full" />
</li>
)
})}
</ul>
</div>
</SectionContent>
</Section>
)
}

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { OpenSource as OpenSourceComponent } from "./OpenSource"
const meta = {
title: "Home/OpenSource",
component: OpenSourceComponent,
} satisfies Meta<typeof OpenSourceComponent>
export default meta
type Story = StoryObj<typeof meta>
export const OpenSource: Story = {
args: {},
}

View File

@@ -0,0 +1,47 @@
import { useTranslations } from "next-intl"
import {
Section,
SectionDescription,
SectionTitle,
} from "../../Layout/Section/Section"
import { Repository } from "./Repository"
export interface OpenSourceProps {}
export const OpenSource: React.FC<OpenSourceProps> = () => {
const t = useTranslations()
return (
<Section verticalSpacing horizontalSpacing id="open-source">
<SectionTitle>{t("home.open-source.title")}</SectionTitle>
<SectionDescription>
{t("home.open-source.description")}
</SectionDescription>
<div className="flex max-w-full flex-col items-center">
<ul className="grid list-none grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2">
<Repository
name="nodejs/node"
description="Node.js JavaScript runtime ✨🐢🚀✨"
href="https://github.com/nodejs/node/commits?author=theoludwig"
/>
<Repository
name="standard/standard"
description="🌟 JavaScript Style Guide, with linter & automatic code fixer"
href="https://github.com/standard/standard/commits?author=theoludwig"
/>
<Repository
name="DefinitelyTyped/DefinitelyTyped"
description="High quality TypeScript type definitions."
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits?author=theoludwig"
/>
<Repository
name="vercel/next.js"
description="The React Framework"
href="https://github.com/vercel/next.js/commits?author=theoludwig"
/>
</ul>
</div>
</Section>
)
}

View File

@@ -0,0 +1,32 @@
import { Typography } from "../../Design/Typography/Typography"
import { SectionContent } from "../../Layout/Section/Section"
import { GitHubIcon } from "../About/SocialMediaList/SocialMediaIcons/GitHubIcon"
export interface RepositoryProps {
name: string
description: string
href: string
}
export const Repository: React.FC<RepositoryProps> = (props) => {
const { name, description, href } = props
return (
<li>
<a href={href} target="_blank">
<SectionContent
className="relative cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.03] sm:p-6"
shadowContainer
>
<Typography as="h3" variant="text1" className="flex items-center">
<GitHubIcon className="mr-2 h-6" />
<span className="text-primary dark:text-primary-dark font-semibold">
{name}
</span>
</Typography>
<p className="mt-4">{description}</p>
</SectionContent>
</a>
</li>
)
}

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { Portfolio as PortfolioComponent } from "./Portfolio"
const meta = {
title: "Home/Portfolio",
component: PortfolioComponent,
} satisfies Meta<typeof PortfolioComponent>
export default meta
type Story = StoryObj<typeof meta>
export const Portfolio: Story = {
args: {},
}

View File

@@ -0,0 +1,38 @@
import { useTranslations } from "next-intl"
import { Section, SectionTitle } from "../../Layout/Section/Section"
import { PortfolioItem, type PortfolioProject } from "./PortfolioItem"
export interface PortfolioProps {}
export const Portfolio: React.FC<PortfolioProps> = () => {
const t = useTranslations()
const items: PortfolioProject[] = [
{
id: "carolo",
title: t("home.portfolio.carolo.title"),
description: t("home.portfolio.carolo.description"),
link: "https://carolo.theoludwig.fr/",
image: "/images/portfolio/Carolo.webp",
},
{
id: "leon",
title: t("home.portfolio.leon.title"),
description: t("home.portfolio.leon.description"),
link: "https://getleon.ai/",
image: "/images/portfolio/Leon.webp",
},
]
return (
<Section verticalSpacing horizontalSpacing id="portfolio">
<SectionTitle>{t("home.portfolio.title")}</SectionTitle>
<ul className="flex w-full list-none flex-wrap justify-center gap-12 px-3">
{items.map((item) => {
return <PortfolioItem key={item.id} portfolioProject={item} />
})}
</ul>
</Section>
)
}

View File

@@ -0,0 +1,53 @@
import Image from "next/image"
import { Typography } from "../../Design/Typography/Typography"
import { SectionContent } from "../../Layout/Section/Section"
export interface PortfolioProject {
id: string
title: string
description: string
image: string
link: string
}
export interface PortfolioItemProps {
portfolioProject: PortfolioProject
}
export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
const { portfolioProject } = props
const { title, description, link, image } = portfolioProject
return (
<li>
<a
className="group inline-flex justify-center"
target="_blank"
href={link}
aria-label={title}
>
<SectionContent
className="relative cursor-pointer items-center p-0 sm:p-0"
shadowContainer
>
<div className="flex justify-center">
<Image
quality={100}
className="size-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
width={300}
height={300}
src={image}
alt={title}
/>
</div>
<div className="absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100">
<Typography variant="h4" as="h3" className="my-6">
{title}
</Typography>
<p className="mx-4 my-6 font-semibold">{description}</p>
</div>
</SectionContent>
</a>
</li>
)
}

View File

@@ -0,0 +1,51 @@
"use client"
import Image from "next/image"
import { useMemo } from "react"
import { Link } from "../../Design/Link/Link"
import { useTheme } from "../../Layout/Header/SwitchTheme"
import type { SkillName } from "./skills"
import { skills } from "./skills"
export interface SkillItemProps {
skillName: SkillName
}
export const SkillItem: React.FC<SkillItemProps> = (props) => {
const { skillName } = props
const skill = skills[skillName]
const { theme } = useTheme()
const skillImage = useMemo(() => {
if (typeof skill.image === "string") {
return skill.image
}
if (theme === "light") {
return skill.image.light
}
return skill.image.dark
}, [skill.image, theme])
return (
<li>
<Link
href={skill.link}
className="mx-2 max-w-xl flex-col items-center justify-center text-center"
target="_blank"
isExternal={false}
>
<Image
className="inline size-16"
quality={100}
width={64}
height={64}
alt={`Logo of ${skillName}`}
src={skillImage}
/>
<p className="mt-1 font-semibold">{skillName}</p>
</Link>
</li>
)
}

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { Skills as SkillsComponent } from "./Skills"
const meta = {
title: "Home/Skills",
component: SkillsComponent,
} satisfies Meta<typeof SkillsComponent>
export default meta
type Story = StoryObj<typeof meta>
export const Skills: Story = {
args: {},
}

View File

@@ -0,0 +1,45 @@
import { useTranslations } from "next-intl"
import { Section, SectionTitle } from "../../Layout/Section/Section"
import { SkillItem } from "./SkillItem"
import { SkillsSection } from "./SkillsSection"
export interface SkillsProps {}
export const Skills: React.FC<SkillsProps> = () => {
const t = useTranslations()
return (
<Section verticalSpacing horizontalSpacing id="skills">
<SectionTitle>{t("home.skills.title")}</SectionTitle>
<SkillsSection title={t("home.skills.programming-languages")}>
<SkillItem skillName="TypeScript" />
<SkillItem skillName="Python" />
<SkillItem skillName="C/C++" />
<SkillItem skillName="PHP" />
</SkillsSection>
<SkillsSection title={t("home.skills.frontend")}>
<SkillItem skillName="HTML" />
<SkillItem skillName="CSS" />
<SkillItem skillName="Tailwind CSS" />
<SkillItem skillName="React.js (+ Next.js)" />
</SkillsSection>
<SkillsSection title={t("home.skills.backend")}>
<SkillItem skillName="Laravel" />
<SkillItem skillName="Node.js" />
<SkillItem skillName="Fastify" />
<SkillItem skillName="PostgreSQL" />
</SkillsSection>
<SkillsSection title={t("home.skills.software-tools")}>
<SkillItem skillName="GNU/Linux" />
<SkillItem skillName="Arch Linux" />
<SkillItem skillName="Visual Studio Code" />
<SkillItem skillName="Git" />
<SkillItem skillName="Docker" />
</SkillsSection>
</Section>
)
}

View File

@@ -0,0 +1,25 @@
import { Typography } from "../../Design/Typography/Typography"
import { SectionContent } from "../../Layout/Section/Section"
export interface SkillsSectionProps extends React.PropsWithChildren {
title: string
}
export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
const { title, children } = props
return (
<section className="mb-12">
<SectionContent shadowContainer className="mx-auto w-full px-4 py-6">
<Typography
variant="h4"
as="h3"
className="mb-6 border-b border-black pb-3 dark:border-white"
>
{title}
</Typography>
<ul className="flex list-none flex-wrap justify-around">{children}</ul>
</SectionContent>
</section>
)
}

View File

@@ -0,0 +1,115 @@
export interface Skill {
link: string
image: string | { [key: string]: string }
}
export const skills = {
JavaScript: {
link: "https://developer.mozilla.org/docs/Web/JavaScript",
image: "/images/skills/JavaScript.webp",
},
TypeScript: {
link: "https://www.typescriptlang.org/",
image: "/images/skills/TypeScript.webp",
},
Python: {
link: "https://www.python.org/",
image: "/images/skills/Python.webp",
},
"C/C++": {
link: "https://isocpp.org/",
image: "/images/skills/C-Cpp.webp",
},
PHP: {
link: "https://www.php.net/",
image: "/images/skills/PHP.webp",
},
Laravel: {
link: "https://laravel.com/",
image: "/images/skills/Laravel.webp",
},
Dart: {
link: "https://dart.dev/",
image: "/images/skills/Dart.webp",
},
Flutter: {
link: "https://flutter.dev/",
image: "/images/skills/Flutter.webp",
},
HTML: {
link: "https://developer.mozilla.org/docs/Web/HTML",
image: "/images/skills/HTML.webp",
},
CSS: {
link: "https://developer.mozilla.org/docs/Web/CSS",
image: "/images/skills/CSS.webp",
},
"Tailwind CSS": {
link: "https://tailwindcss.com/",
image: "/images/skills/TailwindCSS.webp",
},
SASS: {
link: "https://sass-lang.com/",
image: "/images/skills/SASS.svg",
},
"React.js (+ Next.js)": {
link: "https://reactjs.org/",
image: "/images/skills/ReactJS.webp",
},
"Node.js": {
link: "https://nodejs.org/",
image: "/images/skills/NodeJS.webp",
},
Fastify: {
link: "https://www.fastify.io/",
image: {
light: "/images/skills/Fastify-light.webp",
dark: "/images/skills/Fastify-dark.webp",
},
},
Prisma: {
link: "https://www.prisma.io/",
image: {
light: "/images/skills/Prisma-light.webp",
dark: "/images/skills/Prisma-dark.webp",
},
},
PostgreSQL: {
link: "https://www.postgresql.org/",
image: "/images/skills/PostgreSQL.webp",
},
MySQL: {
link: "https://www.mysql.com/",
image: "/images/skills/MySQL.webp",
},
Strapi: {
link: "https://strapi.io/",
image: "/images/skills/Strapi.webp",
},
"Visual Studio Code": {
link: "https://code.visualstudio.com/",
image: "/images/skills/VisualStudioCode.webp",
},
Git: {
link: "https://git-scm.com/",
image: "/images/skills/Git.webp",
},
Ubuntu: {
link: "https://ubuntu.com/",
image: "/images/skills/Ubuntu.webp",
},
"Arch Linux": {
link: "https://archlinux.org/",
image: "/images/skills/ArchLinux.webp",
},
"GNU/Linux": {
link: "https://www.gnu.org/",
image: "/images/skills/GNU-Linux.webp",
},
Docker: {
link: "https://www.docker.com/",
image: "/images/skills/Docker.webp",
},
} as const
export type SkillName = keyof typeof skills