Commit 0de82daa authored by Nurasyl's avatar Nurasyl

Initial commit

parents
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
# 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/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-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:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
<!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 && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.5",
"antd": "^5.17.2",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"typescript": "^5.2.2",
"vite": "^5.2.0"
}
}
<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 { BrowserRouter, Routes, Route } from "react-router-dom";
import { Layout } from "./Components/Layout/Layout";
import { Context } from "./Container/Context/Context";
import { ContextForm } from "./Container/ContextForm/ContextForm";
import { Provider } from "react-redux";
import { store } from "./Store";
export const App = () => {
return (
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout/>}>
<Route index element={<Context/>}/>
<Route path="/quotes/:param" element={<Context/>}/>
<Route path="/form/:param" element={<ContextForm/>}/>
</Route>
</Routes>
</BrowserRouter>
</Provider>
)
}
\ No newline at end of file
This diff is collapsed.
.QuoteCard {
width: 50%;
margin: 0 auto;
margin-bottom: 25px;
}
.QuoteCard > .ant-card-head > .ant-card-head-wrapper > .ant-card-head-title {
text-transform: capitalize;
}
.QuoteCard > .ant-card-body {
display: flex;
align-items: center;
width: 100%;
}
.QuoteCard > .ant-card-body > p {
text-transform: capitalize;
font-size: 18px;
}
.QuoteCard > .ant-card-body > .delete_btn {
margin-left: 20px;
border: none;
border-radius: 50%;
background-color: rgb(217, 84, 84);
color: white;
height: 25px;
width: 25px;
font-size: 20px;
cursor: pointer;
}
\ No newline at end of file
import Card from "antd/es/card/Card";
import "./Card.css";
type TProps = {
text: string
author: string
onDelete: VoidFunction
}
export const QuoteCard = ({text, author, onDelete}: TProps) => {
return (
<Card title={author} bordered={false} className="QuoteCard">
<p>{text}</p>
<button className="delete_btn" onClick={onDelete}>-</button>
</Card>
)
}
\ No newline at end of file
form {
width: 100%;
height: 400px;
border: 2px solid wheat;
border-radius: 8px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: space-evenly;
padding: 15px;
}
form > .select {
width: 100%;
}
form > .text {
height: 80px;
}
form > .btn {
width: 80%;
}
\ No newline at end of file
import { Select, Input, Button } from 'antd';
import { optionsItem } from '../../Data/options.data';
import "./Form.css";
type TProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
onChangeSelect: (value: string, option: { label: string; value: string; } | {label: string; value: string;}[]) => void
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
valueSelect: string
valueAuthor: string
valueText: string
}
export const Form = ({onSubmit, valueSelect, valueAuthor, valueText, onChangeSelect, onChange}: TProps) => {
return (
<form onSubmit={onSubmit}>
<Select
className="select"
placeholder="Category"
value={valueSelect}
onChange={onChangeSelect}
options={optionsItem}
/>
<Input placeholder="Author" name="author" value={valueAuthor} onChange={onChange}/>
<Input className="text" placeholder="Text" name="text" value={valueText} onChange={onChange}/>
<Button className="btn" type="primary" htmlType="submit">Add new quote</Button>
</form>
)
}
\ No newline at end of file
.Header {
height: 65px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30px;
}
.Header > h1 {
color: wheat;
font-size: 45px;
cursor: pointer;
}
nav {
width: 20%;
display: flex;
align-items: center;
justify-content: space-between;
}
nav > a {
font-size: 25px;
color: wheat;
}
nav > a:hover {
color: white;
}
nav > a.active {
color: white;
}
nav > span {
color: wheat;
font-size: 25px;
font-weight: bold;
}
\ No newline at end of file
import { NavigationItem } from "../UI/Navigation/NavigationItem";
import { useAppSelector } from "../../Store";
import './Header.css';
import { shallowEqual } from "react-redux";
type TProps = {
onOpen: VoidFunction
}
export const Header = ({onOpen}: TProps) => {
const {values} = useAppSelector(state => state.quotes, shallowEqual)
return (
<header className="Header">
<h1 onClick={onOpen}>Quotes Central {values.author}</h1>
<nav>
<NavigationItem text="Quotes" to="/" end/>
<span>|</span>
<NavigationItem text="Submit new quotes" to="/form/add" end/>
</nav>
</header>
)
}
\ No newline at end of file
.Layout {
margin: 0;
width: 100%;
height: 100vh;
background-image: url("../../Assets/Layout-bg.jpg");
background-repeat: no-repeat;
background-size: 100% 100%;
}
\ No newline at end of file
import { useState } from "react";
import { Outlet } from "react-router-dom";
import { Header } from "../Header/Header";
import { Menu } from "../Menu/Menu";
import "./Layout.css";
export const Layout = () => {
const [open, setOpen] = useState<boolean>(false)
const onCloseHandler = () => {
setOpen(false);
};
const onOpenHadler = () => {
setOpen(true);
};
return (
<div className="Layout">
<Header onOpen={onOpenHadler}/>
<Menu open={open} onClose={onCloseHandler}/>
<main>
<Outlet/>
</main>
</div>
)
}
\ No newline at end of file
.ant-drawer-header {
display: none !important;
}
.ant-drawer-body {
background-image: url("../../Assets/side-bar-bg.jpg");
background-repeat: no-repeat;
background-size: 100% 100%;
}
.menu-nav {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-evenly;
}
.menu-nav > a {
color: wheat;
font-size: 45px;
text-shadow: 0px 0px 20px black;
}
.menu-nav > a.active {
color: white;
}
\ No newline at end of file
import { Drawer } from "antd";
import { NavigationItem } from "../UI/Navigation/NavigationItem";
import { menuItems } from "../../Data/menu.items.data";
import './Menu.css';
type TProps = {
open: boolean
onClose: VoidFunction
}
export const Menu = ({open, onClose}: TProps) => {
return (
<Drawer title="Quotes" placement={"left"} onClose={onClose} open={open}>
<nav className="menu-nav">
{menuItems.map(item => (
<NavigationItem key={item.text} text={item.text} to={item.to} end/>
))}
</nav>
</Drawer>
)
}
\ No newline at end of file
import { NavLink } from "react-router-dom";
type TProps = {
to: string
end: boolean
text: string
}
export const NavigationItem = ({to, end, text}: TProps) => {
return (
<NavLink to={to} end={end}>
{text}
</NavLink>
)
}
\ No newline at end of file
import axios from "axios";
import { parseQuotesData } from "../Helpers/parseData";
const axiosQuotes = axios.create({
baseURL: "https://burger-278a4-default-rtdb.firebaseio.com/"
})
axiosQuotes.interceptors.response.use((res) => {
return {...res, data: parseQuotesData(res.data)};
})
export default axiosQuotes
.Context {
margin-top: 50px;
}
.Context > h2 {
text-transform: capitalize;
text-align: center;
font-size: 35px;
color: white;
margin-bottom: 25px;
}
.Quotes {
width: 100%;
margin: 0 auto;
padding: 0 60px;
height: 500px;
overflow-y: auto;
overflow-x: auto;
}
::-webkit-scrollbar {
display: none;
}
\ No newline at end of file
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { QuoteCard } from "../../Components/Card/Card";
import { useAppSelector } from "../../Store";
import { useAppDispatch } from "../../Store";
import { shallowEqual } from "react-redux";
import { getQuotes } from "../../Store/slices/quotes.slice";
import { deleteQuote } from "../../Store/slices/quotes.slice";
import "./Context.css";
export const Context = () => {
const {param} = useParams()
const dispatch = useAppDispatch()
const {quotes} = useAppSelector(state => state.quotes, shallowEqual)
useEffect(() => {
dispatch(getQuotes())
}, [dispatch, param])
const onDeleteHandler = (id: string) => {
dispatch(deleteQuote(id))
}
return (
<div className="Context">
<h2>{param}</h2>
<div className="Quotes">
{
quotes?.map(item => (
<QuoteCard
key={item.id}
text={item.text}
author={item.author}
onDelete={() => onDeleteHandler(item?.id || "")}
/>
))
}
</div>
</div>
)
};
\ No newline at end of file
import { useNavigate } from "react-router-dom";
import { Form } from "../../Components/Form/Form";
import { notification } from 'antd';
import { useAppSelector } from "../../Store";
import { useAppDispatch } from "../../Store";
import { onChangeState, postQuotes } from "../../Store/slices/quotes.slice";
import { shallowEqual } from "react-redux";
import "./ContextFrom.css";
export const ContextForm = () => {
const navigate = useNavigate()
const dispatch = useAppDispatch()
const {values} = useAppSelector(state => state.quotes, shallowEqual)
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const {name, value} = e.target
dispatch(onChangeState({type: "input", name, value}))
}
const onChangeSelectHandler = (value: string) => {
dispatch(onChangeState({type: "select", value}))
}
const onSubmitHandler = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const validate = Object.values(values).every(item => item !== "");
if(validate) {
dispatch(postQuotes(values))
navigate(`/quotes/${values.category}`)
} else {
notification.warning({
message: "Field all input",
placement: "top"
})
}
}
return (
<div className="ContextForm">
<Form
onSubmit={onSubmitHandler}
onChangeSelect={onChangeSelectHandler}
onChange={onChangeHandler}
valueSelect={values.category}
valueAuthor={values.author}
valueText={values.text}
/>
</div>
)
}
\ No newline at end of file
.ContextForm {
width: 50%;
height: 80vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
\ No newline at end of file
export const menuItems = [
{
text: "All",
to: "/"
},
{
text: "Star Wars",
to: "/quotes/star-wars"
},
{
text: "Famous people",
to: "/quotes/famous-people"
},
{
text: "Saying",
to: "/quotes/saying"
},
{
text: "Humour",
to: "/quotes/humour"
},
{
text: "Motivational",
to: "/quotes/motivational"
}
]
\ No newline at end of file
export const optionsItem = [
{
label: "Star Wars",
value: "star-wars"
},
{
label: "Famous people",
value: "famous-people"
},
{
label: "Saying",
value: "saying"
},
{
label: "Humour",
value: "humour"
},
{
label: "Motivational",
value: "motivational"
}
]
\ No newline at end of file
import { TQuote } from "../interfaces/quote";
export interface IGetQuote<T> {
[key: string]: T
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const parseQuotesData = (data: IGetQuote<{[key: string]: any}>): TQuote[] => {
return Object.keys(data).map(id => {
return {
id,
text: data[id].text,
category: data[id].category,
author: data[id].author
}
})
};
\ No newline at end of file
import axiosQuotes from "../Config/axiosInstanse";
import { TQuote } from "../interfaces/quote";
class ApiConnector {
async getQuotes() {
const {data} = await axiosQuotes.get("quotes.json");
return data;
}
async postQuotes(body: TQuote) {
const {data} = await axiosQuotes.post("quotes.json", body);
return data;
}
async deleteQuotes(id: string) {
await axiosQuotes.delete(`quotes/${id}.json`);
return id;
}
}
export const apiConnector = new ApiConnector();
\ No newline at end of file
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { QuotesSlice } from "./slices/quotes.slice";
export const store = configureStore({
reducer: {
quotes: QuotesSlice.reducer
}
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
\ No newline at end of file
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { TQuote, TQuoteState } from "../../interfaces/quote";
import { apiConnector } from "../../Integrations/api.connector";
export const getQuotes = createAsyncThunk(
"quotes/getQuotes",
async () => {
try {
return await apiConnector.getQuotes();
} catch (e) {
throw new Error(e as string)
}
}
)
export const postQuotes = createAsyncThunk(
"quotes/postQuotes",
async (body: TQuote) => {
try {
return await apiConnector.postQuotes(body);
} catch (e) {
throw new Error(e as string)
}
}
)
export const deleteQuote = createAsyncThunk(
"quotes/deleteQuote",
async (id: string, {dispatch}) => {
try {
dispatch(deleteLocalQuote(id))
await apiConnector.deleteQuotes(id);
} catch (e) {
throw new Error(e as string)
}
}
)
const initialState: TQuoteState = {
isLoading: false,
quotes: null,
values: {
category: "",
text: "",
author: ""
}
}
export const QuotesSlice = createSlice({
name: "quotes",
initialState,
reducers: {
deleteLocalQuote(state, action) {
const index = state.quotes?.findIndex(item => item.id === action.payload) || 0
state.quotes?.splice(index, 1)
},
onChangeState(state, action) {
const {type, name, value} = action.payload as {type: string, name?: string, value: string}
if(type === "input") {
name === "text" && (state.values.text = value)
name === "author" && (state.values.author = value)
} else if(type === "select") {
state.values.category = value
}
}
},
extraReducers: builder => {
builder
//Get
.addCase(getQuotes.pending, (state) => {
state.isLoading = true
})
.addCase(getQuotes.rejected, (state) => {
state.isLoading = false
})
.addCase(getQuotes.fulfilled, (state, action) => {
state.quotes = action.payload
})
//POST
.addCase(postQuotes.pending, (state) => {
state.isLoading = true
})
.addCase(postQuotes.rejected, (state) => {
state.isLoading = false
})
.addCase(postQuotes.fulfilled, (state) => {
state.isLoading = false;
})
// Delete
.addCase(deleteQuote.pending, (state) => {
state.isLoading = true
})
.addCase(deleteQuote.rejected, (state) => {
state.isLoading = false
})
.addCase(deleteQuote.fulfilled, (state) => {
state.isLoading = false
})
}
})
export const {deleteLocalQuote, onChangeState} = QuotesSlice.actions
\ No newline at end of file
export type TQuote = {
id?: string,
text: string,
category: string,
author: string
}
export type TQuoteState = {
isLoading: boolean,
quotes: TQuote[] | null,
values: {
category: string,
author: string,
text: string
}
}
\ No newline at end of file
* {
box-sizing: border-box;
margin: 0;
text-decoration: none;
}
\ No newline at end of file
import ReactDOM from 'react-dom/client'
import {App} from './App.tsx'
import "./main.css"
ReactDOM.createRoot(document.getElementById('root')!).render(
<App />
)
/// <reference types="vite/client" />
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
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