commit
8af5253a00
@ -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 */; |
@ -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)); |
Loading…
Reference in new issue