1
0
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:
2024-08-19 23:08:50 +00:00
commit 3ec7d9a722
134 changed files with 17088 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
@import "../theme.scss";
.button
{
@include formBase;
cursor: pointer;
gap: $spacingSNudge;
justify-content: center;
text-align: left;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
&.iconBefore > :first-child,
&.iconAfter > :last-child
{
font-size: $fontSizeBase600;
height: $fontSizeBase600;
width: $fontSizeBase600;
}
&:not(.content)
{
min-width: 40px;
padding: $spacingXS;
justify-content: center;
}
&.secondary
{
border-color: transparent;
&:hover,
&:focus-visible
{
border: $strokeWidthThin solid $colorNeutralForeground1;
color: $colorNeutralForeground1;
}
}
&.primary
{
background-image: linear-gradient($colorNeutralBackgroundInverted, $colorNeutralBackgroundInverted);
&:not(:disabled, [disabled])
{
&:hover,
&:focus-visible
{
color: $colorNeutralForegroundInverted;
&:active
{
background-image: linear-gradient($colorNeutralForeground3Pressed, $colorNeutralForeground3Pressed);
color: $colorNeutralForegroundInverted2;
}
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
import Link, { LinkProps } from "next/link";
import React, { useMemo } from "react";
import cls from "./Button.module.scss";
const Button: React.FC<ButtonProps> = ({
as = "button",
iconAfter,
icon,
appearance = "primary",
children,
...props
}) =>
{
const Component = as === "button" && !props.href ?
"button" :
as === "next" ?
Link : "a";
const classList: string = useMemo(() =>
{
const list: string[] = [ cls.button, cls[appearance] ];
// We need these classes to differentiate content in CSS
if (icon)
list.push(cls.iconBefore);
if (iconAfter)
list.push(cls.iconAfter);
if (children)
list.push(cls.content);
if (props.className)
list.push(props.className);
return list.join(" ");
}, [appearance, children, icon, iconAfter, props.className]);
return (
<Component { ...props as any } className={ classList }>
{ icon }
{ children }
{ iconAfter }
</Component>
);
};
export default Button;
// Since we want to render button as both "a" and "button" (depending on the props) we do a little trick here
// Shorthand types
type HtmlButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "disabled">;
type HtmlAnchorProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "type">;
type NextLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> & LinkProps & {
children?: React.ReactNode;
} & React.RefAttributes<HTMLAnchorElement>;
type ButtonOrAnchorProps =
| ({ as?: "a"; href?: string; } & HtmlAnchorProps) // If href is present, it must be an <a>
| ({ as?: "button"; href?: undefined; } & HtmlButtonProps) // If href is absent, it is a <button>
| ({ as: "next"; } & NextLinkProps);
// Extend the common props
type CommonProps =
{
appearance?: "primary" | "secondary";
iconAfter?: React.ReactNode;
icon?: React.ReactNode;
disabled?: boolean;
children?: React.ReactNode;
};
export type ButtonProps = ButtonOrAnchorProps & CommonProps;
+81
View File
@@ -0,0 +1,81 @@
@import "../theme.scss";
.footer
{
@include flex(row);
@include align(flex-end, space-between);
.info
{
@include caption1;
@include flex(column);
gap: $spacingS;
padding: $spacingM;
color: $colorNeutralForeground2;
.copyright
{
@include body2($fontFamilyBaseAlt);
color: $colorNeutralForeground1;
}
.nextjs
{
@include flex(row);
gap: $spacingXS;
align-items: flex-end;
> a
{
padding: $spacingXXS;
padding-bottom: $spacingNone;
> img
{
height: 16px;
width: auto;
transition: filter $durationFast $curveEasyEaseMax;
}
&:hover,
&:focus-visible
{
> img
{
filter: invert(1);
}
}
}
}
}
.illustration
{
@include flex(column);
justify-content: flex-end;
align-self: flex-end;
height: 100px;
max-width: 100%;
z-index: -1;
> img
{
max-width: 100%;
width: auto;
max-height: 280px;
}
}
@media screen and (max-width: 1024px)
{
flex-flow: column;
align-items: flex-start;
.info
{
margin-right: 20%;
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import Package from "@/../package.json";
import { footerImage, nextjsLogo } from "@/_assets/illustrations";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import cls from "./Footer.module.scss";
const Footer: React.FC = () => (
<footer className={ cls.footer }>
<div className={ cls.info }>
<p className={ cls.copyright }>
{ `©${new Date().getFullYear()} ${Package.author.name}` }
</p>
<p>
This site was created with help of some third-party tools and services.
</p>
{ process.env.CLARITY_ID &&
<p>
This site is using Microsoft Clarity for analytics purposes.<br aria-hidden />
By using this site you agree that we and Microsoft can collect and use this data.
</p>
}
<p>
<Link href="/attribution" prefetch={ false }>
See license disclosure and privacy policy for more information
</Link>
</p>
<div className={ cls.nextjs }>
<span aria-hidden>Built with</span>
<a aria-label="Built with Next.js" href="https://nextjs.org/" target="_blank">
<Image aria-hidden src={ nextjsLogo.src } alt={ nextjsLogo.alt } />
</a>
</div>
</div>
<div className={ `${cls.illustration} illustration` }>
<Image src={ footerImage.src } alt={ footerImage.alt } />
</div>
</footer>
);
export default Footer;
+55
View File
@@ -0,0 +1,55 @@
@import "../theme.scss";
.header
{
position: fixed;
top: 0;
width: 100%;
z-index: 10;
@include flex(column);
background-color: $colorNeutralBackground1;
padding: $spacingS $spacingM;
// Header container
> div
{
@include maxCenter;
@include flex(row);
@include align(center, space-between);
gap: $spacingM;
.navigation
{
@media screen and (max-width: 1040px)
{
display: none;
}
}
.socials
{
flex-grow: 1;
justify-content: flex-end;
}
.sidemenu
{
@media screen and (min-width: 1041px)
{
display: none;
}
}
@media screen and (max-width: 640px)
{
.socials,
.resume
{
display: none;
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import TitleLogo from "@/_data/TitleLogo";
import links from "@/_data/links";
import React from "react";
import Button from "./Button";
import cls from "./Header.module.scss";
import NavigationLinks from "./NavigationLinks";
import Sidemenu from "./Sidemenu";
import SocialLinks from "./SocialLinks";
const Header: React.FC = () => (
<header className={ cls.header }>
<div>
<TitleLogo />
<NavigationLinks className={ cls.navigation } />
<SocialLinks className={ cls.socials } size={ 40 } />
<Button className={ cls.resume } as="next" href={ links.resume }>
Download resume
</Button>
<Sidemenu button={ { className: cls.sidemenu } } />
</div>
</header>
);
export default Header;
@@ -0,0 +1,37 @@
@import "../theme.scss";
.navigation
{
@include flex(row);
align-items: center;
.link
{
@include body2($fontFamilyBaseAlt);
color: inherit;
padding: $spacingSNudge $spacingM;
@include flex(column);
> i
{
height: $strokeWidthThick;
background-color: $colorNeutralForeground1;
border-radius: $borderRadiusSmall;
width: 0;
transition: width $durationNormal $curveEasyEaseMax;
}
&:hover, &:focus-visible
{
color: $colorNeutralForegroundInverted;
> i
{
width: 100%;
background-color: $colorNeutralForegroundInverted;
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import links from "@/_data/links";
import Link from "next/link";
import React from "react";
import cls from "./NavigationLinks.module.scss";
const navLinks: { text: string, href: string; }[] =
[
{ text: "Home", href: "/" },
{ text: "My skills", href: "/#skills" },
{ text: "Projects", href: "/#projects" },
{ text: "About", href: "/#about" },
{ text: "Contacts", href: "/#contacts" }
];
const NavigationLinks: React.FC<NavigationLinksProps> = ({ links: linkProps, ...props }) => (
<nav role="navigation" { ...props } className={ `${cls.navigation} ${props.className}` }>
{ navLinks.map((i, index) =>
<Link key={ index } { ...linkProps }
href={ i.href }
className={ `${cls.link} ${linkProps?.className ?? ""}` }>
{ i.text }
<i />
</Link>
) }
{ links.blog &&
<Link { ...linkProps } className={ `${cls.link} ${linkProps?.className ?? ""}` }
href={ links.blog } target="_blank">
Blog
<i />
</Link>
}
</nav>
);
export default NavigationLinks;
export type NavigationLinksProps = React.HTMLAttributes<HTMLDivElement> & {
links?: Omit<React.HTMLAttributes<HTMLAnchorElement>, "href">;
};
+74
View File
@@ -0,0 +1,74 @@
@import "../theme.scss";
.dialog
{
max-height: unset;
max-width: 320px;
width: 100%;
height: 100%;
padding: $spacingNone;
margin: $spacingNone;
margin-left: auto;
background-color: $colorNeutralBackground1;
box-shadow: $shadow16;
color: unset;
border: none;
&::backdrop
{
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
// Since colors use variables, and dialog is rendered outside of the regular DOM,
// we need to specify them as literals (or add variables to the dialog scope, but that's too complicated).
background-color: light-dark(rgba(255, 255, 255, 0.5), rgba(26, 26, 26, 0.5));
}
.wrapper
{
height: 100%;
@include flex(column);
@include align(flex-end, center);
gap: $spacingXXXL;
padding: $spacingXXXL;
> header
{
@include flex(row);
align-items: center;
gap: $spacingL;
> h3
{
@include subtitle1($fontFamilyBaseAlt);
}
}
.navigation
{
flex-flow: column;
align-items: flex-end;
.link
{
align-items: end;
background-position-x: 100%;
}
}
}
transition: right $durationNormal $curveEasyEaseMax;
right: -350px;
&.show
{
right: 0;
}
}
body:has(dialog.menu[open])
{
overflow: hidden;
}
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { Dismiss24Regular, Navigation24Regular } from "@fluentui/react-icons";
import React, { useCallback, useEffect, useRef, useState } from "react";
import Button, { ButtonProps } from "./Button";
import NavigationLinks from "./NavigationLinks";
import cls from "./Sidemenu.module.scss";
import SocialLinks from "./SocialLinks";
import links from "@/_data/links";
const Sidemenu: React.FC<SidemenuProps> = ({ button, ...panelProps }) =>
{
const [isOpen, setOpen] = useState<boolean>(false);
const dialogRef = useRef<HTMLDialogElement>(null);
const onCancel: React.ReactEventHandler<HTMLDialogElement> = useCallback((args) =>
{
args.preventDefault();
setOpen(false);
return true;
}, []);
// We use this method to enable user to close the menu by clicking ouside it.
const onClick: React.MouseEventHandler<HTMLDialogElement> = useCallback((args) =>
{
const wrapper = args.currentTarget.childNodes[0];
// If user clicked outside of the dialog boudaries, or clicked specifically on an anchor, we can close the menu
if (!wrapper.contains(args.target as Node) || args.target instanceof HTMLAnchorElement)
setOpen(false);
}, []);
useEffect(() =>
{
if (isOpen)
{
dialogRef.current?.showModal();
}
else if (dialogRef.current?.classList.contains(cls.show)) // This check is to prevent a bug when the menu is closed before opening
{
dialogRef.current?.addEventListener("transitionend", function WaitForClose()
{
dialogRef.current?.removeEventListener("transitionend", WaitForClose);
dialogRef.current?.close();
});
}
dialogRef.current?.classList.toggle(cls.show, isOpen);
}, [isOpen]);
return <>
<Button { ...button }
appearance="secondary"
title="Menu"
onClick={ () => setOpen(true) }
icon={ <Navigation24Regular /> } />
<dialog { ...panelProps } className={ `${cls.dialog} ${panelProps.className}` } ref={ dialogRef }
onCancel={ onCancel } onClick={ onClick }>
<div className={ cls.wrapper }>
<header>
<h3>Menu</h3>
<Button
appearance="secondary"
title="Close"
onClick={ () => setOpen(false) }
icon={ <Dismiss24Regular /> } />
</header>
<NavigationLinks className={ cls.navigation } links={ { className: cls.link } } />
<SocialLinks />
<Button className={ cls.resume } as="next" href={ links.resume }>Download resume</Button>
</div>
</dialog>
</>;
};
export default Sidemenu;
export type SidemenuProps = React.DialogHTMLAttributes<HTMLDialogElement> & {
button?: ButtonProps;
};
+64
View File
@@ -0,0 +1,64 @@
@import "../theme.scss";
.socials
{
@include flex(row);
align-items: center;
gap: $spacingS;
.link
{
background-image: none;
padding: $spacingNone;
border-radius: $borderRadiusCircular;
--bg-color: var(--network-color);
--icon-color: var(--colorNeutralForegroundStaticInverted);
// Icon
g:first-child
{
fill: var(--icon-color) !important;
}
// Mask
g:last-child
{
fill: var(--bg-color) !important;
}
&:hover,
&:focus-visible
{
--icon-color: var(--network-color);
--bg-color: transparent;
&:active
{
--bg-color: var(--colorNeutralBackground1Pressed);
}
}
// Since GitHub has dark brand color, we need to invert it in dark mode
&.github
{
@media (prefers-color-scheme: dark)
{
--bg-color: var(--colorNeutralForegroundStaticInverted);
--icon-color: var(--network-color);
&:hover,
&:focus-visible
{
--bg-color: transparent;
--icon-color: var(--colorNeutralForegroundStaticInverted);
&:active
{
--bg-color: var(--colorNeutralBackground1Pressed);
}
}
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
import socials from "@/_data/socials";
import React from "react";
import { SocialIcon, networkFor, social_icons } from "react-social-icons";
import cls from "./SocialLinks.module.scss";
const SocialLinks: React.FC<SocialLinksProps> = ({ size = 50, ...props }) => (
<div aria-label="Social links" { ...props } className={ `${cls.socials} ${props.className}` }>
{ Object.entries(socials).map(([network, i]) =>
<SocialIcon
key={ network } title={ network }
url={ i.href } target="_blank"
className={ `${cls.link} ${cls[networkFor(i.href)] ?? ""}` }
style={ {
// @ts-expect-error Inline variables are not supported in TS definition, but it works.
"--network-color": social_icons.get(networkFor(i.href))?.color,
width: size,
height: size
} } />
) }
</div>
);
export default SocialLinks;
export type SocialLinksProps = React.HTMLAttributes<HTMLDivElement> & {
size?: number;
};