├── .env
├── .eslintrc.json
├── .github
└── workflows
│ ├── build.yml
│ └── playwright.yml
├── .gitignore
├── LICENSE
├── README.md
├── components
├── ContentEditor
│ ├── ContentEditor.tsx
│ └── index.ts
├── Date
│ ├── date.module.css
│ ├── date.tsx
│ └── index.ts
├── Footer
│ ├── Footer.module.css
│ ├── Footer.tsx
│ └── index.ts
├── Form
│ ├── CommentForm.jsx
│ └── Form.jsx
├── Header
│ ├── Header.tsx
│ └── index.ts
├── Menu
│ ├── Menu.tsx
│ ├── index.ts
│ └── menu.module.css
└── index.ts
├── data
├── infrastructure
│ ├── Auth0_Docs.md
│ ├── GitHub_Actions.md
│ └── Vercel-Doc.md
├── main.json
├── programming
│ ├── React_TypeScript_Cheatsheets.md
│ ├── Shell_scripting_with_NodeJS.md
│ └── Tackling_TypeScript.md
├── soft-skills
│ ├── Pomodoro_Technique.md
│ ├── jedi-technics.md
│ └── junior-dev-resources.md
└── test-works
│ └── my-first-test-work.md
├── e2e
└── example.spec.ts
├── layouts
├── PageLayout
│ ├── PagesLayout.tsx
│ ├── index.ts
│ └── pageLayout.module.css
└── index.ts
├── lib
├── dbConnect.js
├── getAllFilesIds.ts
└── parseMarkdownFile.ts
├── models
└── Comments.js
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── api
│ ├── comments
│ │ ├── [id]
│ │ │ └── index.js
│ │ └── index.js
│ └── hello.ts
├── comments
│ ├── [pageid]
│ │ ├── edit.js
│ │ └── index.jsx
│ └── index.jsx
├── index.tsx
├── infrastructure
│ ├── [pageid].tsx
│ └── index.tsx
├── programming
│ ├── [pageid].tsx
│ └── index.tsx
├── soft-skills
│ ├── [pageid].tsx
│ └── index.tsx
└── test-works
│ ├── [pageid].tsx
│ └── index.tsx
├── playwright.config.ts
├── pnpm-lock.yaml
├── tests-examples
└── demo-todo-app.spec.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | MONGODB_URI=
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v3
9 |
10 | - name: Setup Node.js environment
11 | uses: actions/setup-node@v3.5.0
12 | with:
13 | node-version: 18
14 |
15 | - uses: pnpm/action-setup@v2.2.2
16 | with:
17 | version: 7.12.2
18 |
19 | - name: Install dependencies
20 | run: pnpm install --no-frozen-lockfile
21 |
22 | - name: Build
23 | run: pnpm run build
24 | env:
25 | MONGODB_URI: ${{ secrets.MONGODB_URI }}
26 |
27 | - name: Static HTML Export
28 | run: pnpm run export
29 |
30 | - name: Disable Jekyl # https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/
31 | run: touch ./out/.nojekyll
32 |
33 | - name: Delpoy 🚀
34 | uses: JamesIves/github-pages-deploy-action@3.7.1
35 | with:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | BRANCH: gh-pages
38 | FOLDER: out
39 |
--------------------------------------------------------------------------------
/.github/workflows/playwright.yml:
--------------------------------------------------------------------------------
1 | name: Playwright Tests
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 | jobs:
8 | test:
9 | timeout-minutes: 60
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: 18
17 |
18 | - uses: pnpm/action-setup@v2.2.2
19 | with:
20 | version: 7.12.2
21 |
22 | - name: Install dependencies
23 | run: pnpm install --no-frozen-lockfile
24 |
25 | - name: Install Playwright Browsers
26 | run: npx playwright install --with-deps
27 |
28 | - name: Run Playwright tests
29 | run: npx playwright test
30 |
31 | - uses: actions/upload-artifact@v3
32 | if: always()
33 | with:
34 | name: playwright-report
35 | path: playwright-report/
36 | retention-days: 30
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # editors
4 | .idea
5 |
6 | # dependencies
7 | node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # next.js
15 | /.next/
16 | /out/
17 |
18 | # production
19 | /build
20 |
21 | # misc
22 | .DS_Store
23 | *.pem
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 | .pnpm-debug.log*
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 | next-env.d.ts
40 | /test-results/
41 | /playwright-report/
42 | /playwright/.cache/
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Easy Deep Learning
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # personal-learn-path
2 | Learn path constructor and knowledge base
3 |
4 | ## Tech stack
5 | - NextJS
6 | - MongoDB
7 |
--------------------------------------------------------------------------------
/components/ContentEditor/ContentEditor.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { Editor, EditorState, convertFromRaw, RawDraftContentState, convertToRaw } from 'draft-js'
3 | import 'draft-js/dist/Draft.css'
4 |
5 | type ContentEditorType = {
6 | onSave: (content: object) => void
7 | }
8 |
9 | const ContentEditor = ({ onSave }: ContentEditorType) => {
10 | const [editorState, setEditorState] = useState(EditorState.createWithContent(emptyContentState))
11 |
12 | return (
13 |
14 | {
18 | console.log('data: ', data)
19 | const content = editorState.getCurrentContent();
20 | console.log('content: ', convertToRaw(content));
21 | }}
22 | />
23 |
24 | )
25 | }
26 |
27 | const emptyContentState = convertFromRaw({
28 | entityMap: {},
29 | blocks: [
30 | {
31 | text: '',
32 | key: 'foo',
33 | type: 'unstyled',
34 | entityRanges: [],
35 | },
36 | ],
37 | } as unknown as RawDraftContentState)
38 |
39 | export default ContentEditor
40 |
--------------------------------------------------------------------------------
/components/ContentEditor/index.ts:
--------------------------------------------------------------------------------
1 | import ContentEditor from './ContentEditor'
2 |
3 | export {
4 | ContentEditor
5 | }
6 |
--------------------------------------------------------------------------------
/components/Date/date.module.css:
--------------------------------------------------------------------------------
1 | .day {}
2 |
3 | .month {}
4 |
5 | .year {}
6 |
--------------------------------------------------------------------------------
/components/Date/date.tsx:
--------------------------------------------------------------------------------
1 | import { parseISO, format } from 'date-fns'
2 | import { ru } from 'date-fns/locale'
3 | import dateStyles from './date.module.css'
4 |
5 | type DatePropsType = {
6 | dateString: string
7 | }
8 |
9 | export default function Date({ dateString }: DatePropsType) {
10 | const date = parseISO(dateString)
11 | return (
12 |
13 | {format(date, 'dd', { locale: ru })}
14 | {format(date, 'mm', { locale: ru })}
15 | {format(date, 'yyyy', { locale: ru })}
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/components/Date/index.ts:
--------------------------------------------------------------------------------
1 | import Date from './date'
2 |
3 | export { Date }
4 |
--------------------------------------------------------------------------------
/components/Footer/Footer.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | display: flex;
3 | }
4 |
5 | .footer__item {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/components/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function Footer () {
4 | return (
5 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/components/Footer/index.ts:
--------------------------------------------------------------------------------
1 | import Footer from './Footer'
2 |
3 | export { Footer }
4 |
--------------------------------------------------------------------------------
/components/Form/CommentForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useRouter } from 'next/router'
3 | import { mutate } from 'swr'
4 |
5 | const CommentForm = ({ formId, commentForm, forNewComment = true }) => {
6 | const router = useRouter()
7 | const contentType = 'application/json'
8 | const [errors, setErrors] = useState({})
9 | const [message, setMessage] = useState('')
10 |
11 | const [form, setForm] = useState({
12 | name: commentForm.name,
13 | email: commentForm.email,
14 | movie_id: commentForm.movie_id,
15 | text: commentForm.text,
16 | date: commentForm.date,
17 | })
18 |
19 | /* The PUT method edits an existing entry in the mongodb database. */
20 | const putData = async (form) => {
21 | const { id } = router.query
22 |
23 | try {
24 | const res = await fetch(`/api/comments/${id}`, {
25 | method: 'PUT',
26 | headers: {
27 | Accept: contentType,
28 | 'Content-Type': contentType,
29 | },
30 | body: JSON.stringify(form),
31 | })
32 |
33 | // Throw error with status code in case Fetch API req failed
34 | if (!res.ok) {
35 | throw new Error(res.status)
36 | }
37 |
38 | const { data } = await res.json()
39 |
40 | mutate(`/api/comments/${id}`, data, false) // Update the local data without a revalidation
41 | router.push('/')
42 | } catch (error) {
43 | setMessage('Failed to update comment')
44 | }
45 | }
46 |
47 | /* The POST method adds a new entry in the mongodb database. */
48 | const postData = async (form) => {
49 | try {
50 | const res = await fetch('/api/comments', {
51 | method: 'POST',
52 | headers: {
53 | Accept: contentType,
54 | 'Content-Type': contentType,
55 | },
56 | body: JSON.stringify(form),
57 | })
58 |
59 | // Throw error with status code in case Fetch API req failed
60 | if (!res.ok) {
61 | throw new Error(res.status)
62 | }
63 |
64 | router.push('/')
65 | } catch (error) {
66 | setMessage('Failed to add comment')
67 | }
68 | }
69 |
70 | const handleChange = (e) => {
71 | const target = e.target
72 | const value =
73 | target.name === 'poddy_trained' ? target.checked : target.value
74 | const name = target.name
75 |
76 | setForm({
77 | ...form,
78 | [name]: value,
79 | })
80 | }
81 |
82 | /* Makes sure comment info is filled for comment name, owner name, species, and image url*/
83 | const formValidate = () => {
84 | let err = {}
85 | if (!form.name) err.name = 'Name is required'
86 | if (!form.owner_name) err.owner_name = 'Owner is required'
87 | if (!form.species) err.species = 'Species is required'
88 | if (!form.image_url) err.image_url = 'Image URL is required'
89 | return err
90 | }
91 |
92 | const handleSubmit = (e) => {
93 | e.preventDefault()
94 | const errs = formValidate()
95 | if (Object.keys(errs).length === 0) {
96 | forNewComment ? postData(form) : putData(form)
97 | } else {
98 | setErrors({ errs })
99 | }
100 | }
101 |
102 | return (
103 | <>
104 |
155 | {message}
156 |
157 | {Object.keys(errors).map((err, index) => (
158 |
{err}
159 | ))}
160 |
161 | >
162 | )
163 | }
164 |
165 | export default CommentForm
166 |
--------------------------------------------------------------------------------
/components/Form/Form.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useRouter } from 'next/router'
3 | import { mutate } from 'swr'
4 |
5 | const SECTION = '_section_here_'
6 |
7 | const Form = ({ formId, _section_here_Form, forNewComment = true }) => {
8 | const router = useRouter()
9 | const contentType = 'application/json'
10 | const [errors, setErrors] = useState({})
11 | const [message, setMessage] = useState('')
12 |
13 | const [form, setForm] = useState({})
14 |
15 | /* The PUT method edits an existing entry in the mongodb database. */
16 | const putData = async (form) => {
17 | const { id } = router.query
18 |
19 | try {
20 | const res = await fetch(`/api/${SECTION}/${id}`, {
21 | method: 'PUT',
22 | headers: {
23 | Accept: contentType,
24 | 'Content-Type': contentType,
25 | },
26 | body: JSON.stringify(form),
27 | })
28 |
29 | // Throw error with status code in case Fetch API req failed
30 | if (!res.ok) {
31 | throw new Error(res.status)
32 | }
33 |
34 | const { data } = await res.json()
35 |
36 | mutate(`/api/${SECTION}/${id}`, data, false) // Update the local data without a revalidation
37 | router.push('/')
38 | } catch (error) {
39 | setMessage('Failed to update SECTION')
40 | }
41 | }
42 |
43 | /* The POST method adds a new entry in the mongodb database. */
44 | const postData = async (form) => {
45 | try {
46 | const res = await fetch(`/api/${SECTION}`, {
47 | method: 'POST',
48 | headers: {
49 | Accept: contentType,
50 | 'Content-Type': contentType,
51 | },
52 | body: JSON.stringify(form),
53 | })
54 |
55 | // Throw error with status code in case Fetch API req failed
56 | if (!res.ok) {
57 | throw new Error(res.status)
58 | }
59 |
60 | router.push('/')
61 | } catch (error) {
62 | setMessage('Failed to add pet')
63 | }
64 | }
65 |
66 | const handleChange = (e) => {
67 | const target = e.target
68 | const value = target.value
69 | const name = target.name
70 |
71 | setForm({
72 | ...form,
73 | [name]: value,
74 | })
75 | }
76 | const formValidate = () => {
77 | let err = {}
78 | if (!form._requiredField_) err._requiredField_ = '_requiredField_ is required'
79 | return err
80 | }
81 |
82 | const handleSubmit = (e) => {
83 | e.preventDefault()
84 | const errs = formValidate()
85 | if (Object.keys(errs).length === 0) {
86 | forNewComment ? postData(form) : putData(form)
87 | } else {
88 | setErrors({ errs })
89 | }
90 | }
91 |
92 | return (
93 | <>
94 |
95 | Name
96 |
104 |
105 |
106 | Submit
107 |
108 |
109 | {message}
110 |
111 | {Object.keys(errors).map((err, index) => (
112 |
{err}
113 | ))}
114 |
115 | >
116 | )
117 | }
118 |
119 | export default Form
120 |
--------------------------------------------------------------------------------
/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from '../index'
2 |
3 | export default function Header () {
4 | return (
5 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | import Header from './Header'
2 |
3 | export { Header }
4 |
--------------------------------------------------------------------------------
/components/Menu/Menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import { NextRouter } from 'next/router'
4 | import style from './menu.module.css'
5 |
6 | interface WithRouterProps {
7 | router: NextRouter
8 | }
9 |
10 | interface ComposedComponent extends WithRouterProps {
11 | children: string
12 | href: string
13 | }
14 |
15 | export default function Menu () {
16 | return (
17 |
18 | Главная
19 | Тестовые задания
20 | Программирование
21 | Деплой
22 | Софт-Скиллы
23 | API
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components/Menu/index.ts:
--------------------------------------------------------------------------------
1 | import Menu from './Menu'
2 |
3 | export { Menu }
4 |
--------------------------------------------------------------------------------
/components/Menu/menu.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | display: flex;
3 | }
4 |
5 | @media (max-width: 500px) {
6 | .root {
7 | flex-direction: column;
8 | margin: 0 auto;
9 | max-width: 50vw;
10 | }
11 | }
12 |
13 | .item {
14 | flex-grow: 1;
15 | text-align: center;
16 | text-decoration: underline 3px #4183c4;
17 | }
18 |
19 | .item_active {
20 | color: #999999;
21 | text-decoration: none;
22 | }
23 |
24 | .item:last-child {
25 | margin-right: 0;
26 | }
27 |
28 | @media (max-width: 500px) {
29 | .item {
30 | margin: 0 0 .5em 0;
31 | text-align: left;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/index.ts:
--------------------------------------------------------------------------------
1 | import { Date } from './Date'
2 | import { Header } from './Header'
3 | import { Menu } from './Menu'
4 | import { Footer } from './Footer'
5 | import { ContentEditor } from './ContentEditor'
6 |
7 | export {
8 | Date,
9 | Header,
10 | Menu,
11 | Footer,
12 | ContentEditor,
13 | }
14 |
--------------------------------------------------------------------------------
/data/infrastructure/Auth0_Docs.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Auth0 Docs'
3 | date: '2022-10-11'
4 | ---
5 |
6 | Auth0 is an easy to implement, adaptable authentication and authorization platform.
7 |
8 | https://auth0.com/docs/
9 |
--------------------------------------------------------------------------------
/data/infrastructure/GitHub_Actions.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'GitHub Actions'
3 | date: '2022-10-11'
4 | ---
5 |
6 | Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI/CD, and combine actions in a completely customized workflow.
7 |
8 | https://docs.github.com/en/actions
9 |
10 |
11 |
--------------------------------------------------------------------------------
/data/infrastructure/Vercel-Doc.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Vercel'
3 | date: '2022-10-11'
4 | ---
5 |
6 | Vercel is the platform for frontend developers, providing the speed and reliability innovators need to create at the moment of inspiration.
7 |
8 | We enable teams to iterate quickly and develop, preview, and ship delightful user experiences. Vercel has zero-configuration support for 35+ frontend frameworks and integrates with your headless content, commerce, or database of choice.
9 |
10 | Start the tutorial or deploy a template in minutes.
11 |
--------------------------------------------------------------------------------
/data/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "My personal learning path",
3 | "title": "My personal learning path"
4 | }
5 |
--------------------------------------------------------------------------------
/data/programming/React_TypeScript_Cheatsheets.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'React TypeScript Cheatsheets'
3 | description: 'Cheatsheets for experienced React developers getting started with TypeScript'
4 | date: '2022-10-11'
5 | ---
6 |
7 |
10 |
--------------------------------------------------------------------------------
/data/programming/Shell_scripting_with_NodeJS.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Shell scripting with Node.js'
3 | date: '2022-10-11'
4 | ---
5 |
6 |
185 |
--------------------------------------------------------------------------------
/data/programming/Tackling_TypeScript.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Tackling TypeScript'
3 | date: '2022-10-11'
4 | ---
5 |
6 |
215 |
--------------------------------------------------------------------------------
/data/soft-skills/Pomodoro_Technique.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'The Pomodoro® Technique'
3 | date: '2022-10-11'
4 | ---
5 |
6 | The Pomodoro® Technique is an easy and fun way to get the most out of time management. Turn time into a valuable ally to accomplish tasks while keeping track of your progress.
7 |
8 | https://francescocirillo.com/products/the-pomodoro-technique
9 |
--------------------------------------------------------------------------------
/data/soft-skills/jedi-technics.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Джедайские техники. Максим Дорофеев.'
3 | date: '2022-10-11'
4 | ---
5 |
6 | Недавно вышла книга Путь джедая Максима Дорофеева. Перед тем как прочитать её, я решил освежить в памяти предыдущую — Джедайские техники. Заодно законспектировал.
7 |
8 | Вообще, то, что я выработал сам для себя и как работаю последние лет 5, — напоминает джедайские техники. Просто уровень «задротскости» у Дорофеева повыше. Но, например, работа со справочниками, встречами и ежедневными задачами — повторяет его методику.
9 |
10 | Работа с проектами по Дорофееву напомнила мне GTD, но GTD я терпеть не могу, а вот «джедайство» почему-то зашло. Возможно, потому что Дорофеев описывает, как работают (а точнее мешают нам работать) когнитивные искажения. Возможно, потому что написано простыми словами. Но в любом случае, поехали.
11 |
12 | И это, да, конспект огромный, приготовьте там себе перекусить что ли.
13 |
14 | https://bespoyasov.ru/blog/jedi-technics/
15 |
--------------------------------------------------------------------------------
/data/soft-skills/junior-dev-resources.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: '7 resources that made me the junior developer I am today'
3 | date: '2022-10-11'
4 | ---
5 |
6 | 💨 The FAST method to learn anything
7 | 🔁 Spaced repetition
8 | 🤝🏻 My accountability partner
9 | 👾 Scrimba Discord community
10 | 📚 Tech Resume Inside Out
11 | 🍅 The pomodoro technique
12 | 🤔 Embracing stoicism
13 |
14 | https://scrimba.com/articles/junior-dev-resources/
15 |
--------------------------------------------------------------------------------
/data/test-works/my-first-test-work.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Моя первая тестовая работа'
3 | date: '2009-11-30'
4 | ---
5 |
6 |
7 |
9 |
10 | Моя первая тестовая работа - верстка сайта "Салоны красоты Великого Новгорода"
11 |
12 |
13 | На данный момент проект salon-vn.ru является молодым быстроразвивающимся сайтом.
14 | Наша работа нацелена на то, чтобы помочь найти интересующий Вас салон красоты в Великом Новгороде и области,
15 | выбрать из огромного количества именно тот, который Вам подходит по ценам на услуги и расположению.
16 |
17 |
18 | «alexbaumgertner.github.io/test-works/salonvn» ,
19 | Салоны красоты Великого Новгорода
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/e2e/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('homepage has title and links to intro page', async ({ page }) => {
4 | // await page.goto('http://localhost:3000/'); WARNING: for local
5 | await page.goto('https://easy-deep-learning.github.io/personal-learn-path/');
6 |
7 | // Expect a title "to contain" a substring.
8 | await expect(page).toHaveTitle(/My personal/);
9 |
10 | // create a locator
11 | const getTestTasksPage = page.getByRole('link', { name: 'Тестовые задания' });
12 |
13 | // Expect an attribute "to be strictly equal" to the value.
14 | await expect(getTestTasksPage).toHaveAttribute('href', '/test-works');
15 |
16 | // Click the get started link.
17 | await getTestTasksPage.click();
18 |
19 | // Expects the URL to contain intro.
20 | await expect(page).toHaveURL(/.*works/);
21 | });
22 |
--------------------------------------------------------------------------------
/layouts/PageLayout/PagesLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Head from 'next/head'
3 | import {
4 | Header,
5 | Footer,
6 | } from '../../components'
7 | import mainData from '../../data/main.json'
8 |
9 | import pageLayoutStyles from './pageLayout.module.css'
10 |
11 | type PagesLayoutProps = {
12 | children: React.ReactNode
13 | }
14 |
15 | export default function PagesLayout ({ children }: PagesLayoutProps) {
16 | return (
17 | <>
18 |
19 |
20 |
21 |
22 |
26 |
27 |
{mainData.title}
28 |
29 |
30 |
31 |
32 | {children}
33 |
34 |
35 |
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/layouts/PageLayout/index.ts:
--------------------------------------------------------------------------------
1 | import PagesLayout from './PagesLayout'
2 |
3 | export { PagesLayout }
4 |
--------------------------------------------------------------------------------
/layouts/PageLayout/pageLayout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | max-width: 36rem;
3 | padding: 0 1rem;
4 | margin: 3rem auto 6rem;
5 | }
6 |
7 | .header {
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | }
12 |
13 | .headerImage {
14 | width: 6rem;
15 | }
16 |
17 | .headerHomeImage {
18 | width: 6rem;
19 | }
20 |
21 | .backToHome {
22 | margin: 3rem 0 0;
23 | }
24 |
--------------------------------------------------------------------------------
/layouts/index.ts:
--------------------------------------------------------------------------------
1 | export { PagesLayout } from './PageLayout'
2 |
--------------------------------------------------------------------------------
/lib/dbConnect.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | const MONGODB_URI = process.env.MONGODB_URI
4 |
5 | if (!MONGODB_URI) {
6 | throw new Error(
7 | 'Please define the MONGODB_URI environment variable inside .env.local / secrets service'
8 | )
9 | }
10 |
11 | /**
12 | * Global is used here to maintain a cached connection across hot reloads
13 | * in development. This prevents connections growing exponentially
14 | * during API Route usage.
15 | */
16 | let cached = global.mongoose
17 |
18 | if (!cached) {
19 | cached = global.mongoose = { conn: null, promise: null }
20 | }
21 |
22 | async function dbConnect() {
23 | if (cached.conn) {
24 | return cached.conn
25 | }
26 |
27 | if (!cached.promise) {
28 | const opts = {
29 | bufferCommands: false,
30 | }
31 |
32 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
33 | return mongoose
34 | })
35 | }
36 |
37 | try {
38 | cached.conn = await cached.promise
39 | } catch (e) {
40 | cached.promise = null
41 | throw e
42 | }
43 |
44 | return cached.conn
45 | }
46 |
47 | export default dbConnect
48 |
--------------------------------------------------------------------------------
/lib/getAllFilesIds.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 |
3 | /**
4 | * @param {String} path
5 | * @param {String} [ext='md']
6 | * @returns {String[]}
7 | */
8 | export default function getAllFilesIds (path: fs.PathLike, ext = 'md') {
9 | const fileNames = fs.readdirSync(path)
10 |
11 | return fileNames.map(name => name.replace(/\.md$/, ''))
12 | }
13 |
--------------------------------------------------------------------------------
/lib/parseMarkdownFile.ts:
--------------------------------------------------------------------------------
1 | import * as matter from 'gray-matter'
2 | import rehypeRaw from 'rehype-raw'
3 | import rehypeStringify from 'rehype-stringify'
4 | import remarkParse from 'remark-parse'
5 | import remarkRehype from 'remark-rehype'
6 | import { unified } from 'unified'
7 |
8 | export default async function parseMarkdownFile (path: string) {
9 | const matterResult = await matter.read(path)
10 |
11 | const convertedData = await unified()
12 | .use(remarkParse)
13 | .use(remarkRehype, { allowDangerousHtml: true })
14 | .use(rehypeRaw)
15 | .use(rehypeStringify)
16 | .process(matterResult.content)
17 |
18 | return {
19 | html: convertedData.value,
20 | meta: matterResult.data,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/models/Comments.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | const CommentSchema = new mongoose.Schema({
4 | _id: mongoose.Types.ObjectId,
5 | name: {
6 | type: String,
7 | required: [true, "Please provide commenter name"],
8 | },
9 | email: {
10 | type: String,
11 | required: [true, 'Please specify the email of commenter.'],
12 | },
13 | movie_id: {
14 | type: mongoose.Types.ObjectId,
15 | },
16 | text: {
17 | type: String,
18 | },
19 | date: {
20 | type: Date,
21 | },
22 | })
23 |
24 | export default mongoose.models.Comment || mongoose.model('Comment', CommentSchema)
25 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | }
6 |
7 | module.exports = nextConfig
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "personal-learn-path-new",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "export": "next export"
11 | },
12 | "dependencies": {
13 | "antd": "^5.2.0",
14 | "date-fns": "^2.29.3",
15 | "draft-js": "^0.11.7",
16 | "gray-matter": "^4.0.3",
17 | "mongoose": "^6.9.1",
18 | "next": "13.1.6",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "rehype-raw": "^6.1.1",
22 | "rehype-stringify": "^9.0.3",
23 | "remark-parse": "^10.0.1",
24 | "remark-rehype": "^10.1.0",
25 | "swr": "^2.0.3",
26 | "unified": "^10.1.2"
27 | },
28 | "devDependencies": {
29 | "@playwright/test": "^1.30.0",
30 | "@types/draft-js": "^0.11.10",
31 | "@types/node": "18.13.0",
32 | "@types/react": "18.0.28",
33 | "@types/react-dom": "18.0.10",
34 | "eslint": "8.34.0",
35 | "eslint-config-next": "13.1.6",
36 | "playwright": "^1.30.0",
37 | "typescript": "4.9.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import 'antd/dist/reset.css'
2 | import type { AppProps } from 'next/app'
3 |
4 | function MyApp({ Component, pageProps }: AppProps) {
5 | return
6 | }
7 |
8 | export default MyApp
9 |
--------------------------------------------------------------------------------
/pages/api/comments/[id]/index.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | import dbConnect from '../../../../lib/dbConnect'
4 | import Comments from '../../../../models/Comments'
5 |
6 | export default async function handler (req, res) {
7 | const {
8 | query: { id },
9 | method,
10 | } = req
11 |
12 | await dbConnect()
13 |
14 | switch (method) {
15 | case 'GET':
16 | try {
17 | const comment = await Comments.findOne({ _id: new mongoose.Types.ObjectId(id) })
18 | if (!comment) {
19 | return res.status(400).json({ success: false })
20 | }
21 | res.status(200).json({ success: true, data: comment })
22 | } catch (error) {
23 | res.status(400).json({ success: false, error })
24 | }
25 | break
26 |
27 | case 'PUT':
28 | try {
29 | const pet = await Comments.findByIdAndUpdate(id, req.body, {
30 | new: true,
31 | runValidators: true,
32 | })
33 | if (!pet) {
34 | return res.status(400).json({ success: false })
35 | }
36 | res.status(200).json({ success: true, data: pet })
37 | } catch (error) {
38 | res.status(400).json({ success: false })
39 | }
40 | break
41 |
42 | case 'DELETE':
43 | try {
44 | const deletedPet = await Comments.deleteOne({ _id: id })
45 | if (!deletedPet) {
46 | return res.status(400).json({ success: false })
47 | }
48 | res.status(200).json({ success: true, data: {} })
49 | } catch (error) {
50 | res.status(400).json({ success: false })
51 | }
52 | break
53 |
54 | default:
55 | res.status(400).json({ success: false })
56 | break
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pages/api/comments/index.js:
--------------------------------------------------------------------------------
1 | import dbConnect from '../../../lib/dbConnect'
2 | import Comments from '../../../models/Comments'
3 |
4 | export default async function handler(req, res) {
5 | const { method } = req
6 |
7 | await dbConnect()
8 |
9 | switch (method) {
10 | case 'GET':
11 | try {
12 | const pets = await Comments.find({}).limit(25) /* find all the data in our database */
13 | res.status(200).json({ success: true, data: pets })
14 | } catch (error) {
15 | res.status(400).json({ success: false })
16 | }
17 | break
18 | case 'POST':
19 | try {
20 | const pet = await Comments.create(
21 | req.body
22 | ) /* create a new model in the database */
23 | res.status(201).json({ success: true, data: pet })
24 | } catch (error) {
25 | res.status(400).json({ success: false })
26 | }
27 | break
28 | default:
29 | res.status(400).json({ success: false })
30 | break
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/pages/comments/[pageid]/edit.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import useSWR from 'swr'
3 | import CommentForm from '../../../components/Form/CommentForm'
4 |
5 | const fetcher = (url) =>
6 | fetch(url)
7 | .then((res) => res.json())
8 | .then((json) => json.data)
9 |
10 | const EditComment = () => {
11 | const router = useRouter()
12 | const { pageid } = router.query
13 | const { data: comment, error } = useSWR(pageid ? `/api/comments/${pageid}` : null, fetcher)
14 |
15 | if (error) return Failed to load
16 | if (!comment) return Loading...
17 |
18 | const commentForm = {
19 | name: comment.name,
20 | email: comment.email,
21 | movie_id: comment.movie_id,
22 | text: comment.text,
23 | date: comment.date,
24 | }
25 |
26 | return
27 | }
28 |
29 | export default EditComment
30 |
--------------------------------------------------------------------------------
/pages/comments/[pageid]/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useRouter } from 'next/router'
3 | import Link from 'next/link'
4 | import dbConnect from '../../../lib/dbConnect'
5 | import Comments from '../../../models/Comments'
6 |
7 | const CommentsPage = ({ comment }) => {
8 | const router = useRouter()
9 | const [message, setMessage] = useState('')
10 |
11 | const handleDelete = async () => {
12 | const commentID = router.query.id
13 |
14 | try {
15 | await fetch(`/api/pets/${commentID}`, {
16 | method: 'Delete',
17 | })
18 | router.push('/')
19 | } catch (error) {
20 | setMessage('Failed to delete the comment.')
21 | }
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 |
{comment.date}{comment.name}
29 |
30 |
{comment.email}
31 |
{comment.movie_id}
32 |
{comment.text}
33 |
{comment.date}
34 |
35 |
36 |
37 | Edit
38 |
39 |
40 | Delete
41 |
42 |
43 |
44 |
45 | {message &&
{message}
}
46 |
47 | )
48 | }
49 |
50 | export async function getServerSideProps ({ params }) {
51 | await dbConnect()
52 |
53 | const comment = await Comments.findById(params.pageid).lean()
54 | comment._id = comment._id.valueOf()
55 |
56 | return {
57 | props: {
58 | comment: {
59 | ...comment,
60 | _id: comment._id.valueOf(),
61 | movie_id: comment.movie_id.valueOf(),
62 | date: comment.date.valueOf()
63 | }
64 | }
65 | }
66 | }
67 |
68 | export default CommentsPage
69 |
--------------------------------------------------------------------------------
/pages/comments/index.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import dbConnect from '../../lib/dbConnect'
3 | import Comments from '../../models/Comments'
4 |
5 | const Index = ({ comments }) => (
6 | <>
7 | {/* Create a card for each commentItem */}
8 | {comments.map((commentItem) => (
9 |
10 |
11 |
12 |
{commentItem.date}: {commentItem.name}
13 |
14 |
{commentItem.text}
15 |
16 |
17 | Edit
18 |
19 |
20 | View
21 |
22 |
23 |
24 |
25 |
26 | ))}
27 | >
28 | )
29 |
30 | /* Retrieves comment(s) data from mongodb database */
31 | export async function getServerSideProps() {
32 | await dbConnect()
33 |
34 | /* find all the data in our database */
35 | const result = await Comments.find({}).limit(20).lean()
36 |
37 | // TODO: simplify
38 | const resultSerializable = result.map(item => ({
39 | ...item,
40 | _id: item._id.valueOf(),
41 | movie_id: item.movie_id.valueOf(),
42 | date: item.date.valueOf()
43 | }))
44 | return { props: { comments: resultSerializable } }
45 | }
46 |
47 | export default Index
48 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type { NextPage } from 'next'
3 | import Head from 'next/head'
4 |
5 | import mainData from '../data/main.json'
6 | import { PagesLayout } from '../layouts'
7 | import { ContentEditor } from '../components'
8 |
9 | const Home: NextPage = () => {
10 |
11 | const onSaveContent = (content: object) => {
12 | console.log("content: ", content); // eslint-disable-line
13 | }
14 |
15 | return (
16 |
17 |
18 | {mainData.title}
19 |
20 | Personal learn path
21 | Add source
22 |
23 |
24 | )
25 | }
26 |
27 | export default Home
28 |
29 |
--------------------------------------------------------------------------------
/pages/infrastructure/[pageid].tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import path from 'path'
3 | import { PagesLayout } from '../../layouts'
4 | import getAllFilesIds from '../../lib/getAllFilesIds'
5 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
6 |
7 |
8 | const pagesDirectory = path.join(process.cwd(), 'data', 'infrastructure')
9 |
10 | export function getStaticPaths () {
11 | const paths = getAllFilesIds(pagesDirectory).map(path => ({ params: { pageid: path } }))
12 |
13 | return {
14 | paths,
15 | fallback: false,
16 | }
17 | }
18 |
19 | type GetStaticPropsType = {
20 | params: {
21 | pageid: string
22 | }
23 | }
24 |
25 | export async function getStaticProps ({ params }: GetStaticPropsType) {
26 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${params.pageid}.md`))
27 |
28 | return {
29 | props: {
30 | pagesData: {
31 | title: fileData.meta.title,
32 | date: fileData.meta.date,
33 | html: fileData.html,
34 | },
35 | },
36 | }
37 | }
38 |
39 | type InfrastructurePageType = {
40 | pagesData: {
41 | date: string
42 | title: string
43 | html: string
44 | }
45 | }
46 |
47 | export default function InfrastructurePage ({ pagesData }: InfrastructurePageType) {
48 | return (
49 |
50 |
51 | {pagesData.date}: {pagesData.title}
52 |
53 |
54 | {pagesData.date}: {pagesData.title}
55 |
56 |
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/pages/infrastructure/index.tsx:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import React from 'react'
3 | import Head from 'next/head'
4 | import Link from 'next/link'
5 | import {
6 | parseISO,
7 | getDate,
8 | getMonth,
9 | getYear,
10 | } from 'date-fns'
11 |
12 | import { List } from 'antd';
13 |
14 | import { PagesLayout } from '../../layouts'
15 | import getAllFilesIds from '../../lib/getAllFilesIds'
16 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
17 |
18 | const sectionName = 'infrastructure'
19 | const pagesDirectory = path.join(process.cwd(), 'data', sectionName)
20 | const pageTitle = `Infrastructure`
21 |
22 | export async function getStaticProps () {
23 | const allPagesData = await Promise.all(
24 | getAllFilesIds(pagesDirectory).map(async (fileId) => {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${fileId}.md`))
26 | // It should be extracted to utils
27 | const dateParsedISO = parseISO(fileData.meta.date)
28 |
29 | return {
30 | pageid: `${fileId}`,
31 | title: fileData.meta.title,
32 | date: fileData.meta.date,
33 | date_parsed: {
34 | year: getYear(dateParsedISO),
35 | month: getMonth(dateParsedISO),
36 | day: getDate(dateParsedISO),
37 | }
38 | }
39 | }),
40 | )
41 |
42 | return {
43 | props: {
44 | allPagesData,
45 | },
46 | }
47 | }
48 |
49 | type InfrastructureIndexPageProps = {
50 | allPagesData: [
51 | {
52 | title: string
53 | date: string
54 | date_parsed: {
55 | day: number
56 | month: number
57 | year: number
58 | }
59 | pageid: string
60 | contentHtml: string
61 | }
62 | ]
63 | }
64 |
65 | export default function InfrastructureIndexPage ({ allPagesData }: InfrastructureIndexPageProps) {
66 | return (
67 |
68 |
69 | {pageTitle}
70 |
71 | {/* @ts-ignore */}
72 |
73 | {pageTitle}
74 | (
78 | {item.date} / {item.title}
79 | )}
80 | />
81 |
82 | {allPagesData.map(pageData => console.log(pageData))}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/pages/programming/[pageid].tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import path from 'path'
3 | import { PagesLayout } from '../../layouts'
4 | import getAllFilesIds from '../../lib/getAllFilesIds'
5 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
6 |
7 | const pagesDirectory = path.join(process.cwd(), 'data', 'programming')
8 |
9 | export function getStaticPaths () {
10 | const paths = getAllFilesIds(pagesDirectory).map(path => ({ params: { pageid: path } }))
11 |
12 | return {
13 | paths,
14 | fallback: false,
15 | }
16 | }
17 |
18 | type GetStaticPropsType = {
19 | params: {
20 | pageid: string
21 | }
22 | }
23 |
24 | export async function getStaticProps ({ params }: GetStaticPropsType) {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${params.pageid}.md`))
26 |
27 | return {
28 | props: {
29 | pagesData: {
30 | title: fileData.meta.title,
31 | date: fileData.meta.date,
32 | html: fileData.html,
33 | },
34 | },
35 | }
36 | }
37 |
38 | type ProgrammingPageType = {
39 | pagesData: {
40 | date: string
41 | title: string
42 | html: string
43 | }
44 | }
45 |
46 | export default function ProgrammingPage ({ pagesData }: ProgrammingPageType) {
47 | return (
48 |
49 |
50 | {pagesData.date}: {pagesData.title}
51 |
52 |
53 | {pagesData.date}: {pagesData.title}
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/pages/programming/index.tsx:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import React from 'react'
3 | import Head from 'next/head'
4 | import Link from 'next/link'
5 | import {
6 | parseISO,
7 | getDate,
8 | getMonth,
9 | getYear,
10 | } from 'date-fns'
11 |
12 | import { List } from 'antd';
13 |
14 | import { PagesLayout } from '../../layouts'
15 | import getAllFilesIds from '../../lib/getAllFilesIds'
16 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
17 |
18 | const sectionName = 'programming'
19 | const pagesDirectory = path.join(process.cwd(), 'data', sectionName)
20 | const pageTitle = `Programming Index Page`
21 |
22 | export async function getStaticProps () {
23 | const allPagesData = await Promise.all(
24 | getAllFilesIds(pagesDirectory).map(async (fileId) => {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${fileId}.md`))
26 | // It should be extracted to utils
27 | const dateParsedISO = parseISO(fileData.meta.date)
28 |
29 | return {
30 | pageid: `${fileId}`,
31 | title: fileData.meta.title,
32 | date: fileData.meta.date,
33 | date_parsed: {
34 | year: getYear(dateParsedISO),
35 | month: getMonth(dateParsedISO),
36 | day: getDate(dateParsedISO),
37 | }
38 | }
39 | }),
40 | )
41 |
42 | return {
43 | props: {
44 | allPagesData,
45 | },
46 | }
47 | }
48 |
49 | type ProgrammingIndexPageProps = {
50 | allPagesData: [
51 | {
52 | title: string
53 | date: string
54 | date_parsed: {
55 | day: number
56 | month: number
57 | year: number
58 | }
59 | pageid: string
60 | contentHtml: string
61 | }
62 | ]
63 | }
64 |
65 | export default function ProgrammingIndexPage ({ allPagesData }: ProgrammingIndexPageProps) {
66 | return (
67 |
68 |
69 | {pageTitle}
70 |
71 | {/* @ts-ignore */}
72 |
73 | {pageTitle}
74 | (
78 | {item.date} / {item.title}
79 | )}
80 | />
81 |
82 | {allPagesData.map(pageData => console.log(pageData))}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/pages/soft-skills/[pageid].tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import path from 'path'
3 | import { PagesLayout } from '../../layouts'
4 | import getAllFilesIds from '../../lib/getAllFilesIds'
5 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
6 |
7 | const pagesDirectory = path.join(process.cwd(), 'data', 'soft-skills')
8 |
9 | export function getStaticPaths () {
10 | const paths = getAllFilesIds(pagesDirectory).map(path => ({ params: { pageid: path } }))
11 |
12 | return {
13 | paths,
14 | fallback: false,
15 | }
16 | }
17 |
18 | type GetStaticPropsType = {
19 | params: {
20 | pageid: string
21 | }
22 | }
23 |
24 | export async function getStaticProps ({ params }: GetStaticPropsType) {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${params.pageid}.md`))
26 |
27 | return {
28 | props: {
29 | pagesData: {
30 | title: fileData.meta.title,
31 | date: fileData.meta.date,
32 | html: fileData.html,
33 | },
34 | },
35 | }
36 | }
37 |
38 | type SoftSkillPageType = {
39 | pagesData: {
40 | date: string
41 | title: string
42 | html: string
43 | }
44 | }
45 |
46 | export default function SoftSkillPage ({ pagesData }: SoftSkillPageType) {
47 | return (
48 |
49 |
50 | {pagesData.date}: {pagesData.title}
51 |
52 |
53 | {pagesData.date}: {pagesData.title}
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/pages/soft-skills/index.tsx:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import React from 'react'
3 | import Head from 'next/head'
4 | import Link from 'next/link'
5 | import {
6 | parseISO,
7 | getDate,
8 | getMonth,
9 | getYear,
10 | } from 'date-fns'
11 |
12 | import { List } from 'antd';
13 |
14 | import { PagesLayout } from '../../layouts'
15 | import getAllFilesIds from '../../lib/getAllFilesIds'
16 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
17 |
18 | const sectionName = 'soft-skills'
19 | const pagesDirectory = path.join(process.cwd(), 'data', sectionName)
20 | const pageTitle = `Soft skills`
21 |
22 | export async function getStaticProps () {
23 | const allPagesData = await Promise.all(
24 | getAllFilesIds(pagesDirectory).map(async (fileId) => {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${fileId}.md`))
26 | // It should be extracted to utils
27 | const dateParsedISO = parseISO(fileData.meta.date)
28 |
29 | return {
30 | pageid: `${fileId}`,
31 | title: fileData.meta.title,
32 | date: fileData.meta.date,
33 | date_parsed: {
34 | year: getYear(dateParsedISO),
35 | month: getMonth(dateParsedISO),
36 | day: getDate(dateParsedISO),
37 | }
38 | }
39 | }),
40 | )
41 |
42 | return {
43 | props: {
44 | allPagesData,
45 | },
46 | }
47 | }
48 |
49 | type SoftSkillsIndexPageProps = {
50 | allPagesData: [
51 | {
52 | title: string
53 | date: string
54 | date_parsed: {
55 | day: number
56 | month: number
57 | year: number
58 | }
59 | pageid: string
60 | contentHtml: string
61 | }
62 | ]
63 | }
64 |
65 | export default function SoftSkillsIndexPage ({ allPagesData }: SoftSkillsIndexPageProps) {
66 | return (
67 |
68 |
69 | {pageTitle}
70 |
71 | {/* @ts-ignore */}
72 |
73 | {pageTitle}
74 | (
78 | {item.date} / {item.title}
79 | )}
80 | />
81 |
82 | {allPagesData.map(pageData => console.log(pageData))}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/pages/test-works/[pageid].tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import path from 'path'
3 | import { PagesLayout } from '../../layouts'
4 | import getAllFilesIds from '../../lib/getAllFilesIds'
5 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
6 |
7 | const pagesDirectory = path.join(process.cwd(), 'data', 'test-works')
8 |
9 | export function getStaticPaths () {
10 | const paths = getAllFilesIds(pagesDirectory).map(path => ({ params: { pageid: path } }))
11 |
12 | return {
13 | paths,
14 | fallback: false,
15 | }
16 | }
17 |
18 | type GetStaticPropsType = {
19 | params: {
20 | pageid: string
21 | }
22 | }
23 |
24 | export async function getStaticProps ({ params }: GetStaticPropsType) {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${params.pageid}.md`))
26 |
27 | return {
28 | props: {
29 | pagesData: {
30 | title: fileData.meta.title,
31 | date: fileData.meta.date,
32 | html: fileData.html,
33 | },
34 | },
35 | }
36 | }
37 |
38 | type TestWorkPageType = {
39 | pagesData: {
40 | date: string
41 | title: string
42 | html: string
43 | }
44 | }
45 |
46 | export default function TestWorkPage ({ pagesData }: TestWorkPageType) {
47 | return (
48 |
49 |
50 | {pagesData.date}: {pagesData.title}
51 |
52 |
53 | {pagesData.date}: {pagesData.title}
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/pages/test-works/index.tsx:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import React from 'react'
3 | import Head from 'next/head'
4 | import Link from 'next/link'
5 | import {
6 | parseISO,
7 | getDate,
8 | getMonth,
9 | getYear,
10 | } from 'date-fns'
11 |
12 | import { List } from 'antd';
13 |
14 | import { PagesLayout } from '../../layouts'
15 | import getAllFilesIds from '../../lib/getAllFilesIds'
16 | import parseMarkdownFile from '../../lib/parseMarkdownFile'
17 |
18 | const sectionName = 'test-works'
19 | const pagesDirectory = path.join(process.cwd(), 'data', sectionName)
20 | const pageTitle = `Тестовые задания`
21 |
22 | export async function getStaticProps () {
23 | const allPagesData = await Promise.all(
24 | getAllFilesIds(pagesDirectory).map(async (fileId) => {
25 | const fileData = await parseMarkdownFile(path.join(pagesDirectory, `${fileId}.md`))
26 | // It should be extracted to utils
27 | const dateParsedISO = parseISO(fileData.meta.date)
28 |
29 | return {
30 | pageid: `${fileId}`,
31 | title: fileData.meta.title,
32 | date: fileData.meta.date,
33 | date_parsed: {
34 | year: getYear(dateParsedISO),
35 | month: getMonth(dateParsedISO),
36 | day: getDate(dateParsedISO),
37 | }
38 | }
39 | }),
40 | )
41 |
42 | return {
43 | props: {
44 | allPagesData,
45 | },
46 | }
47 | }
48 |
49 | type TextWorksPageIndexProps = {
50 | allPagesData: [
51 | {
52 | title: string
53 | date: string
54 | date_parsed: {
55 | day: number
56 | month: number
57 | year: number
58 | }
59 | pageid: string
60 | contentHtml: string
61 | }
62 | ]
63 | }
64 |
65 | export default function TextWorksIndexPage ({ allPagesData }: TextWorksPageIndexProps) {
66 | return (
67 |
68 |
69 | {pageTitle}
70 |
71 | {/* @ts-ignore */}
72 |
73 | {pageTitle}
74 | (
78 | {item.date} / {item.title}
79 | )}
80 | />
81 |
82 | {allPagesData.map(pageData => console.log(pageData))}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 | import { devices } from '@playwright/test';
3 |
4 | /**
5 | * Read environment variables from file.
6 | * https://github.com/motdotla/dotenv
7 | */
8 | // require('dotenv').config();
9 |
10 | /**
11 | * See https://playwright.dev/docs/test-configuration.
12 | */
13 | const config: PlaywrightTestConfig = {
14 | testDir: './e2e',
15 | /* Maximum time one test can run for. */
16 | timeout: 30 * 1000,
17 | expect: {
18 | /**
19 | * Maximum time expect() should wait for the condition to be met.
20 | * For example in `await expect(locator).toHaveText();`
21 | */
22 | timeout: 15000
23 | },
24 | /* Run tests in files in parallel */
25 | fullyParallel: true,
26 | /* Fail the build on CI if you accidentally left test.only in the source code. */
27 | forbidOnly: !!process.env.CI,
28 | /* Retry on CI only */
29 | retries: process.env.CI ? 2 : 0,
30 | /* Opt out of parallel tests on CI. */
31 | workers: process.env.CI ? 1 : undefined,
32 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
33 | reporter: 'html',
34 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
35 | use: {
36 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
37 | actionTimeout: 0,
38 | /* Base URL to use in actions like `await page.goto('/')`. */
39 | // baseURL: 'http://localhost:3000',
40 |
41 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
42 | trace: 'on-first-retry',
43 | },
44 |
45 | /* Configure projects for major browsers */
46 | projects: [
47 | {
48 | name: 'chromium',
49 | use: {
50 | ...devices['Desktop Chrome'],
51 | },
52 | },
53 |
54 | {
55 | name: 'firefox',
56 | use: {
57 | ...devices['Desktop Firefox'],
58 | },
59 | },
60 |
61 | {
62 | name: 'webkit',
63 | use: {
64 | ...devices['Desktop Safari'],
65 | },
66 | },
67 |
68 | /* Test against mobile viewports. */
69 | // {
70 | // name: 'Mobile Chrome',
71 | // use: {
72 | // ...devices['Pixel 5'],
73 | // },
74 | // },
75 | // {
76 | // name: 'Mobile Safari',
77 | // use: {
78 | // ...devices['iPhone 12'],
79 | // },
80 | // },
81 |
82 | /* Test against branded browsers. */
83 | // {
84 | // name: 'Microsoft Edge',
85 | // use: {
86 | // channel: 'msedge',
87 | // },
88 | // },
89 | // {
90 | // name: 'Google Chrome',
91 | // use: {
92 | // channel: 'chrome',
93 | // },
94 | // },
95 | ],
96 |
97 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */
98 | // outputDir: 'test-results/',
99 |
100 | /* Run your local dev server before starting the tests */
101 | // webServer: {
102 | // command: 'npm run start',
103 | // port: 3000,
104 | // },
105 | };
106 |
107 | export default config;
108 |
--------------------------------------------------------------------------------
/tests-examples/demo-todo-app.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, type Page } from '@playwright/test';
2 |
3 | test.beforeEach(async ({ page }) => {
4 | await page.goto('https://demo.playwright.dev/todomvc');
5 | });
6 |
7 | const TODO_ITEMS = [
8 | 'buy some cheese',
9 | 'feed the cat',
10 | 'book a doctors appointment'
11 | ];
12 |
13 | test.describe('New Todo', () => {
14 | test('should allow me to add todo items', async ({ page }) => {
15 | // create a new todo locator
16 | const newTodo = page.getByPlaceholder('What needs to be done?');
17 |
18 | // Create 1st todo.
19 | await newTodo.fill(TODO_ITEMS[0]);
20 | await newTodo.press('Enter');
21 |
22 | // Make sure the list only has one todo item.
23 | await expect(page.getByTestId('todo-title')).toHaveText([
24 | TODO_ITEMS[0]
25 | ]);
26 |
27 | // Create 2nd todo.
28 | await newTodo.fill(TODO_ITEMS[1]);
29 | await newTodo.press('Enter');
30 |
31 | // Make sure the list now has two todo items.
32 | await expect(page.getByTestId('todo-title')).toHaveText([
33 | TODO_ITEMS[0],
34 | TODO_ITEMS[1]
35 | ]);
36 |
37 | await checkNumberOfTodosInLocalStorage(page, 2);
38 | });
39 |
40 | test('should clear text input field when an item is added', async ({ page }) => {
41 | // create a new todo locator
42 | const newTodo = page.getByPlaceholder('What needs to be done?');
43 |
44 | // Create one todo item.
45 | await newTodo.fill(TODO_ITEMS[0]);
46 | await newTodo.press('Enter');
47 |
48 | // Check that input is empty.
49 | await expect(newTodo).toBeEmpty();
50 | await checkNumberOfTodosInLocalStorage(page, 1);
51 | });
52 |
53 | test('should append new items to the bottom of the list', async ({ page }) => {
54 | // Create 3 items.
55 | await createDefaultTodos(page);
56 |
57 | // create a todo count locator
58 | const todoCount = page.getByTestId('todo-count')
59 |
60 | // Check test using different methods.
61 | await expect(page.getByText('3 items left')).toBeVisible();
62 | await expect(todoCount).toHaveText('3 items left');
63 | await expect(todoCount).toContainText('3');
64 | await expect(todoCount).toHaveText(/3/);
65 |
66 | // Check all items in one call.
67 | await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
68 | await checkNumberOfTodosInLocalStorage(page, 3);
69 | });
70 | });
71 |
72 | test.describe('Mark all as completed', () => {
73 | test.beforeEach(async ({ page }) => {
74 | await createDefaultTodos(page);
75 | await checkNumberOfTodosInLocalStorage(page, 3);
76 | });
77 |
78 | test.afterEach(async ({ page }) => {
79 | await checkNumberOfTodosInLocalStorage(page, 3);
80 | });
81 |
82 | test('should allow me to mark all items as completed', async ({ page }) => {
83 | // Complete all todos.
84 | await page.getByLabel('Mark all as complete').check();
85 |
86 | // Ensure all todos have 'completed' class.
87 | await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
88 | await checkNumberOfCompletedTodosInLocalStorage(page, 3);
89 | });
90 |
91 | test('should allow me to clear the complete state of all items', async ({ page }) => {
92 | const toggleAll = page.getByLabel('Mark all as complete');
93 | // Check and then immediately uncheck.
94 | await toggleAll.check();
95 | await toggleAll.uncheck();
96 |
97 | // Should be no completed classes.
98 | await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
99 | });
100 |
101 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
102 | const toggleAll = page.getByLabel('Mark all as complete');
103 | await toggleAll.check();
104 | await expect(toggleAll).toBeChecked();
105 | await checkNumberOfCompletedTodosInLocalStorage(page, 3);
106 |
107 | // Uncheck first todo.
108 | const firstTodo = page.getByTestId('todo-item').nth(0);
109 | await firstTodo.getByRole('checkbox').uncheck();
110 |
111 | // Reuse toggleAll locator and make sure its not checked.
112 | await expect(toggleAll).not.toBeChecked();
113 |
114 | await firstTodo.getByRole('checkbox').check();
115 | await checkNumberOfCompletedTodosInLocalStorage(page, 3);
116 |
117 | // Assert the toggle all is checked again.
118 | await expect(toggleAll).toBeChecked();
119 | });
120 | });
121 |
122 | test.describe('Item', () => {
123 |
124 | test('should allow me to mark items as complete', async ({ page }) => {
125 | // create a new todo locator
126 | const newTodo = page.getByPlaceholder('What needs to be done?');
127 |
128 | // Create two items.
129 | for (const item of TODO_ITEMS.slice(0, 2)) {
130 | await newTodo.fill(item);
131 | await newTodo.press('Enter');
132 | }
133 |
134 | // Check first item.
135 | const firstTodo = page.getByTestId('todo-item').nth(0);
136 | await firstTodo.getByRole('checkbox').check();
137 | await expect(firstTodo).toHaveClass('completed');
138 |
139 | // Check second item.
140 | const secondTodo = page.getByTestId('todo-item').nth(1);
141 | await expect(secondTodo).not.toHaveClass('completed');
142 | await secondTodo.getByRole('checkbox').check();
143 |
144 | // Assert completed class.
145 | await expect(firstTodo).toHaveClass('completed');
146 | await expect(secondTodo).toHaveClass('completed');
147 | });
148 |
149 | test('should allow me to un-mark items as complete', async ({ page }) => {
150 | // create a new todo locator
151 | const newTodo = page.getByPlaceholder('What needs to be done?');
152 |
153 | // Create two items.
154 | for (const item of TODO_ITEMS.slice(0, 2)) {
155 | await newTodo.fill(item);
156 | await newTodo.press('Enter');
157 | }
158 |
159 | const firstTodo = page.getByTestId('todo-item').nth(0);
160 | const secondTodo = page.getByTestId('todo-item').nth(1);
161 | const firstTodoCheckbox = firstTodo.getByRole('checkbox');
162 |
163 | await firstTodoCheckbox.check();
164 | await expect(firstTodo).toHaveClass('completed');
165 | await expect(secondTodo).not.toHaveClass('completed');
166 | await checkNumberOfCompletedTodosInLocalStorage(page, 1);
167 |
168 | await firstTodoCheckbox.uncheck();
169 | await expect(firstTodo).not.toHaveClass('completed');
170 | await expect(secondTodo).not.toHaveClass('completed');
171 | await checkNumberOfCompletedTodosInLocalStorage(page, 0);
172 | });
173 |
174 | test('should allow me to edit an item', async ({ page }) => {
175 | await createDefaultTodos(page);
176 |
177 | const todoItems = page.getByTestId('todo-item');
178 | const secondTodo = todoItems.nth(1);
179 | await secondTodo.dblclick();
180 | await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
181 | await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
182 | await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
183 |
184 | // Explicitly assert the new text value.
185 | await expect(todoItems).toHaveText([
186 | TODO_ITEMS[0],
187 | 'buy some sausages',
188 | TODO_ITEMS[2]
189 | ]);
190 | await checkTodosInLocalStorage(page, 'buy some sausages');
191 | });
192 | });
193 |
194 | test.describe('Editing', () => {
195 | test.beforeEach(async ({ page }) => {
196 | await createDefaultTodos(page);
197 | await checkNumberOfTodosInLocalStorage(page, 3);
198 | });
199 |
200 | test('should hide other controls when editing', async ({ page }) => {
201 | const todoItem = page.getByTestId('todo-item').nth(1);
202 | await todoItem.dblclick();
203 | await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
204 | await expect(todoItem.locator('label', {
205 | hasText: TODO_ITEMS[1],
206 | })).not.toBeVisible();
207 | await checkNumberOfTodosInLocalStorage(page, 3);
208 | });
209 |
210 | test('should save edits on blur', async ({ page }) => {
211 | const todoItems = page.getByTestId('todo-item');
212 | await todoItems.nth(1).dblclick();
213 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
214 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
215 |
216 | await expect(todoItems).toHaveText([
217 | TODO_ITEMS[0],
218 | 'buy some sausages',
219 | TODO_ITEMS[2],
220 | ]);
221 | await checkTodosInLocalStorage(page, 'buy some sausages');
222 | });
223 |
224 | test('should trim entered text', async ({ page }) => {
225 | const todoItems = page.getByTestId('todo-item');
226 | await todoItems.nth(1).dblclick();
227 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
228 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
229 |
230 | await expect(todoItems).toHaveText([
231 | TODO_ITEMS[0],
232 | 'buy some sausages',
233 | TODO_ITEMS[2],
234 | ]);
235 | await checkTodosInLocalStorage(page, 'buy some sausages');
236 | });
237 |
238 | test('should remove the item if an empty text string was entered', async ({ page }) => {
239 | const todoItems = page.getByTestId('todo-item');
240 | await todoItems.nth(1).dblclick();
241 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
242 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
243 |
244 | await expect(todoItems).toHaveText([
245 | TODO_ITEMS[0],
246 | TODO_ITEMS[2],
247 | ]);
248 | });
249 |
250 | test('should cancel edits on escape', async ({ page }) => {
251 | const todoItems = page.getByTestId('todo-item');
252 | await todoItems.nth(1).dblclick();
253 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
254 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
255 | await expect(todoItems).toHaveText(TODO_ITEMS);
256 | });
257 | });
258 |
259 | test.describe('Counter', () => {
260 | test('should display the current number of todo items', async ({ page }) => {
261 | // create a new todo locator
262 | const newTodo = page.getByPlaceholder('What needs to be done?');
263 |
264 | // create a todo count locator
265 | const todoCount = page.getByTestId('todo-count')
266 |
267 | await newTodo.fill(TODO_ITEMS[0]);
268 | await newTodo.press('Enter');
269 |
270 | await expect(todoCount).toContainText('1');
271 |
272 | await newTodo.fill(TODO_ITEMS[1]);
273 | await newTodo.press('Enter');
274 | await expect(todoCount).toContainText('2');
275 |
276 | await checkNumberOfTodosInLocalStorage(page, 2);
277 | });
278 | });
279 |
280 | test.describe('Clear completed button', () => {
281 | test.beforeEach(async ({ page }) => {
282 | await createDefaultTodos(page);
283 | });
284 |
285 | test('should display the correct text', async ({ page }) => {
286 | await page.locator('.todo-list li .toggle').first().check();
287 | await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
288 | });
289 |
290 | test('should remove completed items when clicked', async ({ page }) => {
291 | const todoItems = page.getByTestId('todo-item');
292 | await todoItems.nth(1).getByRole('checkbox').check();
293 | await page.getByRole('button', { name: 'Clear completed' }).click();
294 | await expect(todoItems).toHaveCount(2);
295 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
296 | });
297 |
298 | test('should be hidden when there are no items that are completed', async ({ page }) => {
299 | await page.locator('.todo-list li .toggle').first().check();
300 | await page.getByRole('button', { name: 'Clear completed' }).click();
301 | await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
302 | });
303 | });
304 |
305 | test.describe('Persistence', () => {
306 | test('should persist its data', async ({ page }) => {
307 | // create a new todo locator
308 | const newTodo = page.getByPlaceholder('What needs to be done?');
309 |
310 | for (const item of TODO_ITEMS.slice(0, 2)) {
311 | await newTodo.fill(item);
312 | await newTodo.press('Enter');
313 | }
314 |
315 | const todoItems = page.getByTestId('todo-item');
316 | const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
317 | await firstTodoCheck.check();
318 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
319 | await expect(firstTodoCheck).toBeChecked();
320 | await expect(todoItems).toHaveClass(['completed', '']);
321 |
322 | // Ensure there is 1 completed item.
323 | await checkNumberOfCompletedTodosInLocalStorage(page, 1);
324 |
325 | // Now reload.
326 | await page.reload();
327 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
328 | await expect(firstTodoCheck).toBeChecked();
329 | await expect(todoItems).toHaveClass(['completed', '']);
330 | });
331 | });
332 |
333 | test.describe('Routing', () => {
334 | test.beforeEach(async ({ page }) => {
335 | await createDefaultTodos(page);
336 | // make sure the app had a chance to save updated todos in storage
337 | // before navigating to a new view, otherwise the items can get lost :(
338 | // in some frameworks like Durandal
339 | await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
340 | });
341 |
342 | test('should allow me to display active items', async ({ page }) => {
343 | const todoItem = page.getByTestId('todo-item');
344 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
345 |
346 | await checkNumberOfCompletedTodosInLocalStorage(page, 1);
347 | await page.getByRole('link', { name: 'Active' }).click();
348 | await expect(todoItem).toHaveCount(2);
349 | await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
350 | });
351 |
352 | test('should respect the back button', async ({ page }) => {
353 | const todoItem = page.getByTestId('todo-item');
354 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
355 |
356 | await checkNumberOfCompletedTodosInLocalStorage(page, 1);
357 |
358 | await test.step('Showing all items', async () => {
359 | await page.getByRole('link', { name: 'All' }).click();
360 | await expect(todoItem).toHaveCount(3);
361 | });
362 |
363 | await test.step('Showing active items', async () => {
364 | await page.getByRole('link', { name: 'Active' }).click();
365 | });
366 |
367 | await test.step('Showing completed items', async () => {
368 | await page.getByRole('link', { name: 'Completed' }).click();
369 | });
370 |
371 | await expect(todoItem).toHaveCount(1);
372 | await page.goBack();
373 | await expect(todoItem).toHaveCount(2);
374 | await page.goBack();
375 | await expect(todoItem).toHaveCount(3);
376 | });
377 |
378 | test('should allow me to display completed items', async ({ page }) => {
379 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
380 | await checkNumberOfCompletedTodosInLocalStorage(page, 1);
381 | await page.getByRole('link', { name: 'Completed' }).click();
382 | await expect(page.getByTestId('todo-item')).toHaveCount(1);
383 | });
384 |
385 | test('should allow me to display all items', async ({ page }) => {
386 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
387 | await checkNumberOfCompletedTodosInLocalStorage(page, 1);
388 | await page.getByRole('link', { name: 'Active' }).click();
389 | await page.getByRole('link', { name: 'Completed' }).click();
390 | await page.getByRole('link', { name: 'All' }).click();
391 | await expect(page.getByTestId('todo-item')).toHaveCount(3);
392 | });
393 |
394 | test('should highlight the currently applied filter', async ({ page }) => {
395 | await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
396 |
397 | //create locators for active and completed links
398 | const activeLink = page.getByRole('link', { name: 'Active' });
399 | const completedLink = page.getByRole('link', { name: 'Completed' });
400 | await activeLink.click();
401 |
402 | // Page change - active items.
403 | await expect(activeLink).toHaveClass('selected');
404 | await completedLink.click();
405 |
406 | // Page change - completed items.
407 | await expect(completedLink).toHaveClass('selected');
408 | });
409 | });
410 |
411 | async function createDefaultTodos(page: any) {
412 | // create a new todo locator
413 | const newTodo = page.getByPlaceholder('What needs to be done?');
414 |
415 | for (const item of TODO_ITEMS) {
416 | await newTodo.fill(item);
417 | await newTodo.press('Enter');
418 | }
419 | }
420 |
421 | async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
422 | return await page.waitForFunction(e => {
423 | return JSON.parse(localStorage['react-todos']).length === e;
424 | }, expected);
425 | }
426 |
427 | async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
428 | return await page.waitForFunction(e => {
429 | return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
430 | }, expected);
431 | }
432 |
433 | async function checkTodosInLocalStorage(page: Page, title: string) {
434 | return await page.waitForFunction(t => {
435 | return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
436 | }, title);
437 | }
438 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------