Init hugo project
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["next/babel"],
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"styled-components",
|
|
||||||
{
|
|
||||||
"ssr": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
NEXT_PUBLIC_FORMSPREE_KEY=FORMSPREE_KEY
|
|
||||||
NEXT_PUBLIC_GOOGLE_ANALYTICS_KEY=GOOGLE_ANALYTICS_KEY
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Astrobuild'
|
|
||||||
description: 'Prototype of a collaboration tool between stakeholders for building software projects'
|
|
||||||
date: '2023-06-11'
|
|
||||||
demo: 'https://astrobuild.vercel.app'
|
|
||||||
code: 'https://github.com/hazemKrimi/astrobuild'
|
|
||||||
tags: ['react', 'typescript', 'graphql', 'styled-components', 'apollographql', 'vite', 'react-flow']
|
|
||||||
---
|
|
||||||
|
|
||||||
# Introduction
|
|
||||||
|
|
||||||
This was my final year of studies projects where my collegue and I worked on a prototype for a project building solution for the agency we had an internship in called [Astrolab](https://astrolab-agency.com).
|
|
||||||
As there were lockdowns in Tunisia in 2020/2021 because of covid software agencies and clients could not have in person meetings to discuss a client's software project.
|
|
||||||
So the idea of Astrobuild is to reduce meetings and make the client participate with other stakeholders in the process of creating a software project by tracking and communicating with their associated product owner.
|
|
||||||
|
|
||||||
# Features
|
|
||||||
|
|
||||||
### Client
|
|
||||||
- Account management
|
|
||||||
- Project creation from choosing the templates, features and deliverables (Specification, design, MVP or full build)
|
|
||||||
- Project tracking
|
|
||||||
- Chat with associated product owner for tracking and support
|
|
||||||
- Payment (WIP)
|
|
||||||
|
|
||||||
### Product Owner
|
|
||||||
- Account management
|
|
||||||
- Management of templates
|
|
||||||
- Review of projects and transferring deliverables
|
|
||||||
- Chat with clients on their projects
|
|
||||||
|
|
||||||
### Developer
|
|
||||||
- Account management
|
|
||||||
- Features, categories and wireframes management
|
|
||||||
|
|
||||||
### Admin
|
|
||||||
- Identity and access management (WIP)
|
|
||||||
|
|
||||||
# Technologies Used
|
|
||||||
The frontend project is a [React](https://react.dev) application with [TypeScript](https://www.typescriptlang.org) which consumes a set of [GraphQL](https://graphql.org) APIs using [Apollo GraphQL](https://www.apollographql.com) and a some REST APIs.
|
|
||||||
|
|
||||||
A small components library with a custom theme was made for this project using [Styled Components](https://styled-components.com) which can be viewed at the components folder.
|
|
||||||
|
|
||||||
The prototyping feature was done using [React Flow](https://reactflow.dev) which is a library for creating and interacting with diagrams.
|
|
||||||
|
|
||||||
To view the full architecture of the application go [here](https://github.com/MedAmineFouzai/astrobuild-api/blob/main/README.md).
|
|
||||||
|
|
||||||
# Screenshots
|
|
||||||
|
|
||||||
### Project page for the client
|
|
||||||

|
|
||||||
|
|
||||||
### Template page for the product owner
|
|
||||||

|
|
||||||
|
|
||||||
### Prototype page for the developer
|
|
||||||

|
|
||||||
|
|
||||||
### Support page for the product owner
|
|
||||||

|
|
||||||
|
|
||||||
### User editing page for the admin
|
|
||||||

|
|
||||||
|
|
||||||
# Credits
|
|
||||||
|
|
||||||
- Mohamed Amine Fouzai: [GitHub](https://github.com/MedAmineFouzai), [LinkedIn](https://www.linkedin.com/in/amine-fouzai)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'React Weather App'
|
|
||||||
description: 'Weather app made with React, TypeScript and OpenWeatherMap API'
|
|
||||||
date: '2021-09-19'
|
|
||||||
demo: 'https://hazemkrimi.github.io/react-weather-app'
|
|
||||||
code: 'https://github.com/hazemKrimi/react-weather-app'
|
|
||||||
tags: ['react', 'typescript', 'openweathermap']
|
|
||||||
---
|
|
||||||
|
|
||||||
# About the project
|
|
||||||
|
|
||||||
This is a project that I made as a step in the interview process for my final year internship.
|
|
||||||
|
|
||||||
# Technologies
|
|
||||||
|
|
||||||
- React
|
|
||||||
- TypeScript
|
|
||||||
- Styled Components
|
|
||||||
- OpenWeatherMap API
|
|
||||||
|
|
||||||
# Screenshots
|
|
||||||
|
|
||||||

|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
|
date: {{ .Date }}
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Props } from './types';
|
|
||||||
import { StyledButton } from './styles';
|
|
||||||
|
|
||||||
const Button = ({
|
|
||||||
variant = 'text',
|
|
||||||
href,
|
|
||||||
target,
|
|
||||||
onClick,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: Props) => (
|
|
||||||
<StyledButton
|
|
||||||
href={href}
|
|
||||||
target={target}
|
|
||||||
className={className}
|
|
||||||
onClick={onClick}
|
|
||||||
variant={variant}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Button;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { Props } from './types';
|
|
||||||
|
|
||||||
export const StyledButton = styled(Link)<Props>`
|
|
||||||
position: relative;
|
|
||||||
display: inline;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
color: var(--text);
|
|
||||||
border: ${({ variant }) =>
|
|
||||||
variant === 'outline' ? '2px solid var(--text)' : '2px solid transparent'};
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: ${({ variant }) =>
|
|
||||||
variant === 'outline' ? 'uppercase' : 'inherit'};
|
|
||||||
padding: ${({ variant }) => (variant === 'outline' ? '.5rem 1rem' : '0rem')};
|
|
||||||
text-align: left;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 250ms ease-in-out, border 250ms ease-in-out;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
padding: ${({ variant }) =>
|
|
||||||
variant === 'outline' ? '.5rem .75rem' : '0rem'};
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: -1;
|
|
||||||
background-color: ${({ variant }) =>
|
|
||||||
variant === 'outline' ? 'var(--text)' : 'inherit'};
|
|
||||||
transition: transform 250ms ease-in-out;
|
|
||||||
transform: scaleX(0);
|
|
||||||
transform-origin: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: ${({ variant }) =>
|
|
||||||
variant === 'outline' ? 'var(--background)' : 'inherit'};
|
|
||||||
border: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::before {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export type Props = {
|
|
||||||
variant?: 'outline' | 'text';
|
|
||||||
href: string;
|
|
||||||
target?: HTMLAnchorElement['target'];
|
|
||||||
onClick?: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import { StyledCard } from './styles';
|
|
||||||
import { Props } from './types';
|
|
||||||
|
|
||||||
const Card = ({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
image,
|
|
||||||
tags,
|
|
||||||
href,
|
|
||||||
target,
|
|
||||||
onClick,
|
|
||||||
}: Props) => {
|
|
||||||
return (
|
|
||||||
<StyledCard
|
|
||||||
href={href}
|
|
||||||
onClick={onClick}
|
|
||||||
image={image ? Boolean(image) : undefined}
|
|
||||||
target={target}
|
|
||||||
>
|
|
||||||
<div className='card-content'>
|
|
||||||
<h3>{title}</h3>
|
|
||||||
<p>{description}</p>
|
|
||||||
{tags && (
|
|
||||||
<div className='tags-wrapper'>
|
|
||||||
{tags.map((tag, index) => (
|
|
||||||
<span key={index}>#{tag} </span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{image && (
|
|
||||||
<div className='card-image'>
|
|
||||||
<Image alt={title} src={image} fill />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</StyledCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Card;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export const StyledCard = styled(Link)<{ image?: boolean }>`
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: ${({ image }) => (image ? 'auto 9.375rem' : 'auto')};
|
|
||||||
align-items: stretch;
|
|
||||||
transition: color 0ms ease-in-out;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
@media (max-width: 320px) {
|
|
||||||
grid-template-columns: ${({ image }) => (image ? 'auto 7.813rem' : 'auto')};
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1440px) {
|
|
||||||
grid-template-columns: ${({ image }) =>
|
|
||||||
image ? 'auto 15.625rem' : 'auto'};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
& > div {
|
|
||||||
background: ${({ theme }) => theme.colors.blue};
|
|
||||||
|
|
||||||
* {
|
|
||||||
color: ${({ theme }) => theme.colors.dark.text} !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: ${({ image }) => (image ? 'grayscale(80%)' : 'none')};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
background: var(--secondary-background);
|
|
||||||
display: grid;
|
|
||||||
row-gap: 0.5rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
padding: 0.75rem 0rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-image {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3,
|
|
||||||
p,
|
|
||||||
.tags-wrapper {
|
|
||||||
padding: 0rem 1rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
padding: 0rem 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
image?: string;
|
|
||||||
tags?: string[];
|
|
||||||
href: string;
|
|
||||||
target?: HTMLAnchorElement['target'];
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import Highlight, { defaultProps, Language } from 'prism-react-renderer';
|
|
||||||
import theme from 'prism-react-renderer/themes/vsDark';
|
|
||||||
import { Props } from './types';
|
|
||||||
import { Line, LineContent, LineNo, Pre } from './styles';
|
|
||||||
|
|
||||||
const CodeBlock = ({ children, className }: Props) => {
|
|
||||||
const language = className.replace(/language-/, '') as Language;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Highlight
|
|
||||||
{...defaultProps}
|
|
||||||
theme={theme}
|
|
||||||
code={(children as string).trim()}
|
|
||||||
language={language}
|
|
||||||
>
|
|
||||||
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
|
||||||
<Pre className={className} style={style}>
|
|
||||||
{tokens.map((line, i) => (
|
|
||||||
<Line key={i} {...getLineProps({ line, key: i })}>
|
|
||||||
<LineNo>{i + 1}</LineNo>
|
|
||||||
<LineContent>
|
|
||||||
{line.map((token, key) => (
|
|
||||||
<span key={key} {...getTokenProps({ token, key })} />
|
|
||||||
))}
|
|
||||||
</LineContent>
|
|
||||||
</Line>
|
|
||||||
))}
|
|
||||||
</Pre>
|
|
||||||
)}
|
|
||||||
</Highlight>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CodeBlock;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Pre = styled.pre`
|
|
||||||
text-align: left;
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.5em;
|
|
||||||
overflow: scroll;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Line = styled.div`
|
|
||||||
display: table-row;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const LineNo = styled.span`
|
|
||||||
display: table-cell;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 1em;
|
|
||||||
user-select: none;
|
|
||||||
opacity: 0.5;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const LineContent = styled.span`
|
|
||||||
display: table-cell;
|
|
||||||
`;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface Props {
|
|
||||||
className: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
width: 85%;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Container;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { ThemeContext } from '../../styles/theme';
|
|
||||||
import { StyledFooter } from './styles';
|
|
||||||
import IconButton from '../IconButton';
|
|
||||||
|
|
||||||
const Footer = () => {
|
|
||||||
const { mode } = useContext(ThemeContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledFooter>
|
|
||||||
<div className='contact'>
|
|
||||||
<IconButton
|
|
||||||
alt='GitHub'
|
|
||||||
icon={
|
|
||||||
mode === 'dark'
|
|
||||||
? '/icons/light-github.svg'
|
|
||||||
: '/icons/dark-github.svg'
|
|
||||||
}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
href='https://github.com/hazemKrimi'
|
|
||||||
target='_blank'
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
alt='Twitter'
|
|
||||||
icon={
|
|
||||||
mode === 'dark'
|
|
||||||
? '/icons/light-twitter.svg'
|
|
||||||
: '/icons/dark-twitter.svg'
|
|
||||||
}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
href='https://twitter.com/HazemKrimi'
|
|
||||||
target='_blank'
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
alt='LinkedIn'
|
|
||||||
icon={
|
|
||||||
mode === 'dark'
|
|
||||||
? '/icons/light-linkedin.svg'
|
|
||||||
: '/icons/dark-linkedin.svg'
|
|
||||||
}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
href='https://linkedin.com/in/hazemkrimi'
|
|
||||||
target='_blank'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p>Hazem Krimi © {new Date().getFullYear()}</p>
|
|
||||||
</StyledFooter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const StyledFooter = styled.footer`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
min-height: 100px;
|
|
||||||
width: 85%;
|
|
||||||
margin: auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
column-gap: 2rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-content: center;
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, 16px);
|
|
||||||
column-gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
* {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
display: inline;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { createGlobalStyle } from 'styled-components';
|
|
||||||
|
|
||||||
const GlobalStyles = createGlobalStyle`
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: 'Source Code Pro', monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
outline: none;
|
|
||||||
user-select: text;
|
|
||||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
|
||||||
|
|
||||||
@media(max-width: 768px) {
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::selection {
|
|
||||||
background: var(--text);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
position: relative;
|
|
||||||
min-height: 100%;
|
|
||||||
background: var(--background) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin-inline-start: 1.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0 0 100px;
|
|
||||||
transition: color 250ms ease-in-out, background 250ms ease-in-out;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
|
|
||||||
#nprogress .bar {
|
|
||||||
background: var(--text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nprogress .peg {
|
|
||||||
box-shadow: 0 0 10px var(--text), 0 0 5px var(--text) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body::-webkit-scrollbar {
|
|
||||||
width: 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--text) !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default GlobalStyles;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Wrapper } from './styles';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
const Hero = () => (
|
|
||||||
<Wrapper>
|
|
||||||
<div className='intro'>
|
|
||||||
<h2>Hi, I am Hazem</h2>
|
|
||||||
<h2>I Like Building Software</h2>
|
|
||||||
<h2 className='blue'>Full Stack TypeScript Developer</h2>
|
|
||||||
<h2 className='blue'>Life Long Learner</h2>
|
|
||||||
</div>
|
|
||||||
<div className='photo'>
|
|
||||||
<Image alt='Hazem Krimi' src='/photo.jpg' width={515} height={535} />
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Hero;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
min-height: 45vh;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 32.188rem;
|
|
||||||
align-items: center;
|
|
||||||
height: auto;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
min-height: 35vh;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
|
|
||||||
@media (min-width: 1440px) {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 2560px) {
|
|
||||||
font-size: 3.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue {
|
|
||||||
color: ${({ theme }) => theme.colors.blue};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import { Props } from './types';
|
|
||||||
import { StyledButton, StyledLink } from './styles';
|
|
||||||
|
|
||||||
const IconButton = ({
|
|
||||||
alt,
|
|
||||||
icon,
|
|
||||||
href,
|
|
||||||
target,
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
width = 24,
|
|
||||||
height = 24,
|
|
||||||
}: Props) =>
|
|
||||||
href ? (
|
|
||||||
<StyledLink
|
|
||||||
href={href}
|
|
||||||
target={target}
|
|
||||||
onClick={onClick}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
<Image alt={alt} src={icon} width={width} height={height} />
|
|
||||||
</StyledLink>
|
|
||||||
) : (
|
|
||||||
<StyledButton onClick={onClick} className={className}>
|
|
||||||
<Image alt={alt} src={icon} width={width} height={height} />
|
|
||||||
</StyledButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default IconButton;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import styled, { css } from 'styled-components';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
const sharedStyles = css`
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledLink = styled(Link)`
|
|
||||||
${sharedStyles}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledButton = styled.button`
|
|
||||||
${sharedStyles}
|
|
||||||
`;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export interface Props {
|
|
||||||
alt: string;
|
|
||||||
icon: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
href?: string;
|
|
||||||
target?: HTMLAnchorElement['target'];
|
|
||||||
onClick?: () => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { BigField, SmallField } from './styles';
|
|
||||||
import { Props } from './types';
|
|
||||||
|
|
||||||
const Input = ({
|
|
||||||
type = 'text',
|
|
||||||
variant = 'small',
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
required,
|
|
||||||
placeholder,
|
|
||||||
className,
|
|
||||||
onChange,
|
|
||||||
}: Props) => {
|
|
||||||
return variant === 'small' ? (
|
|
||||||
<SmallField
|
|
||||||
type={type}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
required={required}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className={className}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<BigField
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
required={required}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className={className}
|
|
||||||
onChange={onChange}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Input;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const SmallField = styled.input`
|
|
||||||
border: none;
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--secondary-background);
|
|
||||||
color: var(--text);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const BigField = styled.textarea`
|
|
||||||
resize: none;
|
|
||||||
border: none;
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--secondary-background);
|
|
||||||
color: var(--text);
|
|
||||||
`;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export interface Props {
|
|
||||||
placeholder?: string;
|
|
||||||
type: 'text' | 'email';
|
|
||||||
variant: 'small' | 'big';
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
required?: boolean;
|
|
||||||
onChange?: (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
||||||
) => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { ThemeContext } from '../../styles/theme';
|
|
||||||
import { Props } from './types';
|
|
||||||
import { StyledLink, StyledButton } from './styles';
|
|
||||||
|
|
||||||
const MDXButton = ({
|
|
||||||
variant = 'text',
|
|
||||||
type = 'button',
|
|
||||||
link,
|
|
||||||
target,
|
|
||||||
children,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
}: Props) => {
|
|
||||||
const { mode } = useContext(ThemeContext);
|
|
||||||
|
|
||||||
return link ? (
|
|
||||||
<StyledLink
|
|
||||||
href={link}
|
|
||||||
target={target}
|
|
||||||
variant={variant}
|
|
||||||
type={type}
|
|
||||||
mode={mode}
|
|
||||||
disabled={disabled}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledLink>
|
|
||||||
) : (
|
|
||||||
<StyledButton
|
|
||||||
variant={variant}
|
|
||||||
type={type}
|
|
||||||
mode={mode}
|
|
||||||
disabled={disabled}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MDXButton;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import styled, { css } from 'styled-components';
|
|
||||||
import { Props } from './types';
|
|
||||||
|
|
||||||
export const sharedStyles = css<Props>`
|
|
||||||
cursor: pointer;
|
|
||||||
display: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string) ? 'block' : 'inline'};
|
|
||||||
width: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string) ? '100%' : 'auto'};
|
|
||||||
background: ${({ variant }) => (variant === 'action' ? '#1573CA' : 'none')};
|
|
||||||
color: ${({ variant, mode }) =>
|
|
||||||
variant === 'action' ? 'white' : mode === 'dark' ? 'white' : 'black'};
|
|
||||||
border: ${({ variant, mode }) =>
|
|
||||||
variant === 'outline'
|
|
||||||
? `2px solid ${mode === 'dark' ? 'white' : 'black'}`
|
|
||||||
: 'none'};
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string) ? '1.05rem' : 'inherit'};
|
|
||||||
text-transform: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string)
|
|
||||||
? 'uppercase'
|
|
||||||
: 'inherit'};
|
|
||||||
padding: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string) ? '.5rem 1rem' : '0rem'};
|
|
||||||
text-align: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string) ? 'center' : 'left'};
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 250ms ease-in-out;
|
|
||||||
|
|
||||||
${({ disabled }) =>
|
|
||||||
disabled &&
|
|
||||||
`
|
|
||||||
background: gray;
|
|
||||||
cursor: default;
|
|
||||||
`}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
padding: ${({ variant }) =>
|
|
||||||
['action', 'outline'].includes(variant as string)
|
|
||||||
? '.5rem .75rem'
|
|
||||||
: '0rem'};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledLink = styled(Link)<Props>`
|
|
||||||
${sharedStyles}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledButton = styled.button<Omit<Props, 'href'>>`
|
|
||||||
${sharedStyles}
|
|
||||||
`;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export type Props = {
|
|
||||||
variant?: 'outline' | 'text' | 'action';
|
|
||||||
type?: 'button' | 'submit';
|
|
||||||
link?: string;
|
|
||||||
target?: HTMLAnchorElement['target'];
|
|
||||||
mode?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { useContext, useRef, useEffect } from 'react';
|
|
||||||
import { ThemeContext } from '../../styles/theme';
|
|
||||||
import { Props } from './types';
|
|
||||||
import { Bar } from './styles';
|
|
||||||
import IconButton from '../IconButton';
|
|
||||||
import Button from '../Button';
|
|
||||||
|
|
||||||
const MobileNav = ({ open, close }: Props) => {
|
|
||||||
const { mode, toggle } = useContext(ThemeContext);
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('mousedown', (event: MouseEvent) => {
|
|
||||||
if (ref.current && ref.current.contains(event.target as Node)) {
|
|
||||||
document.addEventListener('mouseup', (event) => {
|
|
||||||
if (ref.current && !ref.current.contains(event.target as Node))
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
document.addEventListener('mouseup', (event) => {
|
|
||||||
if (ref.current && !ref.current.contains(event.target as Node))
|
|
||||||
close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', () => {});
|
|
||||||
document.removeEventListener('mouseup', () => {});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bar open={open} ref={ref}>
|
|
||||||
<div className='close'>
|
|
||||||
<IconButton
|
|
||||||
alt='Theme toggler'
|
|
||||||
icon={
|
|
||||||
mode === 'dark' ? '/icons/dark-close.svg' : '/icons/light-close.svg'
|
|
||||||
}
|
|
||||||
onClick={close}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='mobile-button-wrapper'>
|
|
||||||
<Button
|
|
||||||
href='#'
|
|
||||||
onClick={() => {
|
|
||||||
toggle();
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='mobile-button-wrapper'>
|
|
||||||
<Button href='/projects' onClick={() => close()}>
|
|
||||||
Projects
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='mobile-button-wrapper'>
|
|
||||||
<Button href='/blog' onClick={() => close()}>
|
|
||||||
Blog
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='mobile-button-wrapper'>
|
|
||||||
<Button href='/contact' onClick={() => close()}>
|
|
||||||
Contact
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Bar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MobileNav;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import { StyledProps } from './types';
|
|
||||||
|
|
||||||
export const Bar = styled.nav<StyledProps>`
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
transform-origin: right;
|
|
||||||
transform: ${({ open }) => (open ? 'translateX(0%)' : 'translateX(100%)')};
|
|
||||||
width: 80%;
|
|
||||||
height: 100vh;
|
|
||||||
background: var(--text);
|
|
||||||
transition: transform 250ms ease-in-out;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 30% repeat(4, 50px);
|
|
||||||
padding: 1rem 1rem 5rem 1rem;
|
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
|
||||||
grid-template-rows: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
justify-self: flex-end;
|
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-button-wrapper {
|
|
||||||
display: flex;
|
|
||||||
margin: 0rem 1rem;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--text-inverted) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export type Props = {
|
|
||||||
open: boolean;
|
|
||||||
close: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StyledProps = {
|
|
||||||
open: boolean;
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useContext, useState } from 'react';
|
|
||||||
import { ThemeContext } from '../../styles/theme';
|
|
||||||
import { Bar } from './styles';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Button from '../Button';
|
|
||||||
import IconButton from '../IconButton';
|
|
||||||
import MobileNav from '../MobileNav';
|
|
||||||
|
|
||||||
const Nav = () => {
|
|
||||||
const [mobileNavOpen, setMobileNavOpen] = useState<boolean>(false);
|
|
||||||
const { mode, toggle } = useContext(ThemeContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bar>
|
|
||||||
<Link className='logo' href='/'>
|
|
||||||
<Image
|
|
||||||
className='logo-image'
|
|
||||||
src={mode === 'dark' ? '/light-logo.svg' : '/dark-logo.svg'}
|
|
||||||
alt='Logo Image'
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
/>
|
|
||||||
<h1>Hazem Krimi</h1>
|
|
||||||
</Link>
|
|
||||||
<div className='buttons'>
|
|
||||||
<IconButton
|
|
||||||
alt='Theme toggler'
|
|
||||||
icon={mode === 'dark' ? '/icons/sun.svg' : '/icons/moon.svg'}
|
|
||||||
onClick={toggle}
|
|
||||||
/>
|
|
||||||
<Button href='/projects'>Projects</Button>
|
|
||||||
<Button href='/blog'>Blog</Button>
|
|
||||||
<Button href='/contact'>Contact</Button>
|
|
||||||
<Button href='/resume.pdf' target='_blank' variant='outline'>
|
|
||||||
Resume
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className='mobile-buttons'>
|
|
||||||
<Button href='/resume.pdf' target='_blank' variant='outline'>
|
|
||||||
Resume
|
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
alt='Hamburger menu'
|
|
||||||
icon={
|
|
||||||
mode === 'dark' ? '/icons/light-menu.svg' : '/icons/dark-menu.svg'
|
|
||||||
}
|
|
||||||
onClick={() => setMobileNavOpen(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<MobileNav open={mobileNavOpen} close={() => setMobileNavOpen(false)} />
|
|
||||||
</Bar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Nav;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Bar = styled.nav`
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
|
|
||||||
* {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div,
|
|
||||||
a.logo {
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 1rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.logo {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text);
|
|
||||||
cursor: pointer;
|
|
||||||
grid-template-columns: repeat(2, auto);
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
grid-template-columns: repeat(5, auto);
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-buttons {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, auto);
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
baseURL = 'http://hazemkrimi.tech/'
|
||||||
|
languageCode = 'en-us'
|
||||||
|
title = 'Hazem Krimi'
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
declare module '@mdx-js/react' {
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
export type Components = {
|
|
||||||
[key]?: React.ComponentType<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface MDXProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
components?: Components;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MDXProvider extends React.Component<MDXProviderProps> {}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const withMDX = require('@next/mdx')({
|
|
||||||
extension: /\.mdx?$/,
|
|
||||||
options: {
|
|
||||||
providerImportSource: '@mdx-js/react',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = withMDX({
|
|
||||||
pageExtensions: ['ts', 'tsx', 'md', 'mdx'],
|
|
||||||
images: {
|
|
||||||
domains: ['res.cloudinary.com'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "personal-website",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"format": "prettier --write ./**/*.{js,jsx,ts,tsx}"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@formspree/react": "^2.4.1",
|
|
||||||
"@mdx-js/loader": "^2.3.0",
|
|
||||||
"@next/mdx": "^13.2.4",
|
|
||||||
"gray-matter": "^4.0.3",
|
|
||||||
"next": "^13.2.4",
|
|
||||||
"next-mdx-remote": "^4.4.1",
|
|
||||||
"nprogress": "^0.2.0",
|
|
||||||
"prism-react-renderer": "^1.3.5",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"reading-time": "^1.5.0",
|
|
||||||
"styled-components": "^5.3.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^18.15.3",
|
|
||||||
"@types/nprogress": "^0.2.0",
|
|
||||||
"@types/react": "^18.0.28",
|
|
||||||
"@types/styled-components": "^5.1.26",
|
|
||||||
"babel-plugin-styled-components": "^2.0.7",
|
|
||||||
"babel-runtime": "^6.26.0",
|
|
||||||
"prettier": "^2.8.8",
|
|
||||||
"typescript": "^5.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.x"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { useRouter } from 'next/router';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import IconButton from '../components/IconButton';
|
|
||||||
import { Wrapper } from '../styles/404';
|
|
||||||
|
|
||||||
const NotFound = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<meta property='og:title' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content='Hazem, Krimi, Hazem Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, TypeScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News, Software Developer, Software Engineer, Full Stack TypeScript Developer, Next.js'
|
|
||||||
/>
|
|
||||||
<title>404 Not Found | Hazem Krimi</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<h1>404: This page could not be found</h1>
|
|
||||||
<div className='back' onClick={() => router.push('/')}>
|
|
||||||
<IconButton alt='Back' icon='/icons/arrow-left.svg' />
|
|
||||||
<span>Go Home</span>
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotFound;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import type { AppProps } from 'next/app';
|
|
||||||
import Script from 'next/script';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
import Nav from '../components/Nav';
|
|
||||||
import Theme from '../styles/theme';
|
|
||||||
import Container from '../components/Container';
|
|
||||||
import GlobalStyles from '../components/GlobalStyles';
|
|
||||||
import SharedStyles from '../styles/shared';
|
|
||||||
import Footer from '../components/Footer';
|
|
||||||
|
|
||||||
import NProgress from 'nprogress';
|
|
||||||
import 'nprogress/nprogress.css';
|
|
||||||
|
|
||||||
import { pageview } from '../utils/gtag';
|
|
||||||
import { initStyles } from '../utils/styles';
|
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.events.on('routeChangeStart', () => {
|
|
||||||
NProgress.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.events.on('routeChangeComplete', (url) => {
|
|
||||||
NProgress.done();
|
|
||||||
pageview(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.events.on('routeChangeError', () => {
|
|
||||||
NProgress.done();
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
router.events.off('routeChangeComplete', (url) => {
|
|
||||||
pageview(url);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [router.events]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Script
|
|
||||||
id='styles-init'
|
|
||||||
strategy='afterInteractive'
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: initStyles(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Theme>
|
|
||||||
<SharedStyles>
|
|
||||||
<GlobalStyles />
|
|
||||||
<Container>
|
|
||||||
<Nav />
|
|
||||||
<Component {...pageProps} />
|
|
||||||
<Footer />
|
|
||||||
</Container>
|
|
||||||
</SharedStyles>
|
|
||||||
</Theme>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import Document, {
|
|
||||||
DocumentContext,
|
|
||||||
Html,
|
|
||||||
Head,
|
|
||||||
Main,
|
|
||||||
NextScript,
|
|
||||||
} from 'next/document';
|
|
||||||
|
|
||||||
import { ServerStyleSheet } from 'styled-components';
|
|
||||||
|
|
||||||
import { GOOGLE_ANALYTICS_KEY, initAnalytics } from '../utils/gtag';
|
|
||||||
import { initStyles } from '../utils/styles';
|
|
||||||
|
|
||||||
class Doc extends Document {
|
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
|
||||||
const sheet = new ServerStyleSheet();
|
|
||||||
const originalRenderPage = ctx.renderPage;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ctx.renderPage = () =>
|
|
||||||
originalRenderPage({
|
|
||||||
enhanceApp: (App) => (props) =>
|
|
||||||
sheet.collectStyles(<App {...props} />),
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
|
||||||
return {
|
|
||||||
...initialProps,
|
|
||||||
styles: (
|
|
||||||
<>
|
|
||||||
{initialProps.styles}
|
|
||||||
{sheet.getStyleElement()}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
sheet.seal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head>
|
|
||||||
<link
|
|
||||||
rel='shortcut icon'
|
|
||||||
href='/light-favicon.png'
|
|
||||||
id='light-favicon'
|
|
||||||
></link>
|
|
||||||
<link
|
|
||||||
rel='shortcut icon'
|
|
||||||
href='/dark-favicon.png'
|
|
||||||
id='dark-favicon'
|
|
||||||
></link>
|
|
||||||
<link rel='preconnect' href='https://fonts.gstatic.com' />
|
|
||||||
<link
|
|
||||||
href='https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap'
|
|
||||||
rel='stylesheet'
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
src={`https://www.googletagmanager.com/gtag/js?id=${GOOGLE_ANALYTICS_KEY}`}
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
id='analytics-init'
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: initAnalytics(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
id='styles-init'
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: initStyles(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Doc;
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { getBlogPostsSlugs, getBlogPostdata } from '../../utils/blog';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote';
|
|
||||||
import { MDXProvider } from '@mdx-js/react';
|
|
||||||
import { GetStaticPaths, GetStaticProps } from 'next';
|
|
||||||
import { serialize } from 'next-mdx-remote/serialize';
|
|
||||||
import { Wrapper } from '../../styles/blog/slug';
|
|
||||||
import matter from 'gray-matter';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import IconButton from '../../components/IconButton';
|
|
||||||
import CodeBlock from '../../components/CodeBlock';
|
|
||||||
import readingTime from 'reading-time';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
source: MDXRemoteSerializeResult;
|
|
||||||
frontMatter: any;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BlogPost = ({ source, frontMatter, text }: Props) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const stats = readingTime(text);
|
|
||||||
const htmlOverrides = { code: CodeBlock };
|
|
||||||
const mdxComponents = {};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content={
|
|
||||||
frontMatter?.description
|
|
||||||
? frontMatter.description
|
|
||||||
: `Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content={
|
|
||||||
frontMatter?.description
|
|
||||||
? frontMatter.description
|
|
||||||
: `Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property='og:title'
|
|
||||||
content={`${frontMatter?.title} | Hazem Krimi`}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content={
|
|
||||||
frontMatter?.tags
|
|
||||||
? frontMatter.tags.join(' ')
|
|
||||||
: `Hazem, Krimi, Hazem Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, TypeScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News, Software Developer, Software Engineer, Full Stack TypeScript Developer, Next.js`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<title>{`${frontMatter?.title} | Hazem Krimi`}</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<div className='meta'>
|
|
||||||
<div className='back' onClick={() => router.back()}>
|
|
||||||
<IconButton alt='Back' icon='/icons/arrow-left.svg' />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h1>{frontMatter?.title}</h1>
|
|
||||||
<p>{frontMatter?.description}</p>
|
|
||||||
<p>
|
|
||||||
By <b>{frontMatter?.author}</b> on <b>{frontMatter?.date}</b> (
|
|
||||||
{stats.text})
|
|
||||||
</p>
|
|
||||||
{frontMatter?.tags ? (
|
|
||||||
<div className='tags-wrapper'>
|
|
||||||
{frontMatter.tags.map((tag: string, index: number) => (
|
|
||||||
<span key={index}>#{tag} </span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{frontMatter?.image ? (
|
|
||||||
<div className='image'>
|
|
||||||
<Image alt={frontMatter?.title} src={frontMatter?.image} fill />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
<MDXProvider components={{ ...htmlOverrides, ...mdxComponents }}>
|
|
||||||
<div className='content'>
|
|
||||||
<MDXRemote {...source} />
|
|
||||||
</div>
|
|
||||||
</MDXProvider>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlogPost;
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
const paths = getBlogPostsSlugs();
|
|
||||||
return {
|
|
||||||
paths,
|
|
||||||
fallback: false,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const getStaticProps: GetStaticProps = async ({ params }: any) => {
|
|
||||||
const blogPostContent = await getBlogPostdata(params.slug);
|
|
||||||
|
|
||||||
if (!blogPostContent)
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
source: undefined,
|
|
||||||
frontMatter: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, content } = matter(blogPostContent);
|
|
||||||
const mdxSource = await serialize(content, {
|
|
||||||
scope: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
source: mdxSource,
|
|
||||||
frontMatter: data,
|
|
||||||
text: content,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { useRouter } from 'next/router';
|
|
||||||
import { getBlogPosts } from '../../utils/blog';
|
|
||||||
import { Wrapper } from '../../styles/blog';
|
|
||||||
import Card from '../../components/Card';
|
|
||||||
import IconButton from '../../components/IconButton';
|
|
||||||
import Head from 'next/head';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
blogPosts: {
|
|
||||||
title: string;
|
|
||||||
author: string;
|
|
||||||
description: string;
|
|
||||||
image?: string;
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
tags?: string[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Index = ({ blogPosts }: Props) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content='Hazem Krimi is a Full Stack JavaScript Developer and a Software Engineering Enthusiast'
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech/blog' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content='Hazem Krimi is a Full Stack JavaScript Developer and a Software Engineering Enthusiast'
|
|
||||||
/>
|
|
||||||
<meta property='og:title' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content='Hazem, Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News'
|
|
||||||
/>
|
|
||||||
<title>Blog | Hazem Krimi</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<div className='back' onClick={() => router.back()}>
|
|
||||||
<IconButton alt='Back' icon='/icons/arrow-left.svg' />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h1>Blog</h1>
|
|
||||||
<div className='articles-wrapper'>
|
|
||||||
{blogPosts.length !== 0 ? (
|
|
||||||
blogPosts.map(({ slug, ...rest }) => (
|
|
||||||
<Card {...rest} key={slug} href={`/blog/${slug}`} />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<h4>Nothing for now</h4>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
|
||||||
const blogPosts = getBlogPosts();
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
blogPosts,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useForm, ValidationError } from '@formspree/react';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import { Wrapper } from '../styles/contact';
|
|
||||||
import Input from '../components/Input';
|
|
||||||
import MDXButton from '../components/MDXButton';
|
|
||||||
|
|
||||||
const About = () => {
|
|
||||||
const [form, setForm] = useState<{
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
message: string;
|
|
||||||
}>({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
message: '',
|
|
||||||
});
|
|
||||||
const [state, Submit] = useForm(`${process.env.NEXT_PUBLIC_FORMSPREE_KEY}`);
|
|
||||||
const [submitted, setSubmitted] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
||||||
) => {
|
|
||||||
setForm({ ...form, [event.target.name]: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
try {
|
|
||||||
await Submit(event);
|
|
||||||
setSubmitted(true);
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => setSubmitted(false), 1000);
|
|
||||||
setForm({ name: '', email: '', message: '' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<meta property='og:title' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content='Hazem, Krimi, Hazem Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, TypeScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News, Software Developer, Software Engineer, Full Stack TypeScript Developer, Next.js'
|
|
||||||
/>
|
|
||||||
<title>Contact | Hazem Krimi</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<div className='content'>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
Contact Me{' '}
|
|
||||||
{submitted && <span className='success'>Message sent ✔️</span>}
|
|
||||||
</h1>
|
|
||||||
<form className='contact' onSubmit={handleSubmit}>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
placeholder='Name'
|
|
||||||
variant='small'
|
|
||||||
name='name'
|
|
||||||
required
|
|
||||||
value={form.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<ValidationError
|
|
||||||
className='error'
|
|
||||||
prefix='Name'
|
|
||||||
field='name'
|
|
||||||
errors={state.errors}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
placeholder='Email'
|
|
||||||
variant='small'
|
|
||||||
name='email'
|
|
||||||
required
|
|
||||||
value={form.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<ValidationError
|
|
||||||
className='error'
|
|
||||||
prefix='Email'
|
|
||||||
field='email'
|
|
||||||
errors={state.errors}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
placeholder='Message'
|
|
||||||
variant='big'
|
|
||||||
name='message'
|
|
||||||
required
|
|
||||||
value={form.message}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<ValidationError
|
|
||||||
className='error'
|
|
||||||
prefix='Message'
|
|
||||||
field='message'
|
|
||||||
errors={state.errors}
|
|
||||||
/>
|
|
||||||
<MDXButton
|
|
||||||
type='submit'
|
|
||||||
variant='action'
|
|
||||||
disabled={state.submitting || submitted}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</MDXButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default About;
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { getProjects } from '../utils/projects';
|
|
||||||
import { getBlogPosts } from '../utils/blog';
|
|
||||||
import { GetStaticProps } from 'next';
|
|
||||||
import { Wrapper } from '../styles/home';
|
|
||||||
import Hero from '../components/Hero';
|
|
||||||
import Button from '../components/Button';
|
|
||||||
import Card from '../components/Card';
|
|
||||||
import Head from 'next/head';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
blogPosts: {
|
|
||||||
title: string;
|
|
||||||
author: string;
|
|
||||||
description: string;
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
tags?: string[];
|
|
||||||
}[];
|
|
||||||
projects: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
tags?: string[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Index = ({ blogPosts, projects }: Props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<meta property='og:title' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content='Hazem, Krimi, Hazem Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, TypeScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News, Software Developer, Software Engineer, Full Stack TypeScript Developer, Next.js'
|
|
||||||
/>
|
|
||||||
<title>Hazem Krimi</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<Hero />
|
|
||||||
<div className='content'>
|
|
||||||
<h1>About</h1>
|
|
||||||
<div className='about'>
|
|
||||||
<p>
|
|
||||||
Experienced Full Stack developer with a focus on building
|
|
||||||
user-friendly web and cross-platform mobile applications using
|
|
||||||
cutting-edge technologies. Passionate about ongoing learning and
|
|
||||||
staying up-to-date with the latest trends in software engineering.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{projects.length !== 0 && (
|
|
||||||
<>
|
|
||||||
<h1>Projects</h1>
|
|
||||||
<Button href='/projects' className='blue'>
|
|
||||||
See More
|
|
||||||
</Button>
|
|
||||||
<div className='projects'>
|
|
||||||
<div className='projects-wrapper'>
|
|
||||||
{projects.slice(0, 3).map(({ slug, ...rest }) => (
|
|
||||||
<Card {...rest} key={slug} href={`/projects/${slug}`} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{blogPosts.length !== 0 && (
|
|
||||||
<>
|
|
||||||
<h1>Blog</h1>
|
|
||||||
<Button href='/blog' className='blue'>
|
|
||||||
See More
|
|
||||||
</Button>
|
|
||||||
<div className='blog'>
|
|
||||||
<div className='articles-wrapper'>
|
|
||||||
{blogPosts.slice(0, 3).map(({ slug, ...rest }) => (
|
|
||||||
<Card {...rest} key={slug} href={`/blog/${slug}`} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async () => {
|
|
||||||
const blogPosts = getBlogPosts();
|
|
||||||
const projects = getProjects();
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
blogPosts,
|
|
||||||
projects,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { getPorjectsSlugs, getProjectdata } from '../../utils/projects';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote';
|
|
||||||
import { MDXProvider } from '@mdx-js/react';
|
|
||||||
import { GetStaticPaths, GetStaticProps } from 'next';
|
|
||||||
import { serialize } from 'next-mdx-remote/serialize';
|
|
||||||
import matter from 'gray-matter';
|
|
||||||
import { Wrapper } from '../../styles/projects/slug';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import IconButton from '../../components/IconButton';
|
|
||||||
import CodeBlock from '../../components/CodeBlock';
|
|
||||||
import MDXButton from '../../components/MDXButton';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
source: MDXRemoteSerializeResult;
|
|
||||||
frontMatter: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Project = ({ source, frontMatter }: Props) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const htmlOverrides = { code: CodeBlock };
|
|
||||||
const mdxComponents = { Button: MDXButton };
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content={
|
|
||||||
frontMatter?.description
|
|
||||||
? frontMatter.description
|
|
||||||
: `Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content={
|
|
||||||
frontMatter?.description
|
|
||||||
? frontMatter.description
|
|
||||||
: `Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property='og:title'
|
|
||||||
content={`${frontMatter?.title} | Hazem Krimi`}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content={
|
|
||||||
frontMatter?.tags
|
|
||||||
? frontMatter.tags.join(' ')
|
|
||||||
: `Hazem, Krimi, Hazem Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, TypeScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News, Software Developer, Software Engineer, Full Stack TypeScript Developer, Next.js`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<title>{`${frontMatter?.title} | Hazem Krimi`}</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<div className='meta'>
|
|
||||||
<div className='back' onClick={() => router.back()}>
|
|
||||||
<IconButton alt='Back' icon='/icons/arrow-left.svg' />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h1>{frontMatter?.title}</h1>
|
|
||||||
<p>{frontMatter?.description}</p>
|
|
||||||
{frontMatter?.tags ? (
|
|
||||||
<div className='tags-wrapper'>
|
|
||||||
{frontMatter.tags.map((tag: string, index: number) => (
|
|
||||||
<span key={index}>#{tag} </span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{frontMatter?.image && !frontMatter?.hideImage ? (
|
|
||||||
<div className='image'>
|
|
||||||
<Image alt={frontMatter?.title} src={frontMatter?.image} fill />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
<MDXProvider components={{ ...htmlOverrides, ...mdxComponents }}>
|
|
||||||
<div className='content'>
|
|
||||||
<MDXRemote {...source} />
|
|
||||||
<h1>Showcase</h1>
|
|
||||||
<div className='showcase-buttons'>
|
|
||||||
<MDXButton
|
|
||||||
variant='action'
|
|
||||||
link={frontMatter?.demo}
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
Demo
|
|
||||||
</MDXButton>
|
|
||||||
<MDXButton
|
|
||||||
variant='outline'
|
|
||||||
link={frontMatter?.code}
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
Source Code
|
|
||||||
</MDXButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MDXProvider>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Project;
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
const paths = getPorjectsSlugs();
|
|
||||||
return {
|
|
||||||
paths,
|
|
||||||
fallback: false,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const getStaticProps: GetStaticProps = async ({ params }: any) => {
|
|
||||||
const projectContent = await getProjectdata(params.slug);
|
|
||||||
|
|
||||||
if (!projectContent)
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
source: undefined,
|
|
||||||
frontMatter: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, content } = matter(projectContent);
|
|
||||||
const mdxSource = await serialize(content, {
|
|
||||||
scope: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
source: mdxSource,
|
|
||||||
frontMatter: data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { getProjects } from '../../utils/projects';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { Wrapper } from '../../styles/projects';
|
|
||||||
import Card from '../../components/Card';
|
|
||||||
import IconButton from '../../components/IconButton';
|
|
||||||
import Head from 'next/head';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
projects: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
image?: string;
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
tags?: string[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Index = ({ projects }: Props) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='author' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<link rel='canonical' href='https://hazemkrimi.tech' />
|
|
||||||
<meta property='og:image' content='/logo.png' />
|
|
||||||
<meta
|
|
||||||
property='og:description'
|
|
||||||
content='Hazem Krimi is an experienced Full Stack developer with a focus on building user-friendly
|
|
||||||
web and cross-platform mobile applications using cutting-edge
|
|
||||||
technologies. Passionate about ongoing learning and staying up-to-date
|
|
||||||
with the latest trends in software engineering.'
|
|
||||||
/>
|
|
||||||
<meta property='og:title' content='Hazem Krimi' />
|
|
||||||
<meta
|
|
||||||
name='keywords'
|
|
||||||
content='Hazem, Krimi, Hazem Krimi, Developer, Software, Engineer, Web, Mobile, Frontend, Backend, Fullstack, JavaScript, TypeScript, React.js, React Native, Node.js, Portfolio, Blog, Tutorials, Tech News, Software Developer, Software Engineer, Full Stack TypeScript Developer, Next.js'
|
|
||||||
/>
|
|
||||||
<title>Projects | Hazem Krimi</title>
|
|
||||||
</Head>
|
|
||||||
<Wrapper>
|
|
||||||
<div className='back' onClick={() => router.back()}>
|
|
||||||
<IconButton alt='Back' icon='/icons/arrow-left.svg' />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h1>Projects</h1>
|
|
||||||
<div className='projects-wrapper'>
|
|
||||||
{projects.length !== 0 ? (
|
|
||||||
projects.map(({ slug, ...rest }) => (
|
|
||||||
<Card {...rest} key={slug} href={`/projects/${slug}`} />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<h4>Nothing for now</h4>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Index;
|
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
|
||||||
const projects = getProjects();
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
projects,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 310 B |
@@ -1 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1010.51 567.82"><defs><style>.cls-1{fill:#262626;}</style></defs><title>dark-logo</title><polygon points="478.54 0 221.41 0 0 283.91 257.13 283.91 478.54 0"/><polygon class="cls-1" points="0 283.91 257.13 283.91 477.86 567.82 220.74 567.82 0 283.91"/><polygon class="cls-1" points="531.97 0 789.09 0 1010.51 283.91 753.38 283.91 531.97 0"/><polygon points="1010.51 283.91 753.38 283.91 532.64 567.82 789.77 567.82 1010.51 283.91"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 523 B |
|
Before Width: | Height: | Size: 314 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1573ca" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
|
||||||
|
Before Width: | Height: | Size: 307 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 294 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codepen"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 478 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 519 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
|
|
||||||
|
Before Width: | Height: | Size: 392 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 341 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 400 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 294 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codepen"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 478 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 519 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
|
|
||||||
|
Before Width: | Height: | Size: 392 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 341 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 400 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 276 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 645 B |
|
Before Width: | Height: | Size: 312 B |
@@ -1 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 872.54 490.3"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#ccc;}</style></defs><title>light-logo</title><g id="Hazem_logo" data-name="Hazem logo"><polygon class="cls-1" points="413.2 0 191.18 0 0 245.15 222.02 245.15 413.2 0"/><polygon class="cls-2" points="0 245.15 222.02 245.15 412.62 490.3 190.6 490.3 0 245.15"/><polygon class="cls-2" points="459.34 0 681.36 0 872.54 245.15 650.52 245.15 459.34 0"/><polygon class="cls-1" points="872.54 245.15 650.52 245.15 459.92 490.3 681.94 490.3 872.54 245.15"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 75 KiB |
@@ -1,3 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: *
|
|
||||||
Sitemap: https://hazemkrimi.tech/sitemap.xml
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset
|
|
||||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
|
||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
|
||||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
|
||||||
|
|
||||||
|
|
||||||
<url>
|
|
||||||
<loc>https://hazemkrimi.tech/</loc>
|
|
||||||
<lastmod>2022-02-13T19:02:40+00:00</lastmod>
|
|
||||||
<priority>1.00</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://hazemkrimi.tech/portfolio</loc>
|
|
||||||
<lastmod>2022-02-13T19:02:40+00:00</lastmod>
|
|
||||||
<priority>0.80</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://hazemkrimi.tech/blog</loc>
|
|
||||||
<lastmod>2022-02-13T19:02:40+00:00</lastmod>
|
|
||||||
<priority>0.80</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://hazemkrimi.tech/contact</loc>
|
|
||||||
<lastmod>2022-02-13T19:02:40+00:00</lastmod>
|
|
||||||
<priority>0.80</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://hazemkrimi.tech/resume.pdf</loc>
|
|
||||||
<lastmod>2022-02-13T19:02:40+00:00</lastmod>
|
|
||||||
<priority>0.80</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://hazemkrimi.tech/portfolio/react-weather-app</loc>
|
|
||||||
<lastmod>2022-02-13T19:02:40+00:00</lastmod>
|
|
||||||
<priority>0.80</priority>
|
|
||||||
</url>
|
|
||||||
|
|
||||||
|
|
||||||
</urlset>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'styled-components';
|
|
||||||
|
|
||||||
declare module 'styled-components' {
|
|
||||||
export interface DefaultTheme {
|
|
||||||
colors: {
|
|
||||||
dark: {
|
|
||||||
background: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
light: {
|
|
||||||
background: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
blue: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
min-height: 75vh;
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
min-height: 65vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #3f9aee;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
min-height: 75vh;
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
|
|
||||||
.back {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: ${({ theme }) => theme.colors.blue} !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.articles-wrapper {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(18.75rem, 1fr));
|
|
||||||
grid-auto-rows: minmax(6.25rem, auto);
|
|
||||||
align-items: stretch;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
min-height: 75vh;
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
row-gap: 2rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
row-gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
.back {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: ${({ theme }) => theme.colors.blue} !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
p,
|
|
||||||
.tags-wrapper {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
height: 0.1rem;
|
|
||||||
opacity: 0.3;
|
|
||||||
margin: 1rem auto 0rem auto;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin: 1rem auto 0rem auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin: 0.5rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p * {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
padding: 0rem;
|
|
||||||
grid-template-columns: auto;
|
|
||||||
column-gap: 0rem;
|
|
||||||
row-gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
color: #73d26b;
|
|
||||||
align-self: center;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto;
|
|
||||||
row-gap: 1.5rem;
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #d75050;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
row-gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto;
|
|
||||||
row-gap: 1rem;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
display: inline;
|
|
||||||
font-size: 1.7rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue {
|
|
||||||
color: ${({ theme }) => theme.colors.blue};
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.about,
|
|
||||||
.projects,
|
|
||||||
.blog {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-wrapper,
|
|
||||||
.articles-wrapper {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(18.75rem, 1fr));
|
|
||||||
grid-auto-rows: minmax(6.25rem, auto);
|
|
||||||
align-items: stretch;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
min-height: 75vh;
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
|
|
||||||
.back {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: ${({ theme }) => theme.colors.blue} !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-wrapper {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(18.75rem, 1fr));
|
|
||||||
grid-auto-rows: minmax(6.25rem, auto);
|
|
||||||
align-items: stretch;
|
|
||||||
justify-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
min-height: 75vh;
|
|
||||||
padding: 1rem 0rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
row-gap: 2rem;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
row-gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
.back {
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: ${({ theme }) => theme.colors.blue} !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
p {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
height: 0.1rem;
|
|
||||||
opacity: 0.3;
|
|
||||||
margin: 1rem auto 0rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin: 0.5rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 1rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p * {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
margin-inline-start: 0.5rem;
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase-buttons {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ThemeProvider, DefaultTheme } from 'styled-components';
|
|
||||||
|
|
||||||
const Shared = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const theme: DefaultTheme = {
|
|
||||||
colors: {
|
|
||||||
dark: {
|
|
||||||
background: '#262626',
|
|
||||||
text: 'white',
|
|
||||||
},
|
|
||||||
light: {
|
|
||||||
background: '#F9F9F9',
|
|
||||||
text: 'black',
|
|
||||||
},
|
|
||||||
blue: '#1573CA',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Shared;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { useReducer, useEffect, createContext } from 'react';
|
|
||||||
|
|
||||||
export const ThemeContext = createContext<{ mode: string; toggle: () => void }>(
|
|
||||||
{
|
|
||||||
mode: 'light',
|
|
||||||
toggle: () => {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const reducer = (state: string, action: { type: string }) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'TOGGLE':
|
|
||||||
return state === 'light' ? 'dark' : 'light';
|
|
||||||
case 'DARK':
|
|
||||||
return 'dark';
|
|
||||||
case 'LIGHT':
|
|
||||||
return 'light';
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Theme = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const [mode, dispatch] = useReducer(reducer, 'light');
|
|
||||||
const toggle = () => {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
const lightFavicon = document.querySelector('link#light-favicon')!;
|
|
||||||
const darkFavicon = document.querySelector('link#dark-favicon')!;
|
|
||||||
|
|
||||||
if (mode === 'dark') {
|
|
||||||
window.localStorage.setItem('mode', 'light');
|
|
||||||
root.style.setProperty('--background', '#F9F9F9');
|
|
||||||
root.style.setProperty('--secondary-background', 'white');
|
|
||||||
root.style.setProperty('--text', 'black');
|
|
||||||
root.style.setProperty('--text-inverted', 'white');
|
|
||||||
document.head.append(darkFavicon);
|
|
||||||
} else {
|
|
||||||
window.localStorage.setItem('mode', 'dark');
|
|
||||||
root.style.setProperty('--background', '#262626');
|
|
||||||
root.style.setProperty('--secondary-background', '#2F2F2F');
|
|
||||||
root.style.setProperty('--text', 'white');
|
|
||||||
root.style.setProperty('--text-inverted', 'black');
|
|
||||||
document.head.append(lightFavicon);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'TOGGLE' });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
const initialThemeMode = root.style.getPropertyValue('--mode');
|
|
||||||
|
|
||||||
dispatch({ type: initialThemeMode === 'dark' ? 'DARK' : 'LIGHT' });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeContext.Provider value={{ mode, toggle }}>
|
|
||||||
{children}
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Theme;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"incremental": true,
|
|
||||||
"jsx": "preserve"
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"include": ["**/*.d.ts", "**/*.ts", "**/*.tsx"]
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import matter from 'gray-matter';
|
|
||||||
|
|
||||||
const blogPostsDirectory = path.join(process.cwd(), '_blog');
|
|
||||||
|
|
||||||
export const getBlogPosts = () => {
|
|
||||||
try {
|
|
||||||
const fileNames = fs.readdirSync(blogPostsDirectory);
|
|
||||||
|
|
||||||
if (!fileNames) return [];
|
|
||||||
|
|
||||||
const allBlogPostsData = fileNames.map((filename) => {
|
|
||||||
const slug = filename.replace('.mdx', '');
|
|
||||||
|
|
||||||
const fullPath = path.join(blogPostsDirectory, filename);
|
|
||||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
const { data } = matter(fileContents);
|
|
||||||
|
|
||||||
const options = { month: 'long', day: 'numeric', year: 'numeric' };
|
|
||||||
const formattedDate = new Date(data.date).toLocaleDateString(
|
|
||||||
'en-IN',
|
|
||||||
// @ts-ignore
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
const frontmatter = {
|
|
||||||
...data,
|
|
||||||
date: formattedDate,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
slug,
|
|
||||||
...frontmatter,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return allBlogPostsData.sort((a, b) => {
|
|
||||||
if (new Date(a.date) < new Date(b.date)) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBlogPostsSlugs = () => {
|
|
||||||
try {
|
|
||||||
const fileNames = fs.readdirSync(blogPostsDirectory);
|
|
||||||
|
|
||||||
if (!fileNames) return [];
|
|
||||||
|
|
||||||
return fileNames.map((filename) => {
|
|
||||||
return {
|
|
||||||
params: {
|
|
||||||
slug: filename.replace('.mdx', ''),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBlogPostdata = async (slug: string) => {
|
|
||||||
try {
|
|
||||||
const fullPath = path.join(blogPostsDirectory, `${slug}.mdx`);
|
|
||||||
const postContent = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
|
|
||||||
if (!postContent) return undefined;
|
|
||||||
|
|
||||||
return postContent;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const GOOGLE_ANALYTICS_KEY =
|
|
||||||
process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_KEY;
|
|
||||||
|
|
||||||
export const pageview = (url: any) => {
|
|
||||||
// @ts-ignore
|
|
||||||
window.gtag('config', GOOGLE_ANALYTICS_KEY, {
|
|
||||||
page_path: url,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const event = ({ action, category, label, value }: any) => {
|
|
||||||
// @ts-ignore
|
|
||||||
window.gtag('event', action, {
|
|
||||||
event_category: category,
|
|
||||||
event_label: label,
|
|
||||||
value: value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initAnalytics = () => `
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
|
|
||||||
function gtag() {
|
|
||||||
dataLayer.push(arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', ${GOOGLE_ANALYTICS_KEY}, {
|
|
||||||
page_path: window.location.pathname,
|
|
||||||
});
|
|
||||||
`;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import matter from 'gray-matter';
|
|
||||||
|
|
||||||
const projects = path.join(process.cwd(), '_projects');
|
|
||||||
|
|
||||||
export const getProjects = () => {
|
|
||||||
try {
|
|
||||||
const fileNames = fs.readdirSync(projects);
|
|
||||||
|
|
||||||
if (!fileNames) return [];
|
|
||||||
|
|
||||||
const allProjectsData = fileNames.map((filename) => {
|
|
||||||
const slug = filename.replace('.mdx', '');
|
|
||||||
|
|
||||||
const fullPath = path.join(projects, filename);
|
|
||||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
const { data } = matter(fileContents);
|
|
||||||
|
|
||||||
const options = { month: 'long', day: 'numeric', year: 'numeric' };
|
|
||||||
const formattedDate = new Date(data.date).toLocaleDateString(
|
|
||||||
'en-IN',
|
|
||||||
// @ts-ignore
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
const frontmatter = {
|
|
||||||
...data,
|
|
||||||
date: formattedDate,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
slug,
|
|
||||||
...frontmatter,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return allProjectsData.sort((a, b) => {
|
|
||||||
if (new Date(a.date) < new Date(b.date)) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPorjectsSlugs = () => {
|
|
||||||
try {
|
|
||||||
const fileNames = fs.readdirSync(projects);
|
|
||||||
|
|
||||||
if (!fileNames) return [];
|
|
||||||
|
|
||||||
return fileNames.map((filename) => {
|
|
||||||
return {
|
|
||||||
params: {
|
|
||||||
slug: filename.replace('.mdx', ''),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProjectdata = async (slug: string) => {
|
|
||||||
try {
|
|
||||||
const fullPath = path.join(projects, `${slug}.mdx`);
|
|
||||||
const postContent = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
|
|
||||||
if (!postContent) return undefined;
|
|
||||||
|
|
||||||
return postContent;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
export const initStyles = () => `
|
|
||||||
function getInitialThemeMode() {
|
|
||||||
const persistedColorPreference = window.localStorage.getItem('mode');
|
|
||||||
const hasPersistedPreference = typeof persistedColorPreference === 'string';
|
|
||||||
|
|
||||||
if (hasPersistedPreference) {
|
|
||||||
return persistedColorPreference;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
const hasMediaQueryPreference = typeof mql.matches === 'boolean';
|
|
||||||
|
|
||||||
if (hasMediaQueryPreference) {
|
|
||||||
return mql.matches ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const mode = getInitialThemeMode();
|
|
||||||
const root = document.documentElement;
|
|
||||||
const lightFavicon = document.querySelector('link#light-favicon');
|
|
||||||
const darkFavicon = document.querySelector('link#dark-favicon');
|
|
||||||
|
|
||||||
root.style.setProperty('--mode', mode);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--background',
|
|
||||||
mode === 'light' ? '#F9F9F9' : '#262626'
|
|
||||||
);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--secondary-background',
|
|
||||||
mode === 'light' ? 'white' : '#2F2F2F'
|
|
||||||
);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--text',
|
|
||||||
mode === 'light' ? 'black' : 'white'
|
|
||||||
);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--text-inverted',
|
|
||||||
mode === 'light' ? 'white' : 'black'
|
|
||||||
);
|
|
||||||
document.head.append(mode === 'light' ? darkFavicon : lightFavicon);
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||