Commit 8ca90ff9 authored by Pavel Mishakov's avatar Pavel Mishakov

exam 8 done

parents
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "webinar",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.10.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.2"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
import axios from "axios";
export const mainInstance = axios.create({
baseURL: 'https://burger-project-ajs-22-default-rtdb.firebaseio.com/'
})
\ No newline at end of file
.Form__Frame {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.Form {
display: flex;
flex-direction: column;
gap: 20px;
}
\ No newline at end of file
import { useEffect, useState, type ChangeEvent, type FormEvent } from 'react'
import styles from './Form.module.css'
import type { TCategory } from '../../types/TCategory'
import type { TQuote } from '../../types/TQuote'
import { mainInstance } from '../../axios/mainInstance'
import { useNavigate, useParams } from 'react-router'
type Props ={
type: 'edit'| 'add'
}
const Form = ({type}: Props) => {
const params = useParams()
const [categories, setCategories] = useState<TCategory[]>([])
const [values, setValues] =useState<TQuote>({
author: '',
category: '',
text: ''
})
const navigate = useNavigate()
useEffect(() => {
const fetchCategories = async() => {
try {
const response = await mainInstance.get('categories.json')
if (response?.data) {
setCategories(Array.from(response.data))
}
} catch(err) {
console.log(err)
}
}
fetchCategories()
}, [])
useEffect(() => {
const fetchQuote = async () => {
try {
if (params?.quoteId) {
const response = await mainInstance.get(`quotes/${params?.quoteId}.json`)
if (response?.data) {
const temp = response.data
setValues({
text: temp.text,
author: temp.author,
category: temp.category
})
}
}
} catch(err) {
console.log(err)
}
}
fetchQuote()
return () => {
setValues({
id: '',
text: '',
author: '',
category: ''
})
}
}, [params?.quoteId])
const inputHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setValues(prev => ({...prev, [e.target.name]: e.target.value}))
}
const submit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
try {
if (type === 'edit') {
await mainInstance.patch(`quotes/${params.quoteId}.json`, values)
} else {
await mainInstance.post('quotes.json', values)
}
alert('Success')
navigate('/quotes')
} catch(err) {
console.log(err)
}
}
return (
<section className={styles.Form__Frame}>
<h1>{type === 'edit' ? 'Edit quote' : 'Submit new quote'}</h1>
<form className={styles.Form} onSubmit={submit}>
<select required value={values.category} onChange={inputHandler} name="category">
<option value={''} disabled>Please select category</option>
{categories.map(cat => {
return (
<option key={cat.value} value={cat.value}>{cat.label}</option>
)
})}
</select>
<input placeholder='Author' required onChange={inputHandler} value={values.author} type="text" name='author' />
<input placeholder='Text' required onChange={inputHandler} value={values.text} type="text" name='text' />
<button>{type === 'edit' ? 'Save edit': 'Submit'}</button>
</form>
</section>
)
}
export default Form
\ No newline at end of file
.Header {
background: lightblue;
}
.Header__Container {
max-width: 1000px;
width: 100%;
padding: 20px;
}
.Header__Nav {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.Header__Nav a {
text-decoration: none;
}
\ No newline at end of file
import { NavLink } from 'react-router'
import styles from './Header.module.css'
const Header = () => {
return (
<header className={styles.Header}>
<div className={styles.Header__Container}>
<nav className={styles.Header__Nav}>
<NavLink to={'/quotes'}>
Quotes
</NavLink>
<p>|</p>
<NavLink to={'/add-quote'}>
Add new quote
</NavLink>
</nav>
</div>
</header>
)
}
export default Header
\ No newline at end of file
import { useEffect, useState } from "react"
import { NavLink, useParams } from "react-router"
import type { TQuote } from "../../types/TQuote"
import { mainInstance } from "../../axios/mainInstance"
import type { TFirebaseQuote } from "../../types/TFirebaseQuote"
import type { TCategory } from "../../types/TCategory"
import ListItem from "../ListItem/ListItem"
const List = () => {
const {category} = useParams()
const [quotes, setQuotes] = useState<TQuote[]>([])
const [categories, setCategories] = useState<TCategory[]>([])
useEffect(() => {
const fetchCategories = async() => {
try {
const response = await mainInstance.get('categories.json')
if (response?.data) {
setCategories(Array.from(response.data))
}
} catch(err) {
console.log(err)
}
}
fetchCategories()
}, [])
useEffect(() => {
const fetchQuotes = async() => {
try {
const response = await mainInstance
.get('quotes.json' + (category ? `?orderBy="category"&equalTo="${category}"` : ''))
if (response?.data) {
const firebaseQuotes: TFirebaseQuote = response.data
setQuotes(Object.keys(firebaseQuotes).map(key => {
const quote = firebaseQuotes[key]
return {
id: key,
author: quote.author,
category: quote.category,
text: quote.text
}
}))
}
} catch(err) {
console.log(err)
}
}
fetchQuotes()
}, [category])
return (
<>
<aside>
<NavLink to={{
pathname: "/quotes",
}}>All</NavLink>
{categories.map(cat => {
return (
<NavLink to={{
pathname: `/quotes/${cat.value}`,
}}>{cat.label}</NavLink>
)
})}
</aside>
<h1>QUOTES</h1>
<ul>
{quotes.map(quote => {
return (
<ListItem
key={quote.id}
quote={quote}
setQuotes={setQuotes}
/>
)
})}
</ul>
</>
)
}
export default List
\ No newline at end of file
import { useNavigate } from "react-router"
import type { TQuote } from "../../types/TQuote"
import { mainInstance } from "../../axios/mainInstance"
type Props = {
quote: TQuote
setQuotes: (cb: (value: TQuote[]) => TQuote[]) => void
}
const ListItem = ({quote, setQuotes}: Props) => {
const navigate = useNavigate()
const editHandler = () => {
navigate(`/edit/${quote.id}`)
}
const deleteHandler = async () => {
try {
await mainInstance.delete(`quotes/${quote.id}.json`)
setQuotes(prev => (prev.filter(q => q.id !== quote.id)))
} catch(err) {
console.log(err)
}
}
return (
<li>
<div>
<hr />
<p>Author: {quote.author}</p>
<i>{quote.text}</i>
<p>CATEGORY: {quote.category}</p>
<button onClick={editHandler}>Edit</button>
<button onClick={deleteHandler}>Delete</button>
<hr />
</div>
</li>
)
}
export default ListItem
\ No newline at end of file
import { Outlet } from 'react-router'
import Header from '../../Header/Header'
const RootLayout = () => {
return (
<>
<Header />
<main>
<Outlet />
</main>
</>
)
}
export default RootLayout
\ No newline at end of file
* {
margin: 0;
box-sizing: border-box;
}
\ No newline at end of file
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { createBrowserRouter, RouterProvider } from 'react-router'
import RootLayout from './components/layouts/RootLayout/RootLayout'
import Form from './components/Form/Form'
import List from './components/List/List'
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{
path: 'add-quote',
element: <Form type='add' />
},
{
path: 'edit/:quoteId',
element: <Form type='edit' />
},
{
path: 'quotes',
element: <List />
},
{
path: 'quotes/:category',
element: <List />
}
]
},
{
path: '*',
element: <h1>Page not found</h1>
}
])
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
export type TCategory = {
label: string,
value: string
}
\ No newline at end of file
import type { TQuote } from "./TQuote"
export type TFirebaseQuote = {
[key: string]: Omit<TQuote, 'id'>
}
\ No newline at end of file
export type TQuote = {
id?: string
text: string
author: string
category: string
}
\ No newline at end of file
/// <reference types="vite/client" />
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3000
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment