Simplifies page
This commit is contained in:
3100
package-lock.json
generated
3100
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -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
1263
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FC } from 'react';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Variants } from 'framer-motion';
|
||||
import type { Variants } from 'framer-motion';
|
||||
|
||||
export const bgVariants: Variants = {
|
||||
open: {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Variants } from 'framer-motion';
|
||||
import type { Variants } from 'framer-motion';
|
||||
|
||||
export const centerLineVariants: Variants = {
|
||||
open: {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Variants } from 'framer-motion';
|
||||
import type { Variants } from 'framer-motion';
|
||||
|
||||
export const listVariants: Variants = {
|
||||
open: {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Variants } from 'framer-motion';
|
||||
import type { Variants } from 'framer-motion';
|
||||
|
||||
export const movingLinesVariants: Variants = {
|
||||
closed: (direction) => ({
|
||||
40
src/app/_components/Header/Header.tsx
Normal file
40
src/app/_components/Header/Header.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
29
src/app/_components/Header/NavLink.tsx
Normal file
29
src/app/_components/Header/NavLink.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
49
src/app/layout.tsx
Normal 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
19
src/app/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
ContactForm,
|
||||
HeroBanner,
|
||||
AboutSection,
|
||||
} from '../components/pages/main';
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<HeroBanner />
|
||||
<AboutSection />
|
||||
{/* <WhatCanIOfferSection /> */}
|
||||
{/* <MyWorkSection /> */}
|
||||
<ContactForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { DetailedHTMLProps, FC, forwardRef, InputHTMLAttributes } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useFormState } from 'react-hook-form';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Option } from './Option';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+420 607 445 251
|
||||
</a>
|
||||
</ContactRow>
|
||||
<ContactRow icon={<Mail size={25} />}>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLMotionProps, motion } from 'framer-motion';
|
||||
import { useTranslations } from '@hooks';
|
||||
import { FC } from 'react';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import Button from 'src/components/Button';
|
||||
import Image from 'src/components/Image';
|
||||
import TitledSection from 'src/components/TitledSection';
|
||||
|
||||
50
src/contexts/DarkModeContext.tsx
Normal file
50
src/contexts/DarkModeContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
import { defaultValue } from './constants';
|
||||
import { SiteContextValue } from './types';
|
||||
|
||||
export const SiteContextInstance =
|
||||
createContext<SiteContextValue>(defaultValue);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { SiteContextValue } from '../types';
|
||||
|
||||
export const defaultValue: SiteContextValue = {
|
||||
isLoading: false,
|
||||
darkModeActive: false,
|
||||
setValue() {},
|
||||
translations: {},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './useSiteContext';
|
||||
@@ -1,4 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { SiteContextInstance } from '../SiteContextInstance';
|
||||
|
||||
export const useSiteContext = () => useContext(SiteContextInstance);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './hooks';
|
||||
export * from './types';
|
||||
export * from './SiteContextProvider';
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { SiteContextValues } from './SiteContextValues';
|
||||
|
||||
export interface SiteContextFunctions {
|
||||
setValue<T extends keyof SiteContextValues>(
|
||||
key: T,
|
||||
value: SiteContextValues[T]
|
||||
): void;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { SiteContextValues } from './SiteContextValues';
|
||||
|
||||
export type SiteContextReducerPayload = {
|
||||
key: keyof Omit<SiteContextValues, 'darkModeActive'>;
|
||||
value: SiteContextValues[keyof SiteContextValues];
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
import { SiteContextFunctions } from './SiteContextFunctions';
|
||||
import { SiteContextValues } from './SiteContextValues';
|
||||
|
||||
export type SiteContextValue = SiteContextValues & SiteContextFunctions;
|
||||
@@ -1,7 +0,0 @@
|
||||
export type DarkModes = 'light' | 'dark';
|
||||
|
||||
export interface SiteContextValues {
|
||||
isLoading: boolean;
|
||||
darkModeActive: boolean;
|
||||
translations: Record<string, string>;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './SiteContextFunctions';
|
||||
export * from './SiteContextValue';
|
||||
export * from './SiteContextValues';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SiteContext';
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
|
||||
@@ -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 }), []);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './defaultValue';
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './SiteLayout';
|
||||
export { SiteLayout as default } from './SiteLayout';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SiteLayout';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user