Initail_Commit
This commit is contained in:
17
src/app/about/about.module.scss
Normal file
17
src/app/about/about.module.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.hover {
|
||||
transition: var(--transition-property) var(--transition-duration-micro-medium) var(--transition-timing-function);
|
||||
|
||||
&:hover {
|
||||
transform: translateX(var(--static-space-4));
|
||||
}
|
||||
}
|
||||
|
||||
@media (--s) {
|
||||
.textAlign {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blockAlign {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
93
src/app/about/components/TableOfContents.tsx
Normal file
93
src/app/about/components/TableOfContents.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Flex, Text } from '@/once-ui/components';
|
||||
import styles from '@/app/about/about.module.scss';
|
||||
|
||||
interface TableOfContentsProps {
|
||||
structure: {
|
||||
title: string;
|
||||
display: boolean;
|
||||
items: string[];
|
||||
}[];
|
||||
about: {
|
||||
tableOfContent: {
|
||||
display: boolean;
|
||||
subItems: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const TableOfContents: React.FC<TableOfContentsProps> = ({ structure, about }) => {
|
||||
const scrollTo = (id: string, offset: number) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
const elementPosition = element.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + window.scrollY - offset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!about.tableOfContent.display) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
style={{
|
||||
left: '0',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
position="fixed"
|
||||
paddingLeft="24" gap="32"
|
||||
direction="column" hide="s">
|
||||
{structure
|
||||
.filter(section => section.display)
|
||||
.map((section, sectionIndex) => (
|
||||
<Flex key={sectionIndex} gap="12" direction="column">
|
||||
<Flex
|
||||
style={{ cursor: 'pointer' }}
|
||||
className={styles.hover}
|
||||
gap="8"
|
||||
alignItems="center"
|
||||
onClick={() => scrollTo(section.title, 80)}>
|
||||
<Flex
|
||||
height="1" width="16"
|
||||
background="neutral-strong">
|
||||
</Flex>
|
||||
<Text>
|
||||
{section.title}
|
||||
</Text>
|
||||
</Flex>
|
||||
{about.tableOfContent.subItems && (
|
||||
<>
|
||||
{section.items.map((item, itemIndex) => (
|
||||
<Flex
|
||||
key={itemIndex}
|
||||
style={{ cursor: 'pointer' }}
|
||||
className={styles.hover}
|
||||
gap="12" paddingLeft="24"
|
||||
alignItems="center"
|
||||
onClick={() => scrollTo(item, 80)}>
|
||||
<Flex
|
||||
height="1" width="8"
|
||||
background="neutral-strong">
|
||||
</Flex>
|
||||
<Text>
|
||||
{item}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableOfContents;
|
||||
426
src/app/about/page.tsx
Normal file
426
src/app/about/page.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { Avatar, Button, Flex, Heading, Icon, IconButton, SmartImage, Tag, Text } from '@/once-ui/components';
|
||||
import { person, about, social, baseURL } from '@/app/resources'
|
||||
import TableOfContents from '@/app/about/components/TableOfContents';
|
||||
import styles from '@/app/about/about.module.scss'
|
||||
|
||||
export function generateMetadata() {
|
||||
const title = about.title;
|
||||
const description = about.description;
|
||||
const ogImage = `https://${baseURL}/og?title=${encodeURIComponent(title)}`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: 'website',
|
||||
url: `https://${baseURL}/blog`,
|
||||
images: [
|
||||
{
|
||||
url: ogImage,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title,
|
||||
description,
|
||||
images: [ogImage],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const structure = [
|
||||
{
|
||||
title: about.intro.title,
|
||||
display: about.intro.display,
|
||||
items: []
|
||||
},
|
||||
{
|
||||
title: about.work.title,
|
||||
display: about.work.display,
|
||||
items: about.work.experiences.map(experience => experience.company)
|
||||
},
|
||||
{
|
||||
title: about.studies.title,
|
||||
display: about.studies.display,
|
||||
items: about.studies.institutions.map(institution => institution.name)
|
||||
},
|
||||
{
|
||||
title: about.technical.title,
|
||||
display: about.technical.display,
|
||||
items: about.technical.skills.map(skill => skill.title)
|
||||
},
|
||||
{
|
||||
title: about.certificates.title,
|
||||
display: about.certificates.display,
|
||||
items: about.certificates.certs.map(certs => certs.title)
|
||||
},
|
||||
]
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<Flex
|
||||
fillWidth maxWidth="m"
|
||||
direction="column">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: person.name,
|
||||
jobTitle: person.role,
|
||||
description: about.intro.description,
|
||||
url: `https://${baseURL}/about`,
|
||||
image: `${baseURL}/images/${person.avatar}`,
|
||||
sameAs: social
|
||||
.filter((item) => item.link && !item.link.startsWith('mailto:')) // Filter out empty links and email links
|
||||
.map((item) => item.link),
|
||||
worksFor: {
|
||||
'@type': 'Organization',
|
||||
name: about.work.experiences[0].company || ''
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{ about.tableOfContent.display && (
|
||||
<Flex
|
||||
style={{ left: '0', top: '50%', transform: 'translateY(-50%)' }}
|
||||
position="fixed"
|
||||
paddingLeft="24" gap="32"
|
||||
direction="column" hide="s">
|
||||
<TableOfContents
|
||||
structure={structure}
|
||||
about={about} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
fillWidth
|
||||
mobileDirection="column" justifyContent="center">
|
||||
{ about.avatar.display && (
|
||||
<Flex
|
||||
minWidth="160" paddingX="l" paddingBottom="xl" gap="m"
|
||||
flex={3} direction="column" alignItems="center">
|
||||
<Avatar
|
||||
src={person.avatar}
|
||||
size="xl"/>
|
||||
<Flex
|
||||
gap="8"
|
||||
alignItems="center">
|
||||
<Icon
|
||||
onBackground="accent-weak"
|
||||
name="globe"/>
|
||||
{person.location}
|
||||
</Flex>
|
||||
{ person.languages.length > 0 && (
|
||||
<Flex
|
||||
wrap
|
||||
gap="8">
|
||||
{person.languages.map((language, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
size="l">
|
||||
{language}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
className={styles.blockAlign}
|
||||
fillWidth flex={9} maxWidth={40} direction="column">
|
||||
<Flex
|
||||
id={about.intro.title}
|
||||
fillWidth minHeight="160"
|
||||
direction="column" justifyContent="center"
|
||||
marginBottom="32">
|
||||
{about.calendar.display && (
|
||||
<Flex
|
||||
className={styles.blockAlign}
|
||||
style={{
|
||||
backdropFilter: 'blur(var(--static-space-1))',
|
||||
border: '1px solid var(--brand-alpha-medium)',
|
||||
width: 'fit-content'
|
||||
}}
|
||||
alpha="brand-weak" radius="full"
|
||||
fillWidth padding="4" gap="8" marginBottom="m"
|
||||
alignItems="center">
|
||||
<Flex paddingLeft="12">
|
||||
<Icon
|
||||
name="calendar"
|
||||
onBackground="brand-weak"/>
|
||||
</Flex>
|
||||
<Flex
|
||||
paddingX="8">
|
||||
Schedule a call
|
||||
</Flex>
|
||||
<IconButton
|
||||
href={about.calendar.link}
|
||||
data-border="rounded"
|
||||
variant="tertiary"
|
||||
icon="chevronRight"/>
|
||||
</Flex>
|
||||
)}
|
||||
<Heading
|
||||
className={styles.textAlign}
|
||||
variant="display-strong-l">
|
||||
{person.name}
|
||||
</Heading>
|
||||
<Text
|
||||
className={styles.textAlign}
|
||||
variant="display-default-xs"
|
||||
onBackground="neutral-weak">
|
||||
{person.role}
|
||||
</Text>
|
||||
{social.length > 0 && (
|
||||
<Flex
|
||||
className={styles.blockAlign}
|
||||
paddingTop="20" paddingBottom="8" gap="8" wrap>
|
||||
{social.map((item) => (
|
||||
item.link && (
|
||||
<Button
|
||||
key={item.name}
|
||||
href={item.link}
|
||||
prefixIcon={item.icon}
|
||||
label={item.name}
|
||||
size="s"
|
||||
variant="tertiary"/>
|
||||
)
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{ about.intro.display && (
|
||||
<Flex
|
||||
direction="column"
|
||||
textVariant="body-default-l"
|
||||
fillWidth gap="m" marginBottom="xl">
|
||||
{about.intro.description}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{ about.work.display && (
|
||||
<>
|
||||
<Heading
|
||||
as="h2"
|
||||
id={about.work.title}
|
||||
variant="display-strong-s"
|
||||
marginBottom="m">
|
||||
{about.work.title}
|
||||
</Heading>
|
||||
<Flex
|
||||
direction="column"
|
||||
fillWidth gap="l" marginBottom="40">
|
||||
{about.work.experiences.map((experience, index) => (
|
||||
<Flex
|
||||
key={`${experience.company}-${experience.role}-${index}`}
|
||||
fillWidth
|
||||
direction="column">
|
||||
<Flex
|
||||
fillWidth
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
marginBottom="4">
|
||||
<Text
|
||||
id={experience.company}
|
||||
variant="heading-strong-l">
|
||||
{experience.company}
|
||||
</Text>
|
||||
<Text
|
||||
variant="heading-default-xs"
|
||||
onBackground="neutral-weak">
|
||||
{experience.timeframe}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text
|
||||
variant="body-default-s"
|
||||
onBackground="brand-weak"
|
||||
marginBottom="m">
|
||||
{experience.role}
|
||||
</Text>
|
||||
<Flex
|
||||
as="ul"
|
||||
direction="column" gap="16">
|
||||
{experience.achievements.map((achievement, index) => (
|
||||
<Text
|
||||
as="li"
|
||||
variant="body-default-m"
|
||||
key={`${experience.company}-${index}`}>
|
||||
{achievement}
|
||||
</Text>
|
||||
))}
|
||||
</Flex>
|
||||
{experience.images.length > 0 && (
|
||||
<Flex
|
||||
fillWidth paddingTop="m" paddingLeft="40"
|
||||
wrap>
|
||||
{experience.images.map((image, index) => (
|
||||
<Flex
|
||||
key={index}
|
||||
border="neutral-medium"
|
||||
borderStyle="solid-1"
|
||||
radius="m"
|
||||
minWidth={image.width} height={image.height}>
|
||||
<SmartImage
|
||||
enlarge
|
||||
radius="m"
|
||||
sizes={image.width.toString()}
|
||||
alt={image.alt}
|
||||
src={image.src}/>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ about.studies.display && (
|
||||
<>
|
||||
<Heading
|
||||
as="h2"
|
||||
id={about.studies.title}
|
||||
variant="display-strong-s"
|
||||
marginBottom="m">
|
||||
{about.studies.title}
|
||||
</Heading>
|
||||
<Flex
|
||||
direction="column"
|
||||
fillWidth gap="l" marginBottom="40">
|
||||
{about.studies.institutions.map((institution, index) => (
|
||||
<Flex
|
||||
key={`${institution.name}-${index}`}
|
||||
fillWidth gap="4"
|
||||
direction="column">
|
||||
<Text
|
||||
id={institution.name}
|
||||
variant="heading-strong-l">
|
||||
{institution.name}
|
||||
</Text>
|
||||
<Text
|
||||
variant="heading-default-xs"
|
||||
onBackground="neutral-weak">
|
||||
{institution.description}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ about.technical.display && (
|
||||
<>
|
||||
<Heading
|
||||
as="h2"
|
||||
id={about.technical.title}
|
||||
variant="display-strong-s" marginBottom="40">
|
||||
{about.technical.title}
|
||||
</Heading>
|
||||
<Flex
|
||||
direction="column"
|
||||
fillWidth gap="l" marginBottom="40">
|
||||
{about.technical.skills.map((skill, index) => (
|
||||
<Flex
|
||||
key={`${skill}-${index}`}
|
||||
fillWidth gap="4"
|
||||
direction="column">
|
||||
<Text
|
||||
variant="heading-strong-l">
|
||||
{skill.title}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body-default-m"
|
||||
onBackground="neutral-weak">
|
||||
{skill.description}
|
||||
</Text>
|
||||
{skill.images.length > 0 && (
|
||||
<Flex
|
||||
fillWidth paddingTop="m" gap="12"
|
||||
wrap>
|
||||
{skill.images.map((image, index) => (
|
||||
<Flex
|
||||
key={index}
|
||||
border="neutral-medium"
|
||||
borderStyle="solid-1"
|
||||
radius="m"
|
||||
minWidth={image.width} height={image.height}>
|
||||
<SmartImage
|
||||
enlarge
|
||||
radius="m"
|
||||
sizes={image.width.toString()}
|
||||
alt={image.alt}
|
||||
src={image.src}/>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ about.certificates.display && (
|
||||
<>
|
||||
<Heading
|
||||
as="h2"
|
||||
id={about.certificates.title}
|
||||
variant="display-strong-s" marginBottom="40">
|
||||
{about.certificates.title}
|
||||
</Heading>
|
||||
<Flex
|
||||
direction="column"
|
||||
fillWidth gap="l">
|
||||
{about.certificates.certs.map((cert, index) => (
|
||||
<Flex
|
||||
key={`${cert}-${index}`}
|
||||
fillWidth gap="4"
|
||||
direction="column">
|
||||
<Text
|
||||
variant="heading-strong-l">
|
||||
{cert.title}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body-default-m"
|
||||
onBackground="neutral-weak">
|
||||
{cert.description}
|
||||
</Text>
|
||||
{cert.images.length > 0 && (
|
||||
<Flex
|
||||
fillWidth paddingTop="m" gap="12"
|
||||
wrap>
|
||||
{cert.images.map((image, index) => (
|
||||
<Flex
|
||||
key={index}
|
||||
border="neutral-medium"
|
||||
borderStyle="solid-1"
|
||||
radius="m"
|
||||
minWidth={image.width} height={image.height}>
|
||||
<SmartImage
|
||||
enlarge
|
||||
radius="m"
|
||||
sizes={image.width.toString()}
|
||||
alt={image.alt}
|
||||
src={image.src}/>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user