From 8af5253a0004aa0ddece4a4fa86e2a70f5b17c7e Mon Sep 17 00:00:00 2001 From: joker Date: Wed, 1 Feb 2023 16:31:56 +0300 Subject: [PATCH] first commit --- README.md | 26 + components/Head.tsx | 45 + components/Layout.tsx | 46 + components/LoadingTeamsForm.tsx | 52 + components/Navigation.tsx | 91 + components/RegistrationForm.tsx | 136 + components/ThemeSwitch.tsx | 102 + components/UX/Alert.tsx | 7 + components/UX/Input.tsx | 27 + components/UX/Link.tsx | 14 + components/UX/Select.tsx | 40 + components/UX/index.ts | 4 + components/Video.tsx | 14 + env.local | 6 + jest.config.js | 17 + lib/api.ts | 45 + mysql/members.sql | 63 + next-env.d.ts | 5 + next.config.mjs | 17 + package.json | 79 + pages/_app.tsx | 18 + pages/_document.tsx | 30 + pages/about.tsx | 20 + pages/contacts.tsx | 23 + pages/index.tsx | 52 + pages/posts/[slug].tsx | 98 + postcss.config.js | 8 + posts/lists.mdx | 57 + redux/store.ts | 15 + redux/user/asyncActions.ts | 13 + redux/user/index.ts | 4 + redux/user/selectors.ts | 4 + redux/user/slice.ts | 39 + redux/user/types.ts | 21 + server/db/connect.ts | 13 + server/db/insert.ts | 15 + server/db/select.ts | 13 + styles/globals.css | 119 + tailwind.config.js | 75 + test/__mocks__/fileMock.js | 1 + test/pages/__snapshots__/index.test.tsx.snap | 137 + test/testUtils.ts | 24 + tsconfig.json | 33 + types/layout.ts | 9 + types/post.ts | 7 + utils/mdxUtils.ts | 11 + yarn.lock | 7904 ++++++++++++++++++ 47 files changed, 9599 insertions(+) create mode 100644 README.md create mode 100644 components/Head.tsx create mode 100644 components/Layout.tsx create mode 100644 components/LoadingTeamsForm.tsx create mode 100644 components/Navigation.tsx create mode 100644 components/RegistrationForm.tsx create mode 100644 components/ThemeSwitch.tsx create mode 100644 components/UX/Alert.tsx create mode 100644 components/UX/Input.tsx create mode 100644 components/UX/Link.tsx create mode 100644 components/UX/Select.tsx create mode 100644 components/UX/index.ts create mode 100644 components/Video.tsx create mode 100644 env.local create mode 100644 jest.config.js create mode 100644 lib/api.ts create mode 100644 mysql/members.sql create mode 100644 next-env.d.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 pages/_app.tsx create mode 100644 pages/_document.tsx create mode 100644 pages/about.tsx create mode 100644 pages/contacts.tsx create mode 100644 pages/index.tsx create mode 100644 pages/posts/[slug].tsx create mode 100644 postcss.config.js create mode 100644 posts/lists.mdx create mode 100644 redux/store.ts create mode 100644 redux/user/asyncActions.ts create mode 100644 redux/user/index.ts create mode 100644 redux/user/selectors.ts create mode 100644 redux/user/slice.ts create mode 100644 redux/user/types.ts create mode 100644 server/db/connect.ts create mode 100644 server/db/insert.ts create mode 100644 server/db/select.ts create mode 100644 styles/globals.css create mode 100644 tailwind.config.js create mode 100644 test/__mocks__/fileMock.js create mode 100644 test/pages/__snapshots__/index.test.tsx.snap create mode 100644 test/testUtils.ts create mode 100644 tsconfig.json create mode 100644 types/layout.ts create mode 100644 types/post.ts create mode 100644 utils/mdxUtils.ts create mode 100644 yarn.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dd1d6f --- /dev/null +++ b/README.md @@ -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 diff --git a/components/Head.tsx b/components/Head.tsx new file mode 100644 index 0000000..288da04 --- /dev/null +++ b/components/Head.tsx @@ -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 ( + + {meta.title} + + + + + + + + + + + + + {meta.date && ( + + )} + + ); +}; + +export default Head; diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 0000000..1fb4209 --- /dev/null +++ b/components/Layout.tsx @@ -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 ( + <> + +
+
+
+ + +
+
+
+
+
+ {children} +
+
+ + + ); +}; + +export default Layout; diff --git a/components/LoadingTeamsForm.tsx b/components/LoadingTeamsForm.tsx new file mode 100644 index 0000000..f1f5bdc --- /dev/null +++ b/components/LoadingTeamsForm.tsx @@ -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 = ({ + 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 ( + + + {team_name} + + + {name_team_coach} + + + {training_institution_team} + + + {name_first_participant +', ' + name_second_participant + ', ' + name_third_party} + + + { flatten(classTeam)} + + + ); +}; + +export default LoadingTeamsForm; diff --git a/components/Navigation.tsx b/components/Navigation.tsx new file mode 100644 index 0000000..cba204e --- /dev/null +++ b/components/Navigation.tsx @@ -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 ( + + ); +}; + +export default Navigation; diff --git a/components/RegistrationForm.tsx b/components/RegistrationForm.tsx new file mode 100644 index 0000000..dff393f --- /dev/null +++ b/components/RegistrationForm.tsx @@ -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 = data => { + fetch('/api/registration', { method: 'POST', body: Object.values(data) as any}) + .then((data) => { + props.updateData(data); + }) + methods.reset(defaultValues); + } + return ( + <> +
+
+
+
+

Регистрация команды

+

Введите актуальные данные команды

+

От каждого учебного заведения может быть зарегистрированно неограниченое количеставо команд

+

Подписывайтесь на наш + Telegram канал + , что-бы быть в курсе новостей про соревнование

+
+
+
+ +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ ()[]\\.,;: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,}))$/ })`} /> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + ) +} \ No newline at end of file diff --git a/components/UX/Link.tsx b/components/UX/Link.tsx new file mode 100644 index 0000000..a43d3b1 --- /dev/null +++ b/components/UX/Link.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +type Props = { + children: string; + href: string; + } + +export const Link: React.FC = ({children, href}) => { + return( + <> + {children} + + ) +} \ No newline at end of file diff --git a/components/UX/Select.tsx b/components/UX/Select.tsx new file mode 100644 index 0000000..9cb5e1d --- /dev/null +++ b/components/UX/Select.tsx @@ -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 = ({text, name}) => { + const { register } = useFormContext(); + return( + <> + + + + ) +} \ No newline at end of file diff --git a/components/UX/index.ts b/components/UX/index.ts new file mode 100644 index 0000000..29a0e80 --- /dev/null +++ b/components/UX/index.ts @@ -0,0 +1,4 @@ +export * from './Select' +export * from './Alert' +export * from './Input' +export * from './Link' \ No newline at end of file diff --git a/components/Video.tsx b/components/Video.tsx new file mode 100644 index 0000000..a36c41d --- /dev/null +++ b/components/Video.tsx @@ -0,0 +1,14 @@ +export interface VideoProps { + title: string; + src: string; +} + +export function Video(props: VideoProps) { + return ( + + ); +} + +export default Video; \ No newline at end of file diff --git a/env.local b/env.local new file mode 100644 index 0000000..c972102 --- /dev/null +++ b/env.local @@ -0,0 +1,6 @@ +// .env.local + +USER_="Имя пользователя базы данных" +HOST="IP адрес базы данных" +DATABASE="Имя базы данных" +PASSWORD="Пароль доступа к базе данных" \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..162abff --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + roots: [''], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'], + testPathIgnorePatterns: ['[/\\\\](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)$': '/test/__mocks__/fileMock.js', + }, +}; diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..72903b9 --- /dev/null +++ b/lib/api.ts @@ -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; +} diff --git a/mysql/members.sql b/mysql/members.sql new file mode 100644 index 0000000..f03ae0f --- /dev/null +++ b/mysql/members.sql @@ -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 */; diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..51e330c --- /dev/null +++ b/next.config.mjs @@ -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'], +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..fcb29dc --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000..bb1b75e --- /dev/null +++ b/pages/_app.tsx @@ -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 ( + + + + + + ); +}; + +export default MyApp; diff --git a/pages/_document.tsx b/pages/_document.tsx new file mode 100644 index 0000000..2c979bb --- /dev/null +++ b/pages/_document.tsx @@ -0,0 +1,30 @@ +import Document, { Head, Html, Main, NextScript } from 'next/document'; +class MyDocument extends Document { + render(): JSX.Element { + return ( + + + +