first commit

main
joker 3 years ago
commit 8af5253a00
  1. 26
      README.md
  2. 45
      components/Head.tsx
  3. 46
      components/Layout.tsx
  4. 52
      components/LoadingTeamsForm.tsx
  5. 91
      components/Navigation.tsx
  6. 136
      components/RegistrationForm.tsx
  7. 102
      components/ThemeSwitch.tsx
  8. 7
      components/UX/Alert.tsx
  9. 27
      components/UX/Input.tsx
  10. 14
      components/UX/Link.tsx
  11. 40
      components/UX/Select.tsx
  12. 4
      components/UX/index.ts
  13. 14
      components/Video.tsx
  14. 6
      env.local
  15. 17
      jest.config.js
  16. 45
      lib/api.ts
  17. 63
      mysql/members.sql
  18. 5
      next-env.d.ts
  19. 17
      next.config.mjs
  20. 79
      package.json
  21. 18
      pages/_app.tsx
  22. 30
      pages/_document.tsx
  23. 20
      pages/about.tsx
  24. 23
      pages/contacts.tsx
  25. 52
      pages/index.tsx
  26. 98
      pages/posts/[slug].tsx
  27. 8
      postcss.config.js
  28. 57
      posts/lists.mdx
  29. 15
      redux/store.ts
  30. 13
      redux/user/asyncActions.ts
  31. 4
      redux/user/index.ts
  32. 4
      redux/user/selectors.ts
  33. 39
      redux/user/slice.ts
  34. 21
      redux/user/types.ts
  35. 13
      server/db/connect.ts
  36. 15
      server/db/insert.ts
  37. 13
      server/db/select.ts
  38. 119
      styles/globals.css
  39. 75
      tailwind.config.js
  40. 1
      test/__mocks__/fileMock.js
  41. 137
      test/pages/__snapshots__/index.test.tsx.snap
  42. 24
      test/testUtils.ts
  43. 33
      tsconfig.json
  44. 9
      types/layout.ts
  45. 7
      types/post.ts
  46. 11
      utils/mdxUtils.ts
  47. 7904
      yarn.lock

@ -0,0 +1,26 @@
# RoboTop сайт робототехнического фестиваля
Сайт для мероприятия с возможностью регистрировать пользователей в безе данный MySql. Построен на:
- Сайт написан на [Typescript](https://www.typescriptlang.org/)
- Написание постов [MDX](https://mdxjs.com/)
- Стили и дизайн [Tailwind CSS](https://tailwindcss.com/)
- Статически анализ кода [ESLint](https://eslint.org/)
- Linting, проверка типов и форматирование включены по умолчанию. [`husky`](https://github.com/typicode/husky)
- Тестирование с [Jest](https://jestjs.io/) и [`react-testing-library`](https://testing-library.com/docs/react-testing-library/intro)
- [Redux Toolkit](https://redux-toolkit.js.org/)
- [Mysql2](https://www.npmjs.com/package/mysql2)
## Для запуска сайта:
```bash
git clone http://62.113.100.171:3000/Lab/robotop.krasnikov.pro.git
cd robotop.krasnikov.pro
npm install
npm run dev
Ваш новый сайт будет доступен в http://localhost:3000/
Для настройки подключения к базе данных отредактируйте файл env.local и переименуйте его в .env.local

@ -0,0 +1,45 @@
import NextHead from 'next/head';
import { useRouter } from 'next/router';
import React from 'react';
import { MetaProps } from '../types/layout';
export type WithYandexMetrikaProps = {
children: React.ReactNode;
}
export const WEBSITE_HOST_URL = 'https://robotop.krasnikov.pro';
const Head = ({ customMeta }: { customMeta?: MetaProps }): JSX.Element => {
const router = useRouter();
const meta: MetaProps = {
title: 'РоботТоп - робототехнический фестиваль',
description:
'РоботТОП – это робототехнические соревнования, в которых могут принять участие молодые любители робототехники, объединившись в команды.',
image: `${WEBSITE_HOST_URL}/images/site-preview.png`,
type: 'website',
...customMeta,
};
return (
<NextHead>
<title>{meta.title}</title>
<meta content={meta.description} name="РоботТОП – это робототехнические соревнования, в которых могут принять участие молодые любители робототехники, объединившись в команды." />
<meta property="og:url" content={`${WEBSITE_HOST_URL}${router.asPath}`} />
<link rel="canonical" href={`${WEBSITE_HOST_URL}${router.asPath}`} />
<meta property="og:type" content={meta.type} />
<meta property="og:site_name" content="РоботТоп - робототехнический фестиваль" />
<meta property="og:description" content={meta.description} />
<meta property="og:title" content={meta.title} />
<meta property="og:image" content={meta.image} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={meta.title} />
<meta name="twitter:description" content={meta.description} />
<meta name="twitter:image" content={meta.image} />
{meta.date && (
<meta property="article:published_time" content={meta.date} />
)}
</NextHead>
);
};
export default Head;

@ -0,0 +1,46 @@
import React from 'react';
import { MetaProps } from '../types/layout';
import Head from './Head';
import Navigation from './Navigation';
import ThemeSwitch from './ThemeSwitch';
type LayoutProps = {
children: React.ReactNode;
customMeta?: MetaProps;
};
export const WEBSITE_HOST_URL = 'https://robotop.krasnikov.pro/';
const Layout = ({ children, customMeta }: LayoutProps): JSX.Element => {
return (
<>
<Head customMeta={customMeta} />
<header>
<div className="max-w-5xl px-8 mx-auto max-w">
<div className="flex items-center justify-between py-6">
<Navigation />
<ThemeSwitch />
</div>
</div>
</header>
<main>
<div className="max-w-5xl px-8 py-4 mx-auto max-w">
{children}
</div>
</main>
<footer className="py-8">
<div className="max-w-5xl px-8 mx-auto max-w">
Разработано {' '}
<a
className="text-gray-900 dark:text-white"
href="https://krasnikov.pro" target='_blank' rel="noreferrer"
>
Krasnikov.pro - {(new Date()).getFullYear()} год
</a>
</div>
</footer>
</>
);
};
export default Layout;

@ -0,0 +1,52 @@
import React from 'react';
type UserProps = {
team_name: string;
name_team_coach: string;
training_institution_team: string;
name_first_participant: string;
name_second_participant: string;
name_third_party: string;
classTeam: string[];
};
export const LoadingTeamsForm : React.FC<UserProps> = ({
team_name,
name_team_coach,
training_institution_team,
name_first_participant,
name_second_participant,
name_third_party,
classTeam
}) => {
const flatten = (arr) => {
const arrOfNum = [];
arr.split(',').forEach(str => {
arrOfNum.push(Number(str));
});
return Math.min.apply(null, arrOfNum.filter(Boolean)); //Math.min(...arrOfNum);
}
return (
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{team_name}
</th>
<td className="px-6 py-4">
{name_team_coach}
</td>
<td className="px-6 py-4">
{training_institution_team}
</td>
<td className="px-6 py-4">
{name_first_participant +', ' + name_second_participant + ', ' + name_third_party}
</td>
<td className="px-6 py-4">
{ flatten(classTeam)}
</td>
</tr>
);
};
export default LoadingTeamsForm;

@ -0,0 +1,91 @@
import Link from 'next/link';
import React, { useState } from 'react';
import { MenuIcon, XIcon } from '@heroicons/react/outline'
import { Transition } from "@headlessui/react";
import { useRouter } from 'next/router'
const navigation = [
{ name: 'Главная', href: '/', as: false }
]
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const Navigation = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
return (
<nav className="">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{navigation.map((item) => (
<Link as={ item.as ? '/posts/'+item.as : ''} href={item.href} key={item.name}>
<a
className={classNames('bg-gray-900 text-white text-gray-900 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium'
)}
aria-current={false}
>
{item.name}
</a>
</Link>
))}
</div>
</div>
</div>
<div className="-mr-2 flex md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
type="button"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span className="sr-only">Открыть главное меню</span>
{!isOpen ? (
<MenuIcon className="block h-6 w-6" aria-hidden="false" />
) : (
<XIcon className="block h-6 w-6" aria-hidden="true" />
)}
</button>
</div>
</div>
</div>
<Transition
show={isOpen}
enter="transition ease-out duration-100 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75 transform"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="absolute bg-gray-100 z-50"
>
{(ref) => (
<div className="md:hidden" id="mobile-menu">
<div ref={ref} className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
{navigation.map((item) => (
<Link as={'/posts/'+item.as} href={item.href} key={item.name}>
<a
className={classNames('text-gray-900 hover:bg-gray-900 hover:text-white',
'block px-3 py-2 rounded-md text-base font-medium'
)}
aria-current={item.href ? 'page' : undefined}
>
{item.name}
</a>
</Link>
))}
</div>
</div>
)}
</Transition>
</nav>
);
};
export default Navigation;

@ -0,0 +1,136 @@
import React,{useRef} from 'react';
import { useForm, SubmitHandler, FormProvider } from "react-hook-form";
import { Select, Input, Link } from "./UX";
interface IFormInputs {
name_team_coach: string,
coach_telefon_number: string,
email_address: string,
city_team: string,
training_institution_team: string,
team_name: string,
name_first_participant: string,
first_partial_class: number,
name_second_participant: string,
second_class: number,
name_third_party: string,
third_part_class: number,
body?: string[] | number[]
}
const defaultValues = {
name_team_coach: ``,
coach_telefon_number: ``,
email_address: '',
city_team: '',
training_institution_team: '',
team_name: '',
name_first_participant: '',
first_partial_class: 0,
name_second_participant: 'нет',
second_class: 0,
name_third_party: 'нет',
third_part_class: 0,
};
export const RegistrationForm = (props): JSX.Element => {
const form = useRef(null);
const methods = useForm({ defaultValues });
const onSubmit: SubmitHandler<IFormInputs> = data => {
fetch('/api/registration', { method: 'POST', body: Object.values(data) as any})
.then((data) => {
props.updateData(data);
})
methods.reset(defaultValues);
}
return (
<>
<div className="mt-10 sm:mt-0">
<div className="md:grid md:grid-cols-3 md:gap-6">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-medium leading-6">Регистрация команды</h3>
<p className="mt-1 text-sm">Введите актуальные данные команды</p>
<p className="mt-1 text-sm">От каждого учебного заведения может быть зарегистрированно неограниченое количеставо команд</p>
<p className="mt-1 text-sm"> Подписывайтесь на наш
<Link href="https://t.me/robotop_competition"> Telegram канал</Link>
, что-бы быть в курсе новостей про соревнование </p>
</div>
</div>
<div className="mt-5 md:mt-0 md:col-span-2">
<FormProvider {...methods} >
<form ref={form} onSubmit={methods.handleSubmit(onSubmit)}>
<div className="shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-5 bg-white sm:p-6">
<div className="grid grid-cols-6 gap-6">
<div className="col-span-6 sm:col-span-3">
<Input placeholder="Иванов Иван Иванович" name="name_team_coach" text="Введите ФИО тренера" additional={""}/>
</div>
<div className="col-span-6 sm:col-span-3">
<Input placeholder="+79181234567" name="coach_telefon_number" text="Номер телефона тренера" additional={"valueAsNumber: true"} />
</div>
<div className="col-span-6 sm:col-span-3">
<Input placeholder="you@example.com" name="email_address" text="Email тренера" additional={`pattern: /^(([^<>()[]\\.,;:s@"]+(.[^<>()[]\\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/ })`} />
</div>
<div className="col-span-6 sm:col-span-3">
<Input placeholder="Краснодар" name="city_team" text="Город команда" additional={""} />
</div>
<div className="col-span-3">
<Input placeholder="МАОУ СОШ 103" name="training_institution_team" text="Учебное заведение команды" additional={""} />
</div>
<div className="col-span-3">
<Input placeholder="Фиксики" name="team_name" text="Название команды" additional={""} />
</div>
<div className="col-span-6 sm:col-span-3">
<Input placeholder="Иванов Иван Иванович" name="name_first_participant" text="ФИО первого участника" additional={""} />
</div>
<div className="col-span-6 sm:col-span-3">
<Select text={'Класс участника'} name={'first_partial_class'}/>
</div>
<div className="col-span-6 sm:col-span-3">
<Input placeholder="Иванов Петр Иванович / нет" name="name_second_participant" text="ФИО второго участника" additional={""} />
</div>
<div className="col-span-6 sm:col-span-3">
<Select text={'Класс участника'} name={'second_class'}/>
</div>
<div className="col-span-6 sm:col-span-3">
<Input placeholder="Иванов Дмитрий Иванович / нет" name="name_third_party" text="ФИО третьего участника" additional={""}/>
</div>
<div className="col-span-6 sm:col-span-3">
<Select text={'Класс участника'} name={'third_part_class'}/>
</div>
</div>
</div>
<div className="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Зарегистрировать команду
</button>
</div>
</div>
</form>
</FormProvider>
</div>
</div>
</div>
</>
);
};
export default RegistrationForm;

@ -0,0 +1,102 @@
import { useTheme } from 'next-themes';
import React from 'react';
const ThemeSwitch = (): JSX.Element => {
const [mounted, setMounted] = React.useState(false);
const { theme, setTheme } = useTheme();
// After mounting, we have access to the theme
React.useEffect(() => setMounted(true), []);
if (!mounted) {
return null;
}
const isDark = theme === 'dark';
const color = isDark ? '#fff' : '#000';
const maskColor = isDark ? '#000' : '#fff';
return (
<button
className="theme-button"
type="button"
aria-label="Toggle Dark Mode"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<div className="moon-or-sun" />
<div className="moon-mask" />
<style jsx>{`
.theme-button {
opacity: 0.5;
position: relative;
border-radius: 5px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
}
.theme-button:hover {
opacity: 1;
}
.moon-or-sun {
position: relative;
width: 20px;
height: 20px;
border-radius: 50%;
border: ${isDark ? '4px' : '2px'} solid;
border-color: ${color};
background: ${color};
transform: scale(${isDark ? 0.5 : 1});
transition: all 0.45s ease;
overflow: ${isDark ? 'visible' : 'hidden'};
}
.moon-or-sun::before {
content: '';
position: absolute;
right: -9px;
top: -9px;
height: 20px;
width: 20px;
border: 2px solid;
border-color: ${color};
border-radius: 50%;
transform: translate(${isDark ? '14px, -14px' : '0, 0'});
opacity: ${isDark ? 0 : 1};
transition: transform 0.45s ease;
}
.moon-or-sun::after {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
margin: -4px 0 0 -4px;
position: absolute;
top: 50%;
left: 50%;
box-shadow: 0 -23px 0 ${color}, 0 23px 0 ${color}, 23px 0 0 ${color},
-23px 0 0 ${color}, 15px 15px 0 ${color}, -15px 15px 0 ${color},
15px -15px 0 ${color}, -15px -15px 0 ${color};
transform: scale(${isDark ? 1 : 0});
transition: all 0.35s ease;
}
.moon-mask {
position: absolute;
right: 4px;
top: 4px;
height: 20px;
width: 20px;
border-radius: 50%;
border: 0;
background: ${maskColor};
transform: translate(${isDark ? '4px, -4px' : '0, 0'});
opacity: ${isDark ? 0 : 1};
transition: transform 0.45s ease;
}
`}</style>
</button>
);
};
export default ThemeSwitch;

@ -0,0 +1,7 @@
import React from 'react';
export const Alert = (text) => {
return(
<p className="mt-2 text-sm text-red-600 dark:text-red-500 font-medium">{text}</p>
)
}

@ -0,0 +1,27 @@
import React from 'react';
import { useFormContext } from "react-hook-form";
type Props = {
text: string;
name: string;
placeholder: string;
additional: string;
}
export const Input: React.FC<Props> = ({text, name, placeholder, additional}) => {
const { register } = useFormContext();
const options = {required: true, maxLength: 80, additional};
return(
<>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{text}
</label>
<input
{...register(name, options )}
name={name}
placeholder={placeholder}
className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</>
)
}

@ -0,0 +1,14 @@
import React from 'react';
type Props = {
children: string;
href: string;
}
export const Link: React.FC<Props> = ({children, href}) => {
return(
<>
<a href={href} target='_blank' rel="noreferrer" className="dark:text-white">{children}</a>
</>
)
}

@ -0,0 +1,40 @@
import React from 'react';
//import { useFormContext } from "react-hook-form";
import { useFormContext } from "react-hook-form";
type Props = {
text: string;
name: string;
children?: JSX.Element[] | JSX.Element;
}
export const Select: React.FC<Props> = ({text, name}) => {
const { register } = useFormContext();
return(
<>
<label htmlFor="country" className="block text-sm font-medium text-gray-700">
{text}
</label>
<select
{...register(name)} // ...register("first_partial_class")
name={name}
defaultValue={0}
className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value={0}>-- Выбрать --</option>
<option >1</option>
<option >2</option>
<option >3</option>
<option >4</option>
<option >5</option>
<option >6</option>
<option >7</option>
<option >8</option>
<option >9</option>
<option >10</option>
<option >11</option>
</select>
</>
)
}

@ -0,0 +1,4 @@
export * from './Select'
export * from './Alert'
export * from './Input'
export * from './Link'

@ -0,0 +1,14 @@
export interface VideoProps {
title: string;
src: string;
}
export function Video(props: VideoProps) {
return (
<video controls style={{ width: '960px' }}>
<source src={props.src} />
</video>
);
}
export default Video;

@ -0,0 +1,6 @@
// .env.local
USER_="Имя пользователя базы данных"
HOST="IP адрес базы данных"
DATABASE="Имя базы данных"
PASSWORD="Пароль доступа к базе данных"

@ -0,0 +1,17 @@
module.exports = {
roots: ['<rootDir>'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'],
testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|.next)[/\\\\]'],
transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$'],
transform: {
'^.+\\.(ts|tsx)$': 'babel-jest',
},
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
moduleNameMapper: {
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js',
},
};

@ -0,0 +1,45 @@
import fs from 'fs';
import matter from 'gray-matter';
import { join } from 'path';
import { POSTS_PATH } from '../utils/mdxUtils';
export function getPostSlugs(): string[] {
return fs.readdirSync(POSTS_PATH);
}
type PostItems = {
[key: string]: string;
};
export function getPostBySlug(slug: string, fields: string[] = []): PostItems {
const realSlug = slug.replace(/\.mdx$/, '');
const fullPath = join(POSTS_PATH, `${realSlug}.mdx`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const items: PostItems = {};
// Ensure only the minimal needed data is exposed
fields.forEach((field) => {
if (field === 'slug') {
items[field] = realSlug;
}
if (field === 'content') {
items[field] = content;
}
if (data[field]) {
items[field] = data[field];
}
});
return items;
}
export function getAllPosts(fields: string[] = []): PostItems[] {
const slugs = getPostSlugs();
const posts = slugs
.map((slug) => getPostBySlug(slug, fields))
// sort posts by date in descending order
.sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
return posts;
}

@ -0,0 +1,63 @@
-- phpMyAdmin SQL Dump
-- version 4.9.7
-- https://www.phpmyadmin.net/
-- --------------------------------------------------------
--
-- Table structure for table `members`
--
-- Creation: Jun 14, 2022 at 04:25 AM
-- Last update: Jul 15, 2022 at 10:08 PM
--
DROP TABLE IF EXISTS `members`;
CREATE TABLE `members` (
`id` int(5) NOT NULL,
`name_team_coach` varchar(100) NOT NULL,
`coach_telefon_number` varchar(100) NOT NULL,
`trainer_mail` varchar(100) NOT NULL,
`city_team` varchar(100) NOT NULL,
`training_institution_team` varchar(100) NOT NULL,
`team_name` varchar(100) NOT NULL,
`name_first_participant` varchar(100) NOT NULL,
`first_partial_class` int(2) NOT NULL,
`name_second_participant` varchar(100) NOT NULL,
`second_class` int(2) NOT NULL,
`name_third_party` varchar(100) NOT NULL,
`third_part_class` int(2) NOT NULL,
`reg_time_add` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` int(1) NOT NULL DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Dumping data for table `members`
--
INSERT INTO `members` (`id`, `name_team_coach`, `coach_telefon_number`, `trainer_mail`, `city_team`, `training_institution_team`, `team_name`, `name_first_participant`, `first_partial_class`, `name_second_participant`, `second_class`, `name_third_party`, `third_part_class`, `reg_time_add`, `status`) VALUES
(36, 'Красников Павел Геннадьевич', '79189458044', 'crapsh@gmail.com', 'Краснодар', 'МАОУ СОШ 103', 'Фиксики', 'Иван Филлипов', 8, 'Владислав Дельнов', 8, 'нет', 0, '2022-07-02 18:34:33', 1);
--
-- Indexes for dumped tables
--
--
-- Indexes for table `members`
--
ALTER TABLE `members`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `members`
--
ALTER TABLE `members`
MODIFY `id` int(5) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=98;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

5
next-env.d.ts vendored

@ -0,0 +1,5 @@
/// <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.

@ -0,0 +1,17 @@
import nextMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm, remarkParse, remarkRehype],
rehypePlugins: [rehypeStringify],
},
})
export default withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
})

@ -0,0 +1,79 @@
{
"name": "nextjs-typescript-mdx-blog",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start -p 3005",
"type-check": "tsc --pretty --noEmit",
"format": "prettier --write .",
"lint": "eslint . --ext ts --ext tsx --ext js",
"test": "jest",
"test-all": "yarn lint && yarn type-check && yarn test"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "yarn run type-check"
}
},
"lint-staged": {
"*.@(ts|tsx)": [
"yarn lint",
"yarn format"
]
},
"dependencies": {
"@headlessui/react": "^1.6.5",
"@heroicons/react": "^1.0.6",
"@mux/mux-player": "^1.6.0",
"@mux/mux-player-react": "^1.6.0",
"@next/mdx": "^12.2.0",
"@reduxjs/toolkit": "^1.8.3",
"@tailwindcss/typography": "^0.5.2",
"@types/node-fetch": "^2.6.2",
"axios": "^0.27.2",
"date-fns": "^2.28.0",
"gray-matter": "^4.0.3",
"mysql2": "^2.3.3",
"next": "^12.2.0",
"next-mdx-remote": "^4.0.3",
"next-themes": "^0.2.0",
"next-videos": "^1.4.1",
"react": "^18.2.0",
"react-confirm-alert": "^3.0.2",
"react-dom": "^18.2.0",
"react-hook-form": "^7.33.1",
"react-redux": "^8.0.2",
"react-toastify": "^9.0.5",
"react-yandex-metrika": "^2.6.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"remark-code-titles": "^0.1.2",
"remark-gfm": "^3.0.1"
},
"devDependencies": {
"@testing-library/react": "^13.3.0",
"@types/gtag.js": "^0.0.10",
"@types/jest": "^28.1.4",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"@typescript-eslint/eslint-plugin": "^5.30.3",
"autoprefixer": "^10.4.7",
"babel-jest": "^28.1.2",
"eslint": "^8.19.0",
"eslint-config-next": "^12.2.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.30.1",
"husky": "^8.0.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^28.1.2",
"jest-watch-typeahead": "^1.1.0",
"lint-staged": "^13.0.3",
"postcss": "^8.4.14",
"prettier": "^2.7.1",
"rehype": "^12.0.1",
"tailwindcss": "^3.1.4",
"typescript": "^4.7.4"
}
}

@ -0,0 +1,18 @@
import { ThemeProvider } from 'next-themes';
import type { AppProps } from 'next/app';
import React from 'react';
import { Provider } from 'react-redux';
import '../styles/globals.css';
import { store } from '../redux/store';
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
return (
<ThemeProvider attribute="class" enableSystem={false} defaultTheme="light">
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</ThemeProvider>
);
};
export default MyApp;

@ -0,0 +1,30 @@
import Document, { Head, Html, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render(): JSX.Element {
return (
<Html lang="ru">
<Head />
<body className="bg-white dark:bg-black text-gray-900 dark:text-white">
<script type="text/javascript"
dangerouslySetInnerHTML={{
__html: `
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(89626868, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
`,
}}
/>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

@ -0,0 +1,20 @@
import React from 'react';
import Layout from '../components/Layout';
export const About = (): JSX.Element => {
return (
<Layout
customMeta={{
title: 'О нас',
}}
>
<h2>RobotTop</h2>
<p>Организатор соревнований лаборатория робототехники Krasnikov Robotics</p>
<p>Занимаемся робототехникой с 2006 года</p>
<p>Наши ученики участники так соревнований как EUROBOT, Робофест, FLL, РТК</p>
<p>В 2022 году проводим первый Краевой робототехнический фестиваль на базе МАОУ СОШ 103 </p>
</Layout>
);
};
export default About;

@ -0,0 +1,23 @@
import React from 'react';
import Layout from '../components/Layout';
import { Link } from '../components/UX';
export const About = (): JSX.Element => {
return (
<Layout
customMeta={{
title: 'Контакты',
}}
>
<h1>РоботТоп</h1>
<p><b>Организатор соревнований</b> <Link href="https://school103.centerstart.ru/sveden/common" >МАОУ СОШ 103 г. Краснодар</Link> </p>
<p><b>Главный судья соревнований</b><Link href="https://krasnikov.pro"> Красников Павел Геннадьевич </Link> -
<Link href="tel:+7-918-945-80-44"> 8-918-945-80-44 </Link> -
<Link href="https://t.me/krasnikovPavel"> Telegram</Link>
</p>
<p><b>Вопросы по соревнованиям можно задавать в </b><Link href="https://t.me/robotop_competition"> Telegram группе </Link></p>
</Layout>
);
};
export default About;

@ -0,0 +1,52 @@
import { format, parseISO } from 'date-fns';
import { GetStaticProps } from 'next';
import Link from 'next/link';
import React from 'react';
import Layout from '../components/Layout';
import { getAllPosts } from '../lib/api';
import { PostType } from '../types/post';
type IndexProps = {
posts: PostType[];
};
export const Index = ({ posts }: IndexProps): JSX.Element => {
return (
<Layout>
<h1>Информатика</h1>
{posts.map((post) => (
<article key={post.slug} className="mt-12">
<p className="mb-1 text-sm text-gray-500 dark:text-gray-400">
{format(parseISO(post.date), 'MMMM dd, yyyy')}
</p>
<h1 className="mb-2 text-xl">
<Link as={`/posts/${post.slug}`} href={`/posts/[slug]`}>
<a className="text-gray-900 dark:text-white dark:hover:text-blue-400">
{post.title}
</a>
</Link>
</h1>
<p className="mb-3">{post.description}</p>
<p>
<Link as={`/posts/${post.slug}`} href={`/posts/[slug]`}>
<a>Подробнее...</a>
</Link>
</p>
</article>
))}
<div>
</div>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async () => {
const posts = getAllPosts(['date', 'description', 'slug', 'title']);
return {
props: { posts },
};
};
export default Index;

@ -0,0 +1,98 @@
import { format, parseISO } from 'date-fns';
import fs from 'fs';
import matter from 'gray-matter';
import { GetStaticPaths, GetStaticProps } from 'next';
import { serialize } from 'next-mdx-remote/serialize';
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import path from 'path';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import Layout, { WEBSITE_HOST_URL } from '../../components/Layout';
import { MetaProps } from '../../types/layout';
import { PostType } from '../../types/post';
import { postFilePaths, POSTS_PATH } from '../../utils/mdxUtils';
import { Video } from '../../components/Video'
// Custom components/renderers to pass to MDX.
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
// to handle import statements. Instead, you must include components in scope
// here.
const components = {
Head,
Image,
Link,
Video
};
type PostPageProps = {
source: MDXRemoteSerializeResult;
frontMatter: PostType;
};
const PostPage = ({ source, frontMatter }: PostPageProps): JSX.Element => {
const customMeta: MetaProps = {
title: `${frontMatter.title} - RoboTop`,
description: frontMatter.description,
image: `${WEBSITE_HOST_URL}${frontMatter.image}`,
date: frontMatter.date,
type: 'article',
};
return (
<Layout customMeta={customMeta}>
<article className="max-w">
<h1 className="mb-3 text-gray-900 dark:text-white">
{frontMatter.title}
</h1>
<p className="mb-10 text-sm text-gray-500 dark:text-gray-400">
{format(parseISO(frontMatter.date), 'MMMM dd, yyyy')}
</p>
<div className="prose dark:prose-dark">
<MDXRemote {...source} components={components} />
</div>
</article>
</Layout>
);
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const postFilePath = path.join(POSTS_PATH, `${params.slug}.mdx`);
const source = fs.readFileSync(postFilePath);
const { content, data } = matter(source);
const mdxSource = await serialize(content, {
// Optionally pass remark/rehype plugins
mdxOptions: {
remarkPlugins: [require('remark-code-titles')],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
},
scope: data,
});
return {
props: {
source: mdxSource,
frontMatter: data,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const paths = postFilePaths
// Remove file extensions for page paths
.map((path) => path.replace(/\.mdx?$/, ''))
// Map the path into the static paths object required by Next.js
.map((slug) => ({ params: { slug } }));
return {
paths,
fallback: false,
};
};
export default PostPage;

@ -0,0 +1,8 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: { config: './tailwind.config.js' },
autoprefixer: {},
},
};

@ -0,0 +1,57 @@
---
title: Практическая работа - списки
description: Создание списков и оглавления
date: '2023-02-01'
---
* Вам необходимо создать заголовок документа и три списка **Маркированный**, **Нумерованный**, **Многоуровневый**.
* Ниже приведен пример как должен выглядеть документ и его содержание.
<Image
alt={`Создание списков и оглавления`}
src={`/informatica/4.5_lists/exercise.png`}
width={1073}
height={489}
priority
/>
* Текст
> Маркированные и многоуровневые списки
Программа MS Word имеет в своем функционале следующие три списка:
Маркированный
Нумерованный
Многоуровневый
Состав системного блока:
Материнская плата
Процессор
Видеокарта
Оперативная память
Жесткий диск и SSD
Привод
Охлаждение
Блок питания
Процессоры Intel:
Intel Core i9-13900KS
Intel Xeon Platinum 8380
Intel Core i9-13900K
Intel Core i9-13900KF
Intel Core i9-13900F
Intel Xeon Platinum 8358
Intel Xeon W-3375
Intel Xeon Gold 6348
Socket процессора:
LGA1150
Intel Core i7-4790K
Intel Xeon E3-1285 v4
Intel Xeon E3-1285L v4
LGA1200
Intel Xeon W-1390P
Intel Xeon W-1370P
Intel Core i9-11900K
<Video
src={`/informatica/4.5_lists/word_lists.mp4`}
/>
[На главную](/)

@ -0,0 +1,15 @@
import { configureStore } from '@reduxjs/toolkit';
import userSlice from './user/slice';
import { useDispatch } from 'react-redux';
export const store = configureStore({
reducer: {userSlice},
})
export type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useAppDispatch = () => useDispatch<AppDispatch>();

@ -0,0 +1,13 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { User } from './types';
export const fetchUser = createAsyncThunk(
'User/fetchUserStatus',
async () => {
const { data } = await axios.get<User[]>(`/api/loadingLegisteredCommands`);
// eslint-disable-next-line no-console
//console.log(data);
return data;
},
);

@ -0,0 +1,4 @@
export * from './selectors';
export * from './asyncActions';
export * from './slice';
export * from './types';

@ -0,0 +1,4 @@
import { RootState } from '../store';
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const selectUserData = (state: RootState) => state.userSlice;

@ -0,0 +1,39 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { fetchUser } from './asyncActions';
import { User, Status, UserSliceState } from './types';
const initialState: UserSliceState = {
user_items: [],
user_status: Status.LOADING, // loading | success | error
};
const userSlice = createSlice({
name: 'categories',
initialState,
reducers: {
setUser(user_status, action: PayloadAction<User[]>) {
user_status.user_items = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetchUser.pending, (user_status) => {
user_status.user_status = Status.LOADING;
user_status.user_items = [];
});
builder.addCase(fetchUser.fulfilled, (user_status, action) => {
user_status.user_status = Status.SUCCESS;
user_status.user_items = action.payload;
});
builder.addCase(fetchUser.rejected, (user_status) => {
user_status.user_status = Status.ERROR;
user_status.user_items = [];
});
},
});
export const { setUser } = userSlice.actions;
export default userSlice.reducer;

@ -0,0 +1,21 @@
export type User = {
team_name: string;
name_team_coach: string;
training_institution_team: string;
name_first_participant: string;
name_second_participant: string;
name_third_party: string;
classTeam: string[];
};
export enum Status {
LOADING = 'loading',
SUCCESS = 'completed',
ERROR = 'error',
}
export interface UserSliceState {
user_items: User[];
user_status: Status;
}

@ -0,0 +1,13 @@
import mysql from "mysql2";
const pool = mysql.createPool({
host: process.env.HOST,
user: process.env.DATABASE,
database: process.env.DATABASE,
password: process.env.PASSWORD,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
module.exports = pool;

@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
const pool = require("./connect");
interface definitionInterface{
(message:string):void;
}
export default function Insert(sql: string, argument: string[], callback: definitionInterface) {
pool.query(sql, argument, (err, result) => {
if (err) {
return console.error(err.message);
}
callback(result.insertId);
pool.releaseConnection(pool);
});
}

@ -0,0 +1,13 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
const pool = require("./connect");
import {QueryError} from 'mysql2';
interface definitionInterface{
(message:string):void;
}
export default function Select(sql: string, callback: definitionInterface) {
pool.query(sql, (err: QueryError, rows: string) => {
callback(rows);
pool.releaseConnection(pool);
});
}

@ -0,0 +1,119 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1 {
@apply mb-6 text-3xl font-semibold;
}
h2 {
@apply text-2xl font-semibold;
}
p {
@apply mb-4;
}
a {
@apply text-blue-500 hover:text-blue-400 dark:text-blue-400 dark:hover:text-blue-300;
}
}
/* Post styles */
.prose {
max-width: 100vh;
}
.prose pre {
@apply bg-gray-50 border border-gray-200 dark:border-gray-700 dark:bg-gray-900;
}
.prose code {
@apply text-gray-800 dark:text-gray-200 px-1 py-0.5 border border-gray-100 dark:border-gray-800 rounded-md bg-gray-100 dark:bg-gray-900;
}
.prose img {
/* Don't apply styles to next/image */
@apply m-0;
}
/* Prism Styles */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
@apply text-gray-700 dark:text-gray-300;
}
.token.punctuation {
@apply text-gray-700 dark:text-gray-300;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
@apply text-green-500;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
@apply text-purple-500;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
@apply text-yellow-500;
}
.token.atrule,
.token.attr-value,
.token.keyword {
@apply text-blue-500;
}
.token.function,
.token.class-name {
@apply text-pink-500;
}
.token.regex,
.token.important,
.token.variable {
@apply text-yellow-500;
}
code[class*='language-'],
pre[class*='language-'] {
@apply text-gray-800 dark:text-gray-50;
}
pre::-webkit-scrollbar {
display: none;
}
pre {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Remark Styles */
.remark-code-title {
@apply text-gray-800 dark:text-gray-200 px-5 py-3 border border-b-0 border-gray-200 dark:border-gray-700 rounded-t bg-gray-200 dark:bg-gray-800 text-sm font-mono font-bold;
}
.remark-code-title + pre {
@apply mt-0 rounded-t-none;
}
.mdx-marker {
@apply block -mx-4 px-4 bg-gray-100 dark:bg-gray-800 border-l-4 border-blue-500;
}

@ -0,0 +1,75 @@
const { spacing } = require('tailwindcss/defaultTheme');
module.exports = {
mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class', // or 'media' or 'class'
theme: {
extend: {
typography: (theme) => ({
DEFAULT: {
css: {
color: theme('colors.gray.700'),
a: {
color: theme('colors.blue.500'),
'&:hover': {
color: theme('colors.blue.700'),
},
code: { color: theme('colors.blue.400') },
},
'h2,h3,h4': {
'scroll-margin-top': spacing[32],
},
code: { color: theme('colors.pink.500') },
'blockquote p:first-of-type::before': false,
'blockquote p:last-of-type::after': false,
},
},
dark: {
css: {
color: theme('colors.gray.300'),
a: {
color: theme('colors.blue.400'),
'&:hover': {
color: theme('colors.blue.600'),
},
code: { color: theme('colors.blue.400') },
},
blockquote: {
borderLeftColor: theme('colors.gray.800'),
color: theme('colors.gray.300'),
},
'h2,h3,h4': {
color: theme('colors.gray.100'),
'scroll-margin-top': spacing[32],
},
hr: { borderColor: theme('colors.gray.800') },
ol: {
li: {
'&:before': { color: theme('colors.gray.500') },
},
},
ul: {
li: {
'&:before': { backgroundColor: theme('colors.gray.500') },
},
},
strong: { color: theme('colors.gray.300') },
thead: {
color: theme('colors.gray.100'),
},
tbody: {
tr: {
borderBottomColor: theme('colors.gray.700'),
},
},
},
},
}),
},
},
variants: {
typography: ['dark'],
},
plugins: [require('@tailwindcss/typography')],
};

@ -0,0 +1 @@
module.exports = 'test-file-stub';

@ -0,0 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Home page matches snapshot 1`] = `
<DocumentFragment>
<div
class="jsx-1276654382 container"
>
<main
class="jsx-1276654382"
>
<h1
class="jsx-1276654382 title"
>
Welcome to
<a
class="jsx-1276654382"
href="https://nextjs.org"
>
Next.js!
</a>
</h1>
<p
class="jsx-1276654382 description"
>
Get started by editing
<code
class="jsx-1276654382"
>
pages/index.tsx
</code>
</p>
<button
class="jsx-1276654382"
>
Test Button
</button>
<div
class="jsx-1276654382 grid"
>
<a
class="jsx-1276654382 card"
href="https://nextjs.org/docs"
>
<h3
class="jsx-1276654382"
>
Documentation →
</h3>
<p
class="jsx-1276654382"
>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
class="jsx-1276654382 card"
href="https://nextjs.org/learn"
>
<h3
class="jsx-1276654382"
>
Learn →
</h3>
<p
class="jsx-1276654382"
>
Learn about Next.js in an interactive course with quizzes!
</p>
</a>
<a
class="jsx-1276654382 card"
href="https://github.com/vercel/next.js/tree/master/examples"
>
<h3
class="jsx-1276654382"
>
Examples →
</h3>
<p
class="jsx-1276654382"
>
Discover and deploy boilerplate example Next.js projects.
</p>
</a>
<a
class="jsx-1276654382 card"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
>
<h3
class="jsx-1276654382"
>
Deploy →
</h3>
<p
class="jsx-1276654382"
>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer
class="jsx-1276654382"
>
<a
class="jsx-1276654382"
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
rel="noopener noreferrer"
target="_blank"
>
Powered by
<div
style="display: inline-block; max-width: 100%; overflow: hidden; position: relative; box-sizing: border-box; margin: 0px;"
>
<div
style="box-sizing: border-box; display: block; max-width: 100%;"
>
<img
alt=""
aria-hidden="true"
role="presentation"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iMzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmVyc2lvbj0iMS4xIi8+"
style="max-width: 100%; display: block; margin: 0px; padding: 0px;"
/>
</div>
<img
alt="Vercel Logo"
decoding="async"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
style="visibility: hidden; position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; box-sizing: border-box; padding: 0px; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;"
/>
</div>
</a>
</footer>
</div>
</DocumentFragment>
`;

@ -0,0 +1,24 @@
import { render } from '@testing-library/react';
// import { ThemeProvider } from "my-ui-lib"
// import { TranslationProvider } from "my-i18n-lib"
// import defaultStrings from "i18n/en-x-default"
const Providers = ({ children }) => {
return children;
// return (
// <ThemeProvider theme="light">
// <TranslationProvider messages={defaultStrings}>
// {children}
// </TranslationProvider>
// </ThemeProvider>
// )
};
const customRender = (ui, options = {}) =>
render(ui, { wrapper: Providers, ...options });
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"exclude": [
"node_modules",
".next",
"out"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.js"
]
}

@ -0,0 +1,9 @@
import { PostType } from './post';
export interface MetaProps
extends Pick<PostType, 'date' | 'description' | 'image' | 'title'> {
/**
* For the meta tag `og:type`
*/
type?: string;
}

@ -0,0 +1,7 @@
export type PostType = {
date?: string;
description?: string;
image?: string;
slug: string;
title: string;
};

@ -0,0 +1,11 @@
import fs from 'fs';
import path from 'path';
// POSTS_PATH is useful when you want to get the path to a specific file
export const POSTS_PATH = path.join(process.cwd(), 'posts');
// postFilePaths is the list of all mdx files inside the POSTS_PATH directory
export const postFilePaths = fs
.readdirSync(POSTS_PATH)
// Only include md(x) files
.filter((path) => /\.mdx?$/.test(path));

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save