Simplifies page

This commit is contained in:
2024-03-18 22:00:52 +01:00
parent 359d2a8a56
commit ad044c6eb8
67 changed files with 1518 additions and 3577 deletions

3100
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,25 +9,24 @@
"start": "next start"
},
"dependencies": {
"@hookform/resolvers": "^2.9.8",
"clsx": "^1.2.1",
"framer-motion": "^7.5.3",
"next": "^13.4.8",
"nodemailer": "^6.8.0",
"@hookform/resolvers": "^3.3.2",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
"next": "^14.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.37.0",
"react-intl": "^6.4.4",
"react-hook-form": "^7.48.2",
"sharp": "^0.33.2",
"tabler-icons-react": "^1.56.0",
"zod": "^3.21.4"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.3.3",
"@types/react": "^18.2.14",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
"sass": "^1.63.6",
"tailwindcss": "^3.3.2",
"typescript": "^4.8.4"
"@types/node": "^20.9.0",
"@types/react": "^18.2.37",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"sass": "^1.69.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2"
}
}

1263
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
'use client';
import { AnimatePresence, motion } from 'framer-motion';
import { FC } from 'react';
import NavLink from '../NavLink';
import { NavLink } from '../NavLink';
import { bgVariants, links, listVariants } from './constants';
import { useBurgerMenuContext } from './context';

View File

@@ -1,3 +1,5 @@
'use client';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { FC } from 'react';

View File

@@ -1,4 +1,4 @@
import { Variants } from 'framer-motion';
import type { Variants } from 'framer-motion';
export const bgVariants: Variants = {
open: {

View File

@@ -1,4 +1,4 @@
import { Variants } from 'framer-motion';
import type { Variants } from 'framer-motion';
export const centerLineVariants: Variants = {
open: {

View File

@@ -1,4 +1,4 @@
import { Variants } from 'framer-motion';
import type { Variants } from 'framer-motion';
export const listVariants: Variants = {
open: {

View File

@@ -1,4 +1,4 @@
import { Variants } from 'framer-motion';
import type { Variants } from 'framer-motion';
export const movingLinesVariants: Variants = {
closed: (direction) => ({

View File

@@ -0,0 +1,40 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { Sun, Moon, Code } from 'tabler-icons-react';
import { useDarkMode } from '@contexts/DarkModeContext';
import { NavLink } from './NavLink';
import { links } from './BurgerMenu/constants';
import Button from '@components/Button';
export const PageLayoutHeader: FC = () => {
const { enabled: isDarkMode, toggle } = useDarkMode();
// We need to wait for client to render react on client to prevent error html content not matching server
const [firstRender, setFirstRender] = useState(false);
const DarkModeToggleIcon = firstRender ? (isDarkMode ? Sun : Moon) : Code;
useEffect(() => {
setFirstRender(true);
}, [setFirstRender]);
return (
<>
<header
role="header"
className="top-0 w-full px-2 py-2 z-20 flex items-center max-w-screen-xl mx-auto"
>
<nav className="flex items-center justify-center mr-auto">
{links.map((link) => (
<NavLink key={link.href} href={link.href}>
{link.title}
</NavLink>
))}
</nav>
<Button className="!p-2" onClick={() => toggle()}>
<DarkModeToggleIcon size={30} color="white" />
</Button>
</header>
</>
);
};

View File

@@ -0,0 +1,29 @@
'use client';
import { FC, PropsWithChildren, ReactElement } from 'react';
import Link, { LinkProps } from 'next/link';
export const NavLink: FC<PropsWithChildren<LinkProps>> = ({
href,
children,
onClick,
}): ReactElement => {
return (
<Link
className="text-3xl cursor-pointer dark:text-white text-blue-300 font-semibold whitespace-nowrap overflow-hidden block hover:no-underline"
onClick={onClick}
href={href}
>
<div className="relative" style={{ lineHeight: 1.4 }}>
<span>{children}</span>
<span
className={
'dark:text-project-accents text-project-green absolute w-full h-full left-0 -bottom-full'
}
>
{children}
</span>
</div>
</Link>
);
};

View File

@@ -1,12 +1,10 @@
import '../styles/globals.scss';
import { SiteLayout } from '@layouts';
import { useEffect } from 'react';
import { SiteContextProvider } from 'src/contexts';
'use client';
let prevTitle = '';
import { FC, useEffect } from 'react';
function MyApp({ Component, pageProps }) {
export const TitleChanger: FC = () => {
useEffect(() => {
let prevTitle = '';
const onUnloadCallback = () => {
switch (document.visibilityState) {
case 'hidden':
@@ -25,13 +23,5 @@ function MyApp({ Component, pageProps }) {
};
}, []);
return (
<SiteContextProvider>
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
</SiteContextProvider>
);
}
export default MyApp;
return null;
};

49
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { DarkModeContextProvider } from '@contexts/DarkModeContext';
import { TitleChanger } from './_components/TitleChanger';
import '../styles/globals.scss';
export const metadata = {
description:
'Hledáte správného programátora, který je schopný se nezalekne maličkostí, tak trochu superman? Můžu s hrdostí oznámit, že tady Vaše hledání končí. Jmenuju se Ondřej Langr a odteď jsem Vaše solution na webový development.',
authors: [{ name: 'Ondřej Langr' }],
title: 'Ondřej Langr Portfolio',
keywords:
'HTML, CSS, JavaScript, Fullstack development, React, Vue, Vuejs, Svelte, Android, Laravel, Redux, Zustand, Top programátor, webový specialista, Webapp, PWA',
icons: { icon: '/favicon-32x32.png', apple: '/apple-touch-icon.png' },
};
export const viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#5c04cc',
};
export default function Layout({ children }) {
return (
<DarkModeContextProvider>
<TitleChanger />
<html lang="cs">
<head>
<script
defer
data-domain="ondrejlangr.cz"
src="https://plausible.io/js/script.js"
></script>
</head>
<body>
<div className="dark:bg-siteBg transition-all duration-200">
<div className="absolute top-0 left-0 w-full h-full z-0 dark:bg-gradient-to-t dark:from-sitePurple opacity-20" />
<div className="relative z-10 ">
{/* <PageLayoutHeader /> */}
<main role="main">{children}</main>
<footer role="footer">{/*<FooterContactForm /> */}</footer>
</div>
</div>
</body>
</html>
</DarkModeContextProvider>
);
}

19
src/app/page.tsx Normal file
View File

@@ -0,0 +1,19 @@
import {
ContactForm,
HeroBanner,
AboutSection,
} from '../components/pages/main';
const HomePage = () => {
return (
<>
<HeroBanner />
<AboutSection />
{/* <WhatCanIOfferSection /> */}
{/* <MyWorkSection /> */}
<ContactForm />
</>
);
};
export default HomePage;

View File

@@ -1,3 +1,5 @@
'use client';
import clsx from 'clsx';
import { HTMLMotionProps, motion } from 'framer-motion';
import { FC, ReactElement } from 'react';
@@ -22,7 +24,7 @@ const Button: FC<ButtonProps> = ({
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={clsx(
'dark:bg-project-accents bg-project-green rounded-xl rounded-tl-none border-0 py-3 px-6 text-white text-xl font-semibold uppercase outline-none cursor-pointer',
'dark:bg-project-accents bg-project-green rounded-xl rounded-tr-none border-0 py-3 px-6 text-white text-xl font-semibold uppercase outline-none cursor-pointer',
loading && 'pointer-events-none opacity-60 cursor-progress',
className
)}

View File

@@ -1,11 +0,0 @@
import Head from "next/head";
import { FC, ReactElement } from "react";
import { SEOpageTitle } from "../../utils";
const HeadTitle: FC<{ title: string }> = ({ title }): ReactElement => <>
<Head>
<title>{SEOpageTitle(title)}</title>
</Head>
</>
export default HeadTitle;

View File

@@ -11,13 +11,13 @@ const TitledSection: FC<
<>
<section
className={clsx(
'relative mt-32 flex flex-wrap max-w-screen-xl mx-auto',
'relative mt-20 flex flex-wrap max-w-screen-xl mx-auto',
className
)}
{...rest}
>
<div className="w-full">
<div className="max-w-screen-xl px-4 font-bold mx-auto pt-32 text-center md:text-left">
<div className="max-w-screen-xl px-4 font-bold mx-auto pt-20 text-center md:text-left">
<h1 className="mt-0 text-blue-300 dark:text-white text-6xl xsm:text-7xl mb-0 uppercase relative inline-block">
<span className="z-10 relative drop-shadow-sm">{title}</span>
<span className="absolute w-0 dark:w-full bg-project-accents bottom-0 left-0 h-6 z-0 transition-all duration-200" />

View File

@@ -1,3 +1,5 @@
'use client';
import { DetailedHTMLProps, FC, forwardRef, InputHTMLAttributes } from 'react';
import clsx from 'clsx';
import { useFormState } from 'react-hook-form';

View File

@@ -1,3 +1,5 @@
'use client';
import { FC } from 'react';
import { useFormContext } from 'react-hook-form';
import { Option } from './Option';

View File

@@ -1,9 +1,9 @@
'use client';
import { DetailedHTMLProps, FC, forwardRef } from 'react';
import clsx from 'clsx';
import { useFormState } from 'react-hook-form';
import { Check, Cross } from 'tabler-icons-react';
import { SelectHTMLAttributes } from 'react';
import { useIntl } from 'react-intl';
import { useTranslations } from '@hooks';
export interface SelectProps

View File

@@ -1,3 +1,4 @@
'use client';
import {
DetailedHTMLProps,
FC,
@@ -5,7 +6,7 @@ import {
TextareaHTMLAttributes,
} from 'react';
import clsx from 'clsx';
import { useFormContext, useFormState } from 'react-hook-form';
import { useFormState } from 'react-hook-form';
import { Check, Cross } from 'tabler-icons-react';
export interface TextareaProps

View File

@@ -1,36 +1,31 @@
'use client';
import TitledSection from '@components/TitledSection';
import { FC, ReactElement } from 'react';
import { useSiteContext } from '@contexts';
import Button from '@components/Button';
import { FormState } from './types';
import { validationSchema } from './schemas';
import { initialValue } from './constants';
import { Input, Select, Textarea } from '@components/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslations } from '@hooks';
import { ContactRow } from './ContactRow';
import { Mail, Phone } from 'tabler-icons-react';
import { SocialLinks } from '../HeroBanner/SocialLinks';
import { useDarkMode } from '@contexts/DarkModeContext';
export const ContactForm: FC = (): ReactElement => {
const formMethods = useForm({
defaultValues: initialValue,
resolver: zodResolver(validationSchema),
});
const { setValue } = useSiteContext();
const { t } = useTranslations();
const { handleSubmit, reset, formState, register } = formMethods;
const { handleSubmit, reset } = formMethods;
const onSubmitCallback = async (values: FormState) => {
setValue('isLoading', true);
await fetch('/api/contactus', {
method: 'POST',
body: JSON.stringify(values),
});
setValue('isLoading', false);
reset();
};
@@ -59,7 +54,7 @@ export const ContactForm: FC = (): ReactElement => {
href="tel:+420607445251"
className="dark:text-project-accents text-blue-300"
>
+420 607 445 251
&#43;&#52;&#50;&#48;&#32;&#54;&#48;&#55;&#32;&#52;&#52;&#53;&#32;&#50;&#53;&#49;
</a>
</ContactRow>
<ContactRow icon={<Mail size={25} />}>

View File

@@ -1,3 +1,5 @@
'use client';
import { HTMLMotionProps, motion } from 'framer-motion';
import { useTranslations } from '@hooks';
import { FC } from 'react';

View File

@@ -1,3 +1,5 @@
'use client';
import Button from 'src/components/Button';
import Image from 'src/components/Image';
import TitledSection from 'src/components/TitledSection';

View File

@@ -0,0 +1,50 @@
'use client';
import {
FC,
PropsWithChildren,
createContext,
useContext,
useMemo,
useState,
} from 'react';
export const darkModeContext = createContext({
enabled: false,
toggle: (forceNextValue?: boolean) => {},
});
export const useDarkMode = () => useContext(darkModeContext);
export const DarkModeContextProvider: FC<PropsWithChildren> = ({
children,
}) => {
// TODO: Save it to cookies and set default by prefers color scheme
const [isToggled, setIsToggled] = useState(false);
// const [activeDarkMode, setActiveDarkMode] = useLocalStorage(
// 'theme',
// typeof window === 'undefined'
// ? 'light'
// : ((window.matchMedia('(prefers-color-scheme: dark)').matches
// ? 'dark'
// : 'light') as DarkModes)
// );
return (
<darkModeContext.Provider
value={useMemo(
() => ({
enabled: isToggled,
toggle(nextValueForced) {
setIsToggled((previous) => {
return nextValueForced ?? !previous;
});
},
}),
[isToggled]
)}
>
{children}
</darkModeContext.Provider>
);
};

View File

@@ -1,6 +0,0 @@
import { createContext } from 'react';
import { defaultValue } from './constants';
import { SiteContextValue } from './types';
export const SiteContextInstance =
createContext<SiteContextValue>(defaultValue);

View File

@@ -1,58 +0,0 @@
import { useLocalStorage } from '@hooks';
import { FC, ReactNode, useEffect, useMemo, useReducer } from 'react';
import { defaultValue } from './constants';
import { reducer } from './reducer';
import { SiteContextInstance } from './SiteContextInstance';
import { SiteContextValue, DarkModes } from './types';
export interface SiteContextProviderProps {
children: ReactNode;
}
export const SiteContextProvider: FC<SiteContextProviderProps> = ({
children,
}) => {
const [value, updateReducerValue] = useReducer(reducer, defaultValue);
const [activeDarkMode, setActiveDarkMode] = useLocalStorage(
'theme',
typeof window === 'undefined'
? 'light'
: ((window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light') as DarkModes)
);
const contextValue: SiteContextValue = useMemo(
() => ({
...value,
darkModeActive: activeDarkMode === 'dark',
setValue(key, value) {
if (key === 'darkModeActive') {
setActiveDarkMode(value ? 'dark' : 'light');
return;
}
updateReducerValue({ key, value });
},
}),
[value, updateReducerValue, activeDarkMode, setActiveDarkMode]
);
useEffect(() => {
if (activeDarkMode === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [activeDarkMode]);
// window.matchMedia('(prefers-color-scheme: dark)').matches
// useEffect(() => {}, [])
return (
<SiteContextInstance.Provider value={contextValue}>
{children}
</SiteContextInstance.Provider>
);
};

View File

@@ -1,8 +0,0 @@
import { SiteContextValue } from '../types';
export const defaultValue: SiteContextValue = {
isLoading: false,
darkModeActive: false,
setValue() {},
translations: {},
};

View File

@@ -1 +0,0 @@
export * from './useSiteContext';

View File

@@ -1,4 +0,0 @@
import { useContext } from 'react';
import { SiteContextInstance } from '../SiteContextInstance';
export const useSiteContext = () => useContext(SiteContextInstance);

View File

@@ -1,3 +0,0 @@
export * from './hooks';
export * from './types';
export * from './SiteContextProvider';

View File

@@ -1,10 +0,0 @@
import { Reducer } from 'react';
import { SiteContextValues } from './types';
import { SiteContextReducerPayload } from './types/SiteContextReducerPayload';
export const reducer: Reducer<SiteContextValues, SiteContextReducerPayload> = (
prevState,
{ key, value }
) => {
return { ...prevState, [key]: value };
};

View File

@@ -1,8 +0,0 @@
import { SiteContextValues } from './SiteContextValues';
export interface SiteContextFunctions {
setValue<T extends keyof SiteContextValues>(
key: T,
value: SiteContextValues[T]
): void;
}

View File

@@ -1,6 +0,0 @@
import { SiteContextValues } from './SiteContextValues';
export type SiteContextReducerPayload = {
key: keyof Omit<SiteContextValues, 'darkModeActive'>;
value: SiteContextValues[keyof SiteContextValues];
};

View File

@@ -1,4 +0,0 @@
import { SiteContextFunctions } from './SiteContextFunctions';
import { SiteContextValues } from './SiteContextValues';
export type SiteContextValue = SiteContextValues & SiteContextFunctions;

View File

@@ -1,7 +0,0 @@
export type DarkModes = 'light' | 'dark';
export interface SiteContextValues {
isLoading: boolean;
darkModeActive: boolean;
translations: Record<string, string>;
}

View File

@@ -1,3 +0,0 @@
export * from './SiteContextFunctions';
export * from './SiteContextValue';
export * from './SiteContextValues';

View File

@@ -1 +0,0 @@
export * from './SiteContext';

View File

@@ -1,3 +1,5 @@
'use client';
import { useState } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {

View File

@@ -1,11 +1,9 @@
import { useSiteContext } from '@contexts';
import { useMemo } from 'react';
export const useTranslations = () => {
const { translations } = useSiteContext();
return useMemo(
() => ({ t: (key: string) => translations[key] ?? key }),
[translations]
);
const localizations = {
en: {},
};
export const useTranslations = () => {
return useMemo(() => ({ t: (key: string) => localizations[key] ?? key }), []);
};

View File

@@ -1,41 +0,0 @@
import { FC, ReactElement } from 'react';
import Head from 'next/head';
import config from '@config';
const PageLayoutHead: FC = (): ReactElement => {
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={config.site.description} />
<meta name="keywords" content={config.site.keywords} />
<meta name="author" content={config.site.author} />
<link
rel="apple-touch-icon"
sizes="120x120"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/webmanifest.json" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#131113" />
<meta name="theme-color" content="#5c04cc" />
</Head>
</>
);
};
export default PageLayoutHead;

View File

@@ -1 +0,0 @@
export * from './defaultValue';

View File

@@ -1,37 +0,0 @@
import { FC, useEffect, useState } from 'react';
import { BurgerMenu } from './BurgerMenu';
import { Sun, Moon, Code } from 'tabler-icons-react';
import Button from '@components/Button';
import { useSiteContext } from '@contexts';
export const PageLayoutHeader: FC = () => {
const { darkModeActive, setValue } = useSiteContext();
// We need to wait for client to render react on client to prevent error html content not matching server
const [firstRender, setFirstRender] = useState(false);
const DarkModeToggleIcon = firstRender ? (darkModeActive ? Sun : Moon) : Code;
useEffect(() => {
setFirstRender(true);
}, [setFirstRender]);
return (
<>
<header
role="header"
className="top-0 w-full px-2 py-2 z-20 flex items-center justify-end sticky max-w-screen-xl mx-auto"
>
<div className="flex items-center gap-4 py-3 px-4 bg-white dark:shadow-none shadow-lg shadow-blue-200 rounded-xl">
{/*<img height={70} width={"auto"} src={"/logo-full.svg"} />*/}
<Button
className="!p-2"
onClick={() => setValue('darkModeActive', !darkModeActive)}
>
<DarkModeToggleIcon size={30} color="white" />
</Button>
<BurgerMenu />
</div>
</header>
</>
);
};

View File

@@ -1,70 +0,0 @@
import { FC, ReactElement } from 'react';
import Link from 'next/link';
import { HTMLMotionProps, motion, useCycle, Variants } from 'framer-motion';
const variants: Variants = {
open: {
y: 0,
opacity: 1,
transition: {
y: { stiffness: 350, velocity: 150, mass: 1, type: 'spring' },
},
},
closed: {
y: -130,
opacity: 0,
transition: {
y: { stiffness: 350, velocity: 150, mass: 1, type: 'spring' },
},
},
};
const fancyVariants: Variants = {
enter: {
y: '-100%',
},
exit: {
y: 0,
},
};
const MotionLink = motion(Link);
const NavLink: FC<HTMLMotionProps<'a'>> = ({
href,
children,
onClick,
}): ReactElement => {
const [hs, toggleH] = useCycle('exit', 'enter');
return (
<li className="my-7">
<MotionLink
variants={variants}
className="text-5xl xsm:text-7xl cursor-pointer dark:text-white text-blue-300 font-semibold whitespace-nowrap overflow-hidden block hover:no-underline"
onHoverStart={() => toggleH()}
onHoverEnd={() => toggleH()}
onClick={onClick}
href={href}
>
<motion.div
variants={fancyVariants}
animate={hs}
className="relative"
style={{ lineHeight: 1.4 }}
>
<span>{children}</span>
<span
className={
'dark:text-project-accents text-project-green absolute w-full h-full left-0 -bottom-full'
}
>
{children}
</span>
</motion.div>
</MotionLink>
</li>
);
};
export default NavLink;

View File

@@ -1,50 +0,0 @@
import { useSiteContext } from '@contexts';
import clsx from 'clsx';
import { FC, ReactNode } from 'react';
import PageLayoutHead from './Head';
import { PageLayoutHeader } from './Header';
export interface SiteLayoutProps {
children: ReactNode;
}
export const SiteLayout: FC<SiteLayoutProps> = ({ children }) => {
const { isLoading } = useSiteContext();
return (
<div className="dark:bg-siteBg transition-all duration-200">
<div className="absolute top-0 left-0 w-full h-full z-0 dark:bg-gradient-to-t dark:from-sitePurple opacity-20" />
<div className={clsx(isLoading && 'cursor-wait', 'relative z-10 ')}>
<PageLayoutHead />
<PageLayoutHeader />
<main role="main">{children}</main>
<footer role="footer">
{/*<FooterContactForm /> */}
<div>
<a></a>
</div>
</footer>
{/*<div className="z-20 fixed top-0 left-0 w-full h-screen bg-white flex items-center justify-center flex-col">
<div className="my-2">
<b>Stránka se intenzivně připravuje.</b>
</div>{" "}
<div className="my-2">
{" "}
V mezičase mi stačí brnknout na{" "}
<a href="tel:+420607445251" className="text-indigo-700">
{" "}
+420 607 445 251{" "}
</a>{" "}
nebo si se mnou psát na mailu{" "}
<a href="mailto:hi@ondrejlangr.cz" className="text-indigo-700">
{" "}
hi@ondrejlangr.cz{" "}
</a>
</div>
</div>*/}
</div>
</div>
);
};

View File

@@ -1,2 +0,0 @@
export * from './SiteLayout';
export { SiteLayout as default } from './SiteLayout';

View File

@@ -1 +0,0 @@
export * from './SiteLayout';

View File

@@ -1,15 +0,0 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<link rel="stylesheet" href="https://use.typekit.net/asq3wpq.css" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -1,41 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import nodemailer from "nodemailer";
type ResponseData = {};
const transporter = nodemailer.createTransport({
host: "smtp.forpsi.com",
port: 587,
auth: {
user: process.env.MAILER_EMAIL,
pass: process.env.MAILER_PASSWORD,
},
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.setHeader("Content-Type", "application/json");
const parsed = JSON.parse(req.body);
if (parsed?.fullname && parsed?.email && parsed?.message) {
await transporter.sendMail({
from: process.env.MAILER_EMAIL,
to: "hi@ondrejlangr.cz",
subject: "Nová zpráva v kontaktním formuláři!",
html: `<h1>Zpráva od: ${parsed.fullname}</h1>
<p><b>Obsah zprávy:</b> ${parsed.message}</p><br/>
<p>Budget: ${parsed.budget || "unknown"}</p>
<p>Tech: ${parsed.tech || "default"}</p>
<p>Telephone: ${parsed.telephone || "none"}</p>
<a href="mailto:${parsed.email}">Odpovědět na mail: ${
parsed.email
}</a>`,
});
return res.status(200).json({ error: false });
} else {
return res.status(404).json({ error: true, payload: parsed });
}
}

View File

@@ -1,26 +0,0 @@
import {
ContactForm,
HeroBanner,
AboutSection,
WhatCanIOfferSection,
MyWorkSection,
} from '../components/pages/main';
import HeadTitle from '../components/HeadTitle';
import { useTranslations } from '@hooks';
const HomePage = () => {
const { t } = useTranslations();
return (
<>
<HeadTitle title={t('Main page')} />
<HeroBanner />
<AboutSection />
<WhatCanIOfferSection />
<MyWorkSection />
<ContactForm />
</>
);
};
export default HomePage;

View File

@@ -31,8 +31,8 @@
"@layouts": [
"src/layouts"
],
"@contexts": [
"src/contexts"
"@contexts/*": [
"src/contexts/*"
],
"@hooks": [
"src/hooks"
@@ -41,12 +41,18 @@
"public/*"
]
},
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"