├── 1_Students App ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── @types.ts │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── absents │ │ │ └── absents.component.tsx │ │ ├── add-form │ │ │ ├── add-form.component.tsx │ │ │ └── add-form.css │ │ ├── common │ │ │ └── guarded-route │ │ │ │ └── guarded-route.component.tsx │ │ ├── courses-list-form │ │ │ └── courses-list-form.component.tsx │ │ ├── courses-list │ │ │ ├── courses-list.component.tsx │ │ │ └── courses-list.css │ │ ├── nav-bar │ │ │ ├── nav-bar.component.tsx │ │ │ └── nav-bar.css │ │ └── student │ │ │ ├── student.component.tsx │ │ │ └── student.css │ ├── hooks │ │ └── local-storage.hook.ts │ ├── index.css │ ├── main.tsx │ ├── providers │ │ ├── authProvider.tsx │ │ └── stateProvider.tsx │ ├── screens │ │ ├── About.screen.tsx │ │ ├── AddStudent.screen.tsx │ │ ├── Login.screen.tsx │ │ ├── Main.screen.tsx │ │ ├── NotFound.screen.tsx │ │ └── StudentDetails.screen.tsx │ ├── state │ │ └── reducer.ts │ ├── utils │ │ └── validation.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── 2_Todo App ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── dashboard │ │ │ ├── dashboard-component.tsx │ │ │ └── dashboard.css │ │ ├── form │ │ │ ├── form.component.tsx │ │ │ └── form.css │ │ ├── todo-item │ │ │ ├── todo-item.component.tsx │ │ │ └── todo-item.css │ │ ├── todo-list │ │ │ ├── todo-list.component.tsx │ │ │ └── todo-list.css │ │ └── types.ts │ ├── hooks │ │ └── local-storage.hook.ts │ ├── main.tsx │ ├── state │ │ └── reducer.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── 3_next-example ├── .gitignore ├── README.md ├── app │ ├── antd │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── news │ │ └── [postId] │ │ │ └── page.tsx │ ├── page.module.css │ ├── page.tsx │ └── reach │ │ ├── about │ │ └── page.tsx │ │ ├── contact │ │ └── page.tsx │ │ ├── layout.module.css │ │ ├── layout.tsx │ │ └── portfolio │ │ └── page.tsx ├── components │ ├── Form.tsx │ └── main-header │ │ ├── MainHeader.tsx │ │ └── main-header.module.css ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── vercel.svg │ └── window.svg ├── tailwind.config.ts └── tsconfig.json ├── 4_news_app ├── .env ├── .gitignore ├── README.md ├── app │ ├── (auth) │ │ ├── layout.module.css │ │ ├── layout.tsx │ │ └── user │ │ │ ├── login │ │ │ ├── login.module.css │ │ │ └── page.tsx │ │ │ └── signup │ │ │ ├── page.tsx │ │ │ └── signup.module.css │ ├── (main) │ │ ├── @latestgb │ │ │ └── page.tsx │ │ ├── @latestus │ │ │ └── page.tsx │ │ ├── add-news │ │ │ ├── add-article.module.css │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── categories │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── news-list │ │ │ ├── [[...slug]] │ │ │ │ ├── loading-deleted.tsx │ │ │ │ ├── news-list.module.css │ │ │ │ ├── news-list.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.module.css │ │ │ └── layout.tsx │ │ ├── news │ │ │ └── [slug] │ │ │ │ ├── article.module.css │ │ │ │ └── page.tsx │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── login │ │ │ │ └── route.ts │ │ ├── news │ │ │ ├── [slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── route.ts │ ├── error.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── loading.tsx │ └── not-found.tsx ├── components │ ├── add-article │ │ ├── AddArticle.tsx │ │ ├── SubmitArticle.tsx │ │ └── add-article.module.css │ ├── article-item │ │ ├── ArticleItem.tsx │ │ └── article-item.module.css │ ├── auth │ │ └── login-form │ │ │ ├── LoginForm.tsx │ │ │ └── login.module.css │ ├── categories │ │ ├── Categories.tsx │ │ └── categories.module.css │ ├── category │ │ ├── Category.tsx │ │ └── category.module.css │ ├── header │ │ ├── Header.tsx │ │ ├── NavLink.tsx │ │ └── header.module.css │ ├── hero │ │ ├── Hero.tsx │ │ └── hero.module.css │ └── latest-news │ │ ├── LatestNews.tsx │ │ ├── item │ │ ├── Item.tsx │ │ └── item.module.css │ │ └── latest-news.module.css ├── constants │ └── data.ts ├── controllers │ └── news-actions.ts ├── eslint.config.mjs ├── initDatabase.js ├── middleware.ts ├── news.db ├── next.config.ts ├── package-lock.json ├── package.json ├── public │ ├── 404.svg │ ├── cat1.png │ ├── cat2.png │ ├── cat3.jpg │ ├── cats │ │ ├── finance.webp │ │ ├── gaza.webp │ │ ├── global.webp │ │ ├── palestine.webp │ │ ├── sports.webp │ │ ├── weather.webp │ │ └── westbank.webp │ ├── file.svg │ ├── globe.svg │ ├── images │ │ ├── corporate-communications-strategist.jpg │ │ ├── customer-accounts-officer.jpg │ │ └── international-identity-orchestrator.jpg │ ├── logo.png │ ├── n1.jpg │ ├── next.svg │ ├── vercel.svg │ └── window.svg ├── services │ ├── auth.ts │ └── news.service.ts ├── tsconfig.json ├── types │ └── index.d.ts └── utils │ └── auth.ts ├── memory-card-game ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── screens.txt ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── card-list │ │ │ ├── card-list.css │ │ │ └── card-list.tsx │ │ ├── card │ │ │ ├── card.css │ │ │ └── card.tsx │ │ ├── congrats │ │ │ ├── congrats.css │ │ │ └── congrats.tsx │ │ ├── score-list │ │ │ ├── score-list.css │ │ │ └── score-list.tsx │ │ └── status-bar │ │ │ ├── status-bar.css │ │ │ └── status-bar.tsx │ ├── hooks │ │ └── game-logic.hook.ts │ ├── main.tsx │ ├── providers │ │ ├── modeProvider.tsx │ │ └── reducer.ts │ ├── screens │ │ ├── game.screen.tsx │ │ ├── levels.screen.tsx │ │ ├── not-found.screen.tsx │ │ ├── score-board.screen.tsx │ │ └── screens.css │ ├── types │ │ └── @types.ts │ ├── utils │ │ └── game.util.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── testing-ui ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── index.css ├── main.tsx └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /1_Students App/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /1_Students App/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@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 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /1_Students App/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | '@typescript-eslint/no-unused-vars': 'warn', 23 | '@typescript-eslint/no-empty-object-type': 'warn', 24 | '@typescript-eslint/no-explicit-any': 'warn', 25 | 'react-refresh/only-export-components': [ 26 | 'warn', 27 | { allowConstantExport: true }, 28 | ], 29 | }, 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /1_Students App/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /1_Students App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "first-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1", 15 | "react-router-dom": "^7.1.1" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.15.0", 19 | "@types/react": "^18.3.12", 20 | "@types/react-dom": "^18.3.1", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "eslint": "^9.15.0", 23 | "eslint-plugin-react-hooks": "^5.0.0", 24 | "eslint-plugin-react-refresh": "^0.4.14", 25 | "globals": "^15.12.0", 26 | "typescript": "~5.6.2", 27 | "typescript-eslint": "^8.15.0", 28 | "vite": "^6.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /1_Students App/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1_Students App/src/@types.ts: -------------------------------------------------------------------------------- 1 | export interface IStudent { 2 | id: string; 3 | name: string; 4 | age: number; 5 | absents: number; 6 | isGraduated: boolean; 7 | coursesList: string[]; 8 | } 9 | 10 | export interface IUserData { 11 | userName: string; 12 | role: Role; 13 | } 14 | 15 | export enum Role { 16 | ADMIN = 'admin', 17 | Teacher = 'teacher', 18 | GUEST = 'guest' 19 | } -------------------------------------------------------------------------------- /1_Students App/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | justify-content: center; 4 | align-items: flex-start; 5 | text-align: center; 6 | background-color: #fff6eb; 7 | } 8 | 9 | #root { 10 | width: 100%; 11 | } 12 | 13 | .main { 14 | padding: 30px 20px; 15 | } 16 | 17 | .main h1 { 18 | margin-top: 0; 19 | } 20 | 21 | .main-screen>.stats, .main-screen>.filter { 22 | display: flex; 23 | justify-content: flex-start; 24 | margin-bottom: 10px; 25 | column-gap: 10px; 26 | align-items: center; 27 | } 28 | 29 | .main-screen>.stats>:last-child { 30 | margin-left: auto; 31 | } 32 | 33 | .main.wrapper { 34 | border-left: 8px solid #DA498D; 35 | } 36 | 37 | .main-screen .list { 38 | flex-wrap: wrap; 39 | display: flex; 40 | justify-content: center; 41 | align-items: stretch; 42 | gap: 20px; 43 | margin-top: 20px; 44 | } 45 | 46 | .main-screen .list>.std-wrapper { 47 | flex: 0; 48 | flex-basis: 43%; 49 | } 50 | 51 | [class$="-screen"] h2 { 52 | font-size: 1.5rem; 53 | font-weight: bold; 54 | color: #DA498D; 55 | text-transform: uppercase; 56 | text-align: left; 57 | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 58 | } 59 | 60 | .add-screen, .about-screen { 61 | min-height: 500px; 62 | } 63 | 64 | div.spinner { 65 | border: 8px solid #f3f3f3; 66 | border-top: 8px solid #DA498D; 67 | border-radius: 50%; 68 | width: 50px; 69 | height: 50px; 70 | animation: spin 1s linear infinite; 71 | margin: 30px auto; 72 | } 73 | 74 | @keyframes spin { 75 | 0% { 76 | transform: rotate(0deg); 77 | } 78 | 79 | 100% { 80 | transform: rotate(360deg); 81 | } 82 | } -------------------------------------------------------------------------------- /1_Students App/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import Main from './screens/Main.screen'; 3 | import About from './screens/About.screen'; 4 | import NotFound from './screens/NotFound.screen'; 5 | import { Route, Routes } from 'react-router-dom'; 6 | import StudentDetails from './screens/StudentDetails.screen'; 7 | import { Role } from './@types'; 8 | import AddStudent from './screens/AddStudent.screen'; 9 | import Login from './screens/Login.screen'; 10 | import NavBar from './components/nav-bar/nav-bar.component'; 11 | import Guarded from './components/common/guarded-route/guarded-route.component'; 12 | 13 | function App() { 14 | const h1Style = { color: '#69247C', fontSize: '24px' }; 15 | 16 | return ( 17 |
18 |

Welcome to GSG React/Next Course

19 | 20 | 21 |
} /> 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | 28 |
29 | ) 30 | } 31 | 32 | export default App; -------------------------------------------------------------------------------- /1_Students App/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1_Students App/src/components/absents/absents.component.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from 'react' 2 | import { IStudent } from '../../@types'; 3 | import { AuthContext } from '../../providers/authProvider'; 4 | import { StateContext } from '../../providers/stateProvider'; 5 | 6 | type IProps = IStudent 7 | 8 | const Absents = (props: IProps) => { 9 | const [absents, setAbsents] = useState(props.absents); 10 | const [absentColor, setAbsentColor] = useState('#213547'); 11 | const prevAbsents = useRef(props.absents); 12 | const { user } = useContext(AuthContext); 13 | const { dispatch } = useContext(StateContext); 14 | 15 | 16 | useEffect(() => { 17 | if (absents >= 10) { 18 | setAbsentColor('#ff0000'); 19 | } else if (absents >= 7) { 20 | setAbsentColor('#fd9c0e'); 21 | } else if (absents >= 5) { 22 | setAbsentColor('#d6c728'); 23 | } else { 24 | setAbsentColor('#213547'); 25 | } 26 | }, [absents]); 27 | 28 | const addAbsent = () => { 29 | prevAbsents.current = absents; 30 | setAbsents(absents + 1); 31 | if (dispatch) { 32 | dispatch({ type: "UPDATE_ABSENTS", payload: { id: props.id, change: +1 } }); 33 | } 34 | } 35 | 36 | const removeAbsent = () => { 37 | if (absents - 1 >= 0) { 38 | prevAbsents.current = absents; 39 | setAbsents(absents - 1); 40 | if (dispatch) { 41 | dispatch({ type: "UPDATE_ABSENTS", payload: { id: props.id, change: -1 } }); 42 | } 43 | } 44 | } 45 | 46 | const resetAbsent = () => { 47 | prevAbsents.current = absents; 48 | setAbsents(0); 49 | if (dispatch) { 50 | dispatch({ type: "UPDATE_ABSENTS", payload: { id: props.id, change: -absents } }); 51 | } 52 | } 53 | 54 | return ( 55 |
56 | Absents: {absents} 57 | 58 | 59 | 60 |
61 | ) 62 | } 63 | 64 | export default Absents; -------------------------------------------------------------------------------- /1_Students App/src/components/add-form/add-form.component.tsx: -------------------------------------------------------------------------------- 1 | import './add-form.css'; 2 | import { useEffect, useState } from 'react'; 3 | import { IStudent } from '../../@types.ts'; 4 | import CoursesListForm from '../courses-list-form/courses-list-form.component'; 5 | import { validateStudent } from '../../utils/validation.ts'; 6 | import { useNavigate } from 'react-router-dom'; 7 | 8 | const INITIAL_STUDENT = { age: 0, coursesList: [], id: '', isGraduated: false, name: '', absents: 0 }; 9 | 10 | interface IProps { 11 | className?: string; 12 | onSubmit: (std: IStudent) => void; 13 | } 14 | 15 | const AddForm = (props: IProps) => { 16 | const [student, setStudent] = useState(INITIAL_STUDENT); 17 | const [isOpen, setIsOpen] = useState(true); 18 | const [errorsList, setErrorsList] = useState([]); 19 | const [message, setMessage] = useState(''); 20 | const nav = useNavigate(); 21 | 22 | useEffect(() => { 23 | console.log("Hello from Add Form component!"); 24 | }, []); 25 | 26 | const handleChange = (field: string, value: any) => { 27 | setStudent({ ...student, [field]: value }); 28 | } 29 | 30 | const handleSubmit = () => { 31 | const newStudent: IStudent = { ...student, id: Date.now().toString() }; 32 | 33 | const errors = validateStudent(newStudent); 34 | if (errors.length > 0) { 35 | setErrorsList(errors); 36 | } else { 37 | setErrorsList([]); 38 | props.onSubmit(newStudent); 39 | handleClear(); 40 | setMessage('Student Added Successfully'); 41 | setTimeout(() => { 42 | nav('/'); 43 | }, 1500); 44 | } 45 | } 46 | 47 | const handleClear = () => { 48 | setStudent(INITIAL_STUDENT); 49 | } 50 | 51 | const handleCoursesChange = (list: string[]) => { 52 | setStudent({ ...student, coursesList: list }); 53 | } 54 | 55 | return ( 56 |
57 | 61 |
62 | 63 | handleChange('name', e.target.value)} 68 | /> 69 |
70 |
71 | 72 | handleChange('age', e.target.value)} 79 | /> 80 |
81 |
82 | 83 | handleChange('isGraduated', e.target.checked)} 88 | /> 89 |
90 | 91 |
92 | 98 | 99 |
100 | { 101 | Boolean(errorsList.length) && ( 102 |
103 |

You have the following error/s:

104 | { 105 | errorsList.map(error =>

- {error}

) 106 | } 107 |
108 | ) 109 | } 110 | {Boolean(message) &&

{message}

} 111 |
112 | ) 113 | }; 114 | 115 | export default AddForm; -------------------------------------------------------------------------------- /1_Students App/src/components/add-form/add-form.css: -------------------------------------------------------------------------------- 1 | .wrapper.addForm.closed { 2 | max-height: 32px; 3 | overflow: hidden; 4 | } 5 | 6 | .wrapper.addForm.open { 7 | overflow: hidden; 8 | max-height: 1000px; 9 | } 10 | 11 | .wrapper.addForm { 12 | display: flex; 13 | flex-direction: column; 14 | row-gap: 10px; 15 | margin-bottom: 30px; 16 | border-right: 3px solid gray; 17 | border-radius: 4px; 18 | padding-right: 5px; 19 | background-color: #fff6eb; 20 | transition: 200ms max-height ease-in; 21 | } 22 | 23 | .wrapper.addForm .input { 24 | display: flex; 25 | column-gap: 10px; 26 | } 27 | 28 | .wrapper.addForm label { 29 | min-width: 110px; 30 | display: inline-block; 31 | text-align: left; 32 | } 33 | 34 | .wrapper.addForm .Actions { 35 | display: flex; 36 | justify-content: flex-end; 37 | column-gap: 10px; 38 | } 39 | 40 | .wrapper.addForm .addCourseForm form { 41 | display: flex; 42 | justify-content: flex-start; 43 | column-gap: 20px; 44 | } 45 | 46 | .wrapper.addForm .addCourseForm ul { 47 | display: flex; 48 | list-style-type: none; 49 | flex-wrap: wrap; 50 | gap: 10px; 51 | padding: 0; 52 | } 53 | 54 | .wrapper.addForm .report { 55 | color: #ee5454; 56 | text-align: left; 57 | font-size: 12px; 58 | } 59 | 60 | .wrapper.addForm .report h4 { 61 | margin: 0; 62 | } 63 | 64 | .wrapper.addForm .report p { 65 | margin: 0; 66 | line-height: 1em; 67 | } 68 | 69 | .wrapper.addForm .toggle-btn { 70 | width: 150px; 71 | margin-left: auto; 72 | } -------------------------------------------------------------------------------- /1_Students App/src/components/common/guarded-route/guarded-route.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { AuthContext } from '../../../providers/authProvider'; 3 | import { Link } from 'react-router-dom'; 4 | import { Role } from '../../../@types'; 5 | 6 | interface IProps { 7 | children: React.ReactNode; 8 | roles: Role[]; 9 | } 10 | 11 | const Guarded = (props: IProps) => { 12 | const { user, loading } = useContext(AuthContext); 13 | 14 | if (loading) { 15 | return null; 16 | } 17 | 18 | if (user === null) { // User is not logged in 19 | return ( 20 |
21 |

You must be logged in to see this screen!

22 | Login in here 23 |
24 | ); 25 | } else if (!props.roles.includes(user.role)) { // User doesn't have permission 26 | return ( 27 |
28 |

You don't have sufficient permissions to see this screen!

29 |
30 | ); 31 | } 32 | 33 | return props.children; 34 | } 35 | 36 | export default Guarded; -------------------------------------------------------------------------------- /1_Students App/src/components/courses-list-form/courses-list-form.component.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | interface IProps { 4 | value: string[]; 5 | onSubmit: (list: string[]) => void; 6 | } 7 | 8 | const CoursesListForm = (props: IProps) => { 9 | const [courseList, setCoursesList] = useState(props.value); 10 | const inputRef = useRef(null); 11 | 12 | useEffect(() => { 13 | setCoursesList(props.value); 14 | }, [props.value]); 15 | 16 | const handleSubmit = (event: React.FormEvent) => { 17 | event.preventDefault(); 18 | const newCourse = event.currentTarget["courseName"].value; 19 | const newList = [...courseList, newCourse]; 20 | setCoursesList(newList); 21 | props.onSubmit(newList); 22 | 23 | if (inputRef.current) { 24 | inputRef.current.value = ""; 25 | } 26 | } 27 | 28 | return ( 29 |
30 |
31 |
32 | 33 | 34 |
35 | 36 |
37 |
    38 | {courseList.map((course, index) =>
  • {course}
  • )} 39 |
40 |
41 | ) 42 | }; 43 | 44 | export default CoursesListForm; -------------------------------------------------------------------------------- /1_Students App/src/components/courses-list/courses-list.component.tsx: -------------------------------------------------------------------------------- 1 | 2 | interface IProps { 3 | list: string[]; 4 | } 5 | 6 | const CoursesList = (props: IProps) => { 7 | return ( 8 |
    9 | { 10 | props.list.map((item, index) =>
  • {item}
  • ) 11 | } 12 |
13 | ) 14 | } 15 | 16 | export default CoursesList; -------------------------------------------------------------------------------- /1_Students App/src/components/courses-list/courses-list.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/1_Students App/src/components/courses-list/courses-list.css -------------------------------------------------------------------------------- /1_Students App/src/components/nav-bar/nav-bar.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import './nav-bar.css'; 3 | import { Link, useLocation } from 'react-router-dom'; 4 | import { AuthContext } from '../../providers/authProvider'; 5 | import { Role } from '../../@types'; 6 | 7 | const NavBar = () => { 8 | const location = useLocation(); 9 | const { user, logout } = useContext(AuthContext); 10 | 11 | const handleLogout = (e: React.MouseEvent) => { 12 | e.preventDefault(); 13 | logout(); 14 | } 15 | 16 | return ( 17 | 38 | ) 39 | } 40 | 41 | export default NavBar -------------------------------------------------------------------------------- /1_Students App/src/components/nav-bar/nav-bar.css: -------------------------------------------------------------------------------- 1 | 2 | nav { 3 | margin: 20px 0; 4 | border-bottom: 2px solid #DA498D; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | nav span { 11 | display: flex; 12 | column-gap: 10px; 13 | align-items: center; 14 | } 15 | 16 | nav a { 17 | padding: 3px; 18 | font-size: 18px; 19 | font-weight: 400; 20 | /* TODO: selected tab */ 21 | } 22 | 23 | nav a:hover { 24 | text-shadow: 0px 0px 2px rgb(105, 36, 124); 25 | color: #93729d; 26 | } 27 | 28 | nav a.active { 29 | border-bottom: 3px solid #DA498D; 30 | } 31 | -------------------------------------------------------------------------------- /1_Students App/src/components/student/student.component.tsx: -------------------------------------------------------------------------------- 1 | import './student.css'; 2 | import { IStudent } from '../../@types'; 3 | import CoursesList from '../courses-list/courses-list.component'; 4 | import { Link } from 'react-router-dom'; 5 | import Absents from '../absents/absents.component'; 6 | 7 | interface IProps extends IStudent { 8 | mode: 'details' | 'list'; 9 | } 10 | 11 | const Student = (props: IProps) => { 12 | return ( 13 |
14 |
15 | Student: 16 | { 17 | props.mode === 'list' 18 | ? {props.name.toUpperCase()} 19 | : props.name.toUpperCase() 20 | } 21 |
22 |
23 | Age: {props.age} 24 |
25 |
26 | Is Graduated: {props.isGraduated ? 'Yes' : 'No'} 27 |
28 |
29 | Courses List: 30 | 31 |
32 | { 33 | props.mode === 'list' && 34 | } 35 |
36 | ) 37 | } 38 | 39 | export default Student; -------------------------------------------------------------------------------- /1_Students App/src/components/student/student.css: -------------------------------------------------------------------------------- 1 | .std-wrapper { 2 | background-color: #ffffff; 3 | border-radius: 20px; 4 | box-shadow: 4px 4px 9px -7px #b4b4b4; 5 | padding: 16px; 6 | } 7 | 8 | .std-wrapper ul.courses-list { 9 | list-style-type: none; 10 | display: flex; 11 | gap: 10px; 12 | flex-wrap: wrap; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | .std-wrapper .data-field { 18 | text-align: left; 19 | display: flex; 20 | flex-direction: row; 21 | column-gap: 20px; 22 | } 23 | 24 | .std-wrapper .data-field b { 25 | color: #5b5b5b; 26 | } 27 | 28 | .std-wrapper .absents { 29 | display: flex; 30 | justify-content: flex-end; 31 | column-gap: 10px; 32 | margin-top: 20px; 33 | } -------------------------------------------------------------------------------- /1_Students App/src/hooks/local-storage.hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | const useLocalStorage = (state: any, storageKey: string) => { 4 | const [storedData, setStoredData] = useState({ data: null, loading: true }); 5 | const isInitialMount = useRef(true); 6 | 7 | useEffect(() => { 8 | // Read the data on the first render 9 | const strData = localStorage.getItem(storageKey); 10 | try { 11 | if (strData !== null) { 12 | setStoredData({ data: JSON.parse(strData), loading: false }); 13 | } else { 14 | setStoredData({ data: null, loading: false }); 15 | } 16 | } catch { 17 | setStoredData({ data: strData || null, loading: false }); 18 | } 19 | }, []); 20 | 21 | useEffect(() => { 22 | if (isInitialMount.current) { 23 | isInitialMount.current = false; 24 | return; 25 | } 26 | 27 | if (typeof (state) === 'object') { 28 | localStorage.setItem(storageKey, JSON.stringify(state)); 29 | } else if (state === null) { 30 | localStorage.removeItem(storageKey); 31 | } else { 32 | localStorage.setItem(storageKey, state.toString()); 33 | } 34 | }, [state, storageKey]); 35 | 36 | return { storedData: storedData.data, loading: storedData.loading }; 37 | } 38 | 39 | export default useLocalStorage; -------------------------------------------------------------------------------- /1_Students App/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.3em .7em; 43 | font-size: 14px; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #f9f9f9; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | border-color: #FAC67A; 50 | } 51 | 52 | button:disabled { 53 | pointer-events: none; 54 | cursor: not-allowed; 55 | } 56 | 57 | button:hover { 58 | border-color: #866634; 59 | background-color: #dc9fbc; 60 | } 61 | 62 | input, select { 63 | font-size: 14px; 64 | padding: 5px; 65 | border-radius: 5px; 66 | border: 1px solid #FAC67A; 67 | outline: 0; 68 | } 69 | 70 | @media (prefers-color-scheme: light) { 71 | :root { 72 | color: #213547; 73 | background-color: #ffffff; 74 | } 75 | 76 | a:hover { 77 | color: #747bff; 78 | } 79 | 80 | button { 81 | background-color: #f9f9f9; 82 | } 83 | } -------------------------------------------------------------------------------- /1_Students App/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import './index.css'; 3 | import App from './App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { AuthProvider } from './providers/authProvider'; 6 | import StateProvider from './providers/stateProvider'; 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /1_Students App/src/providers/authProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useLayoutEffect, useState } from "react"; 2 | import { IUserData } from "../@types"; 3 | import useLocalStorage from "../hooks/local-storage.hook"; 4 | 5 | export interface IAuthContext { 6 | user: IUserData | null; 7 | loading: boolean; 8 | login: (data: IUserData) => void; 9 | logout: () => void; 10 | } 11 | 12 | export const AuthContext = createContext({ user: null, login: () => { }, logout: () => { }, loading: true }); 13 | 14 | export const AuthProvider = (props: { children: React.ReactNode }) => { 15 | const [user, setUser] = useState(null); 16 | const { storedData, loading } = useLocalStorage(user, 'auth-user'); 17 | 18 | useLayoutEffect(() => { 19 | if (!loading) { 20 | setUser(storedData); 21 | } 22 | }, [storedData, loading]); 23 | 24 | const login = (data: IUserData) => { 25 | if (data.userName.length >= 3) { 26 | setUser(data); 27 | } else { 28 | setUser(null); 29 | } 30 | } 31 | 32 | const logout = () => { 33 | setUser(null); 34 | } 35 | 36 | const data = { user, loading, login, logout }; 37 | 38 | return {props.children}; 39 | }; -------------------------------------------------------------------------------- /1_Students App/src/providers/stateProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useLayoutEffect, useReducer } from 'react' 2 | import { stateReducer, State, Action } from '../state/reducer'; 3 | import useLocalStorage from '../hooks/local-storage.hook'; 4 | import { IStudent } from '../@types'; 5 | 6 | interface IProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | interface IStateContext { 11 | state: State; 12 | loading: boolean; 13 | dispatch: React.Dispatch; 14 | } 15 | 16 | const INTI_STATE = { state: { totalAbsents: 0, studentsList: [] }, loading: true, dispatch: () => { } }; 17 | 18 | export const StateContext = createContext(INTI_STATE); 19 | 20 | const StateProvider = (props: IProps) => { 21 | const [state, dispatch] = useReducer(stateReducer, { studentsList: [], totalAbsents: 0 }); 22 | const { storedData, loading } = useLocalStorage(state.studentsList, 'students-list'); 23 | 24 | useLayoutEffect(() => { 25 | if (!loading) { 26 | const stdList: IStudent[] = storedData || []; 27 | dispatch({ type: "INIT", payload: stdList }); 28 | } 29 | }, [loading, storedData]); 30 | 31 | return ( 32 | {props.children} 33 | ) 34 | } 35 | 36 | export default StateProvider -------------------------------------------------------------------------------- /1_Students App/src/screens/About.screen.tsx: -------------------------------------------------------------------------------- 1 | const About = () => { 2 | return ( 3 |
4 |

About Students App

5 |

6 | This is an app that we use to explain topics of react in GSG training.
7 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui corrupti optio omnis soluta nam quas accusamus magni. Ipsa quos ipsam quae voluptas tempore quaerat, quidem veniam illum, eius autem cumque? 8 |

9 |
10 | ) 11 | } 12 | 13 | export default About; -------------------------------------------------------------------------------- /1_Students App/src/screens/AddStudent.screen.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import AddForm from "../components/add-form/add-form.component"; 3 | import { StateContext } from "../providers/stateProvider"; 4 | 5 | const AddStudent = () => { 6 | const { dispatch } = useContext(StateContext); 7 | 8 | return ( 9 |
10 |

Add New Student

11 | dispatch({ type: "ADD_STUDENT", payload: newStudent })} /> 12 |
13 | ) 14 | } 15 | 16 | export default AddStudent; -------------------------------------------------------------------------------- /1_Students App/src/screens/Login.screen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { AuthContext } from '../providers/authProvider'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { Role } from '../@types'; 5 | 6 | const Login = () => { 7 | const { login } = useContext(AuthContext); 8 | const navigate = useNavigate(); 9 | 10 | const handleLogin = (e: React.FormEvent) => { 11 | e.preventDefault(); 12 | const userName = e.currentTarget['userName'].value; 13 | const role = (e.currentTarget['role'] as any).value; 14 | 15 | if (userName) { 16 | login({ userName, role }); 17 | navigate('/'); 18 | } 19 | } 20 | 21 | return ( 22 |
23 |

Please enter your login data

24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 36 |
37 |
38 | 39 |
40 |
41 |
42 | ) 43 | } 44 | 45 | export default Login; -------------------------------------------------------------------------------- /1_Students App/src/screens/NotFound.screen.tsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return ( 3 | <> 4 |

Page Not found (404)

5 |

6 | We can't find the page you are looking for 7 |

8 | 9 | ) 10 | } 11 | 12 | export default NotFound -------------------------------------------------------------------------------- /1_Students App/src/screens/StudentDetails.screen.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useParams } from 'react-router-dom'; 2 | import Student from '../components/student/student.component' 3 | import { IStudent } from '../@types'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | const StudentDetails = () => { 7 | const { id } = useParams(); 8 | const navigate = useNavigate(); 9 | const [currentStudent, setCurrentStudent] = useState(); 10 | 11 | useEffect(() => { 12 | // find the student with ID id from the local storage database 13 | const studentsListStr = localStorage.getItem("students-list"); 14 | if (studentsListStr) { 15 | const stdList: IStudent[] = JSON.parse(studentsListStr); 16 | const std = stdList.find(item => item.id === id); 17 | if (std) { 18 | setCurrentStudent(std); 19 | } else { 20 | navigate('/404'); 21 | } 22 | } 23 | }, [id]); 24 | 25 | return ( 26 |
27 |

Student Details: {currentStudent?.name}

28 | { 29 | currentStudent && ( 30 | 39 | ) 40 | } 41 |
42 | ) 43 | } 44 | 45 | export default StudentDetails -------------------------------------------------------------------------------- /1_Students App/src/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { IStudent } from "../@types"; 2 | 3 | export type State = { 4 | studentsList: IStudent[]; 5 | totalAbsents: number; 6 | }; 7 | 8 | export type Action = 9 | | { type: "INIT"; payload: IStudent[] } 10 | | { type: "ADD_STUDENT"; payload: IStudent } 11 | | { type: "REMOVE_FIRST" } 12 | | { type: "UPDATE_ABSENTS"; payload: { id: string; change: number } }; 13 | 14 | export const stateReducer = (state: State, action: Action): State => { 15 | switch (action.type) { 16 | case "INIT": { 17 | const totalAbsents: number = action.payload.reduce( 18 | (prev, cur) => prev + cur.absents, 0); 19 | return { studentsList: action.payload, totalAbsents }; 20 | } 21 | case "ADD_STUDENT": 22 | return { 23 | ...state, 24 | studentsList: [action.payload, ...state.studentsList], 25 | }; 26 | case "REMOVE_FIRST": 27 | return { 28 | ...state, 29 | studentsList: state.studentsList.slice(1), 30 | }; 31 | case "UPDATE_ABSENTS": 32 | return { 33 | studentsList: state.studentsList.map((student) => 34 | student.id === action.payload.id 35 | ? { ...student, absents: student.absents + action.payload.change } 36 | : student 37 | ), 38 | totalAbsents: state.totalAbsents + action.payload.change, 39 | }; 40 | default: 41 | return state; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /1_Students App/src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | import { IStudent } from "../@types"; 2 | 3 | const validateStudent = (newStudent: IStudent) => { 4 | const errors: string[] = []; 5 | // Validate the object before sending it. 6 | if (newStudent.name.length < 3) { 7 | errors.push("The name must be more than 3 letters"); 8 | } 9 | 10 | if (newStudent.age < 17 || newStudent.age > 40) { 11 | errors.push("The age must be between 17 and 40"); 12 | } 13 | 14 | if (newStudent.coursesList.length <= 0) { 15 | errors.push("You must add at least one course"); 16 | } 17 | 18 | return errors 19 | } 20 | 21 | export { 22 | validateStudent 23 | } -------------------------------------------------------------------------------- /1_Students App/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /1_Students App/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /1_Students App/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /1_Students App/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUncheckedSideEffectImports": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /1_Students App/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /2_Todo App/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /2_Todo App/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@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 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /2_Todo App/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | '@typescript-eslint/no-explicit-any': 'warn', 23 | 'react-refresh/only-export-components': [ 24 | 'warn', 25 | { allowConstantExport: true }, 26 | ], 27 | }, 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /2_Todo App/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /2_Todo App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@phosphor-icons/react": "^2.1.7", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.17.0", 19 | "@types/react": "^18.3.18", 20 | "@types/react-dom": "^18.3.5", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "eslint": "^9.17.0", 23 | "eslint-plugin-react-hooks": "^5.0.0", 24 | "eslint-plugin-react-refresh": "^0.4.16", 25 | "globals": "^15.14.0", 26 | "typescript": "~5.6.2", 27 | "typescript-eslint": "^8.18.2", 28 | "vite": "^6.0.5" 29 | } 30 | } -------------------------------------------------------------------------------- /2_Todo App/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2_Todo App/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 3 | } 4 | 5 | h1 { 6 | font-size: 16px; 7 | } 8 | 9 | h1.light { 10 | color: #000; 11 | } 12 | 13 | h1.dark { 14 | color: #fff; 15 | } -------------------------------------------------------------------------------- /2_Todo App/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect, useReducer, useRef, useState } from 'react' 2 | import './App.css' 3 | import Dashboard from './components/dashboard/dashboard-component' 4 | import Form from './components/form/form.component' 5 | import TodoList from './components/todo-list/todo-list.component' 6 | import { ITodoItem } from './components/types' 7 | import useLocalStorage from './hooks/local-storage.hook' 8 | import reducer from './state/reducer' 9 | import { ThemeContext } from './main' 10 | 11 | function App() { 12 | const [date, setDate] = useState(''); 13 | const timerRef = useRef(); 14 | const [state, dispatch] = useReducer(reducer, { todos: [], userName: 'Ahmad' }); 15 | const { theme, setTheme } = useContext(ThemeContext); 16 | 17 | const { storedData } = useLocalStorage(state.todos, 'todo-list'); 18 | 19 | useEffect(() => { 20 | document.body.style.backgroundColor = theme === 'light' ? '#fff' : '#000'; 21 | }, [theme]); 22 | 23 | useEffect(() => { 24 | dispatch({ type: 'INIT_TODOS', payload: storedData || [] }); 25 | }, [storedData]); 26 | 27 | useEffect(() => { 28 | timerRef.current = setInterval(() => { 29 | setDate(new Date().toLocaleTimeString()); 30 | }, 1000); 31 | }, []); 32 | 33 | const stopTime = () => { 34 | if (timerRef.current) { 35 | clearInterval(timerRef.current); 36 | } 37 | } 38 | 39 | const handleNewItem = useCallback((item: ITodoItem) => { 40 | dispatch({ type: 'ADD_TODO', payload: item }); 41 | }, [state.todos]); 42 | 43 | const handleTaskToggle = (e: React.ChangeEvent) => { 44 | const itemId = Number(e.target.dataset["itemId"]); 45 | dispatch({ type: 'TOGGLE_TODO', payload: itemId }); 46 | } 47 | 48 | const handleDelete = (index: number) => { 49 | // This will delete the item at index! 50 | const itemId = state.todos[index].id; 51 | dispatch({ type: 'REMOVE_TODO', payload: itemId }); 52 | } 53 | 54 | const toggleTheme = () => { 55 | setTheme(old => (old === 'light' ? 'dark' : 'light')); 56 | } 57 | 58 | return ( 59 |
60 |

61 | Todo App - Hello {state.userName} - {date} 62 | 63 | 64 |

65 |
66 | 67 | 68 |
69 | ) 70 | } 71 | 72 | export default App 73 | -------------------------------------------------------------------------------- /2_Todo App/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2_Todo App/src/components/dashboard/dashboard-component.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react'; 2 | import { ITodoItem } from '../types'; 3 | import './dashboard.css'; 4 | import { ThemeContext } from '../../main'; 5 | 6 | interface IProps { 7 | items: ITodoItem[]; 8 | } 9 | 10 | const Dashboard = (props: IProps) => { 11 | const { theme } = useContext(ThemeContext) 12 | const urgentCount = useMemo(() => { 13 | return props.items.filter(item => item.isUrgent).length; 14 | }, [props.items]); 15 | 16 | const completedCount = useMemo(() => { 17 | return props.items.filter(item => item.isDone).length; 18 | }, [props.items]); 19 | 20 | return ( 21 |
22 |
23 | {props.items.length} 24 | Created Tasks 25 |
26 |
27 | {urgentCount} 28 | Urgent Tasks 29 |
30 |
31 | {completedCount} 32 | Completed Tasks 33 |
34 |
35 | ) 36 | } 37 | 38 | export default Dashboard -------------------------------------------------------------------------------- /2_Todo App/src/components/dashboard/dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard-wrapper { 2 | border: 1px solid black; 3 | margin: 20px; 4 | padding: 10px; 5 | column-gap: 10px; 6 | display: flex; 7 | justify-content: space-evenly; 8 | } 9 | 10 | .dashboard-wrapper>div { 11 | display: flex; 12 | column-gap: 5px; 13 | border: 1px solid black; 14 | padding: 10px; 15 | width: 100%; 16 | } 17 | 18 | .dashboard-wrapper.light { 19 | background-color: rgb(255, 255, 255); 20 | } 21 | 22 | .dashboard-wrapper.dark { 23 | background-color: rgb(70, 70, 70); 24 | color: #fff; 25 | } 26 | 27 | .dashboard-wrapper.dark>div { 28 | border-color: #fff; 29 | } -------------------------------------------------------------------------------- /2_Todo App/src/components/form/form.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import './form.css'; 3 | import { ITodoItem } from '../types'; 4 | import { ThemeContext } from '../../main'; 5 | 6 | interface IProps { 7 | onSubmit: (item: ITodoItem) => void; 8 | } 9 | 10 | const Form = React.memo((props: IProps) => { 11 | const { theme } = useContext(ThemeContext); 12 | 13 | const handleSubmit = (e: React.FormEvent) => { 14 | e.preventDefault(); 15 | const title: string = e.currentTarget["task"].value; 16 | const isUrgent: boolean = e.currentTarget["urgent"].checked; 17 | if (title.length > 3) { 18 | const newTask: ITodoItem = { 19 | id: Date.now(), 20 | title, 21 | isUrgent, 22 | isDone: false 23 | } 24 | 25 | props.onSubmit(newTask); 26 | } 27 | } 28 | 29 | return ( 30 | 31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 | ) 39 | }); 40 | 41 | export default Form; -------------------------------------------------------------------------------- /2_Todo App/src/components/form/form.css: -------------------------------------------------------------------------------- 1 | .form-wrapper { 2 | margin: 20px; 3 | padding: 10px; 4 | display: flex; 5 | flex-direction: column; 6 | row-gap: 10px; 7 | } 8 | 9 | .form-wrapper .task-input { 10 | flex-basis: 100%; 11 | padding: 10px; 12 | } 13 | 14 | .form-wrapper .submit { 15 | padding: 10px; 16 | } 17 | 18 | .form-wrapper.light { 19 | border: 1px solid black; 20 | } 21 | 22 | .form-wrapper.dark { 23 | border: 1px solid white; 24 | color: white; 25 | } -------------------------------------------------------------------------------- /2_Todo App/src/components/todo-item/todo-item.component.tsx: -------------------------------------------------------------------------------- 1 | import { Trash } from '@phosphor-icons/react'; 2 | import './todo-item.css' 3 | import { ITodoItem } from '../types'; 4 | import { useContext } from 'react'; 5 | import { ThemeContext } from '../../main'; 6 | 7 | interface IProps { 8 | data: ITodoItem 9 | onToggle: (e: React.ChangeEvent) => void 10 | onDelete: () => void 11 | }; 12 | 13 | const TodoItem = ({ data, onToggle, onDelete }: IProps) => { 14 | const { theme } = useContext(ThemeContext); 15 | return ( 16 |
17 | 18 |
19 | 26 | 27 |
28 | {data.title} 29 |
30 | 31 |
32 | ) 33 | } 34 | 35 | export default TodoItem; -------------------------------------------------------------------------------- /2_Todo App/src/components/todo-item/todo-item.css: -------------------------------------------------------------------------------- 1 | .item-wrapper { 2 | border: 1px solid black; 3 | padding: 10px; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | } 8 | 9 | .item-wrapper.dark { 10 | border-color: white; 11 | color: white; 12 | } 13 | 14 | .item-wrapper.done { 15 | text-decoration: line-through; 16 | order: 999999; 17 | } 18 | 19 | .item-wrapper.urgent { 20 | border-bottom: 3px solid firebrick; 21 | } 22 | 23 | .item-wrapper>.delete { 24 | cursor: pointer; 25 | } 26 | 27 | .item-wrapper>span { 28 | display: flex; 29 | align-items: center; 30 | } 31 | 32 | .item-wrapper>.item-details { 33 | position: relative; 34 | padding-left: 10px; 35 | } 36 | 37 | .round-checkbox label { 38 | background-color: #fff; 39 | border: 1px solid #ccc; 40 | border-radius: 50%; 41 | cursor: pointer; 42 | height: 20px; 43 | width: 20px; 44 | left: 0; 45 | position: absolute; 46 | top: 0; 47 | } 48 | 49 | .round-checkbox label:after { 50 | border: 2px solid #fff; 51 | border-top: none; 52 | border-right: none; 53 | content: ""; 54 | height: 5px; 55 | width: 10px; 56 | left: 4px; 57 | top: 5px; 58 | opacity: 0; 59 | position: absolute; 60 | transform: rotate(-45deg); 61 | } 62 | 63 | .round-checkbox input[type="checkbox"] { 64 | visibility: hidden; 65 | } 66 | 67 | .round-checkbox input[type="checkbox"]:checked+label { 68 | background-color: #66bb6a; 69 | border-color: #66bb6a; 70 | } 71 | 72 | .round-checkbox input[type="checkbox"]:checked+label:after { 73 | opacity: 1; 74 | } -------------------------------------------------------------------------------- /2_Todo App/src/components/todo-list/todo-list.component.tsx: -------------------------------------------------------------------------------- 1 | import TodoItem from "../todo-item/todo-item.component"; 2 | import { ITodoItem } from "../types"; 3 | import './todo-list.css'; 4 | 5 | interface IProps { 6 | items: ITodoItem[]; 7 | onToggle: (e: React.ChangeEvent) => void; 8 | onDelete: (index: number) => void; 9 | } 10 | 11 | const TodoList = (props: IProps) => { 12 | return ( 13 |
14 | { 15 | props.items.map((item, index) => ( 16 | props.onDelete(index)} 21 | /> 22 | )) 23 | } 24 |
25 | ) 26 | } 27 | 28 | export default TodoList; -------------------------------------------------------------------------------- /2_Todo App/src/components/todo-list/todo-list.css: -------------------------------------------------------------------------------- 1 | .list-wrapper { 2 | border: 1px solid black; 3 | margin: 20px; 4 | padding: 10px; 5 | display: flex; 6 | flex-direction: column; 7 | row-gap: 10px; 8 | } -------------------------------------------------------------------------------- /2_Todo App/src/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface ITodoItem { 2 | id: number; 3 | title: string; 4 | isUrgent: boolean; 5 | isDone: boolean; 6 | } -------------------------------------------------------------------------------- /2_Todo App/src/hooks/local-storage.hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useLocalStorage = (state: any, storageKey: string) => { 4 | const [storedData, setStoredData] = useState(); 5 | 6 | useEffect(() => { 7 | // Read the data on the first render 8 | const strData = localStorage.getItem(storageKey); 9 | try { 10 | if (strData !== null) { 11 | setStoredData(JSON.parse(strData)); 12 | } else { 13 | setStoredData(null); 14 | } 15 | } catch { 16 | setStoredData(strData); 17 | } 18 | }, []); 19 | 20 | useEffect(() => { 21 | if (typeof (state) === 'object') { 22 | localStorage.setItem(storageKey, JSON.stringify(state)); 23 | } else { 24 | localStorage.setItem(storageKey, state.toString()); 25 | } 26 | }, [state, storageKey]); 27 | 28 | return { storedData }; 29 | } 30 | 31 | export default useLocalStorage; -------------------------------------------------------------------------------- /2_Todo App/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App.tsx' 3 | import { createContext, useEffect, useState } from 'react' 4 | 5 | 6 | interface IContextState { 7 | theme: string, 8 | setTheme: React.Dispatch> 9 | } 10 | 11 | const storedTheme = localStorage.getItem('theme'); 12 | const DEFAULT_THEME = storedTheme || 'light'; 13 | 14 | export const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => { } }); 15 | 16 | const WrapperComponent = () => { 17 | const [theme, setTheme] = useState(DEFAULT_THEME); 18 | 19 | useEffect(() => { 20 | localStorage.setItem('theme', theme); 21 | }, [theme]); 22 | 23 | return ( 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | createRoot(document.getElementById('root')!).render( 31 | 32 | ); -------------------------------------------------------------------------------- /2_Todo App/src/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ITodoItem } from "../components/types"; 2 | 3 | interface IState { 4 | todos: ITodoItem[]; 5 | userName: string; 6 | } 7 | 8 | // interface IAction { 9 | // type: string; 10 | // payload: any; 11 | // } 12 | 13 | type Action = { type: 'INIT_TODOS', payload: ITodoItem[] } 14 | | { type: 'ADD_TODO', payload: ITodoItem } 15 | | { type: 'REMOVE_TODO', payload: number } 16 | | { type: 'TOGGLE_TODO', payload: number } 17 | 18 | const reducer = (state: IState, action: Action): IState => { 19 | switch (action.type) { 20 | case 'INIT_TODOS': { 21 | if (state.todos.length === 0) { 22 | return { ...state, todos: action.payload } 23 | } 24 | return state; 25 | } 26 | case 'ADD_TODO': { 27 | const newTodo = action.payload; 28 | newTodo.id = Date.now(); 29 | return { ...state, todos: [...state.todos, newTodo] } 30 | } 31 | case 'REMOVE_TODO': 32 | return { ...state, todos: state.todos.filter(item => item.id !== action.payload) } 33 | case 'TOGGLE_TODO': { 34 | return { ...state, todos: state.todos.map(item => (item.id === action.payload) ? { ...item, isDone: !item.isDone } : item) } 35 | } 36 | default: { 37 | return state; 38 | } 39 | } 40 | } 41 | 42 | export default reducer; -------------------------------------------------------------------------------- /2_Todo App/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2_Todo App/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /2_Todo App/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /2_Todo App/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /2_Todo App/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /3_next-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /3_next-example/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /3_next-example/app/antd/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react' 3 | import { Button, Form, Checkbox, Input } from 'antd'; 4 | import { SyncOutlined } from '@ant-design/icons'; 5 | import { DatePicker } from 'antd'; 6 | import type { FormProps } from 'antd'; 7 | 8 | const { RangePicker } = DatePicker; 9 | 10 | const Page = () => { 11 | type FieldType = { 12 | username?: string; 13 | password?: string; 14 | remember?: string; 15 | }; 16 | 17 | const onFinish: FormProps['onFinish'] = (values) => { 18 | console.log('Success:', values); 19 | }; 20 | 21 | const onFinishFailed: FormProps['onFinishFailed'] = (errorInfo) => { 22 | console.log('Failed:', errorInfo); 23 | }; 24 | 25 | return ( 26 |
27 |
28 | 29 | 32 | { 34 | 35 | console.log(v?.[0]?.toDate()); 36 | console.log(v?.[1]?.toDate()); 37 | }} 38 | /> 39 |
40 |
41 |
51 | 52 | label="Username" 53 | name="username" 54 | rules={[{ required: true, message: 'Please input your username!' }]} 55 | > 56 | 57 | 58 | 59 | 60 | label="Password" 61 | name="password" 62 | rules={[{ required: true, message: 'Please input your password!' }]} 63 | > 64 | 65 | 66 | 67 | name="remember" valuePropName="checked" label={null}> 68 | Remember me 69 | 70 | 71 | 72 | 75 | 76 | 77 |
78 |
79 | ) 80 | } 81 | 82 | export default Page -------------------------------------------------------------------------------- /3_next-example/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/3_next-example/app/favicon.ico -------------------------------------------------------------------------------- /3_next-example/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | :root { 10 | --background: #0a0a0a; 11 | --foreground: #ededed; 12 | } 13 | } 14 | 15 | html, 16 | body { 17 | max-width: 100vw; 18 | overflow-x: hidden; 19 | } 20 | 21 | body { 22 | color: var(--foreground); 23 | background: var(--background); 24 | font-family: Arial, Helvetica, sans-serif; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | a { 30 | color: inherit; 31 | text-decoration: none; 32 | } 33 | 34 | @media (prefers-color-scheme: dark) { 35 | html { 36 | color-scheme: dark; 37 | } 38 | } -------------------------------------------------------------------------------- /3_next-example/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import MainHeader from "@/components/main-header/MainHeader"; 5 | import { AntdRegistry } from '@ant-design/nextjs-registry'; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { 23 | return ( 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /3_next-example/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | justify-items: center; 12 | min-height: 100svh; 13 | padding: 80px; 14 | gap: 20px; 15 | font-family: var(--font-geist-sans); 16 | } 17 | 18 | .page nav { 19 | display: flex; 20 | column-gap: 20px; 21 | } -------------------------------------------------------------------------------- /3_next-example/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './page.module.css'; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 |

Hello Next JS

8 |

This is is the first next js app

9 |
10 |

Latest News

11 |
    12 |
  • 13 | Gaza News 14 |
  • 15 |
  • 16 | Westbank News 17 |
  • 18 |
19 |
20 |
21 | ); 22 | } 23 | 24 | export default Home; -------------------------------------------------------------------------------- /3_next-example/app/reach/about/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const About = () => { 4 | return ( 5 |
6 |

About App

7 |

8 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam assumenda fugit, ut alias sunt reiciendis facere voluptates laboriosam possimus pariatur, delectus commodi quibusdam corrupti! Quam porro iste dolore temporibus voluptatum. 9 | Magni ipsum veniam temporibus sit modi necessitatibus atque, dolore commodi veritatis cupiditate doloremque accusantium. Praesentium commodi tempore et animi, temporibus eligendi natus sequi, ipsa veniam, maxime voluptas aperiam asperiores quibusdam? 10 | Porro magni amet maxime recusandae explicabo sed. Ducimus quos cupiditate, animi et, alias accusamus inventore minima soluta molestias cumque repellat nobis nam. Debitis nisi aliquam nulla sit? Quo, repudiandae atque? 11 |

12 |
13 | ) 14 | } 15 | 16 | export default About -------------------------------------------------------------------------------- /3_next-example/app/reach/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from '@/components/Form'; 3 | 4 | const Contact = () => { 5 | return ( 6 |
7 |

Contact Us

8 |
9 |
10 | ) 11 | } 12 | 13 | export default Contact; -------------------------------------------------------------------------------- /3_next-example/app/reach/layout.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: flex-start; 5 | } 6 | 7 | .wrapper>aside { 8 | flex-basis: 20vw; 9 | background-color: #ccc; 10 | min-height: 70vh; 11 | padding-top: 30px; 12 | padding-left: 10px; 13 | } 14 | 15 | .wrapper>main { 16 | flex-basis: 80vw; 17 | min-height: 70vh; 18 | border: 1px solid gray; 19 | } -------------------------------------------------------------------------------- /3_next-example/app/reach/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import classes from './layout.module.css'; 4 | 5 | interface IProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | const ReachUSLayout = (props: IProps) => { 10 | return ( 11 |
12 | 22 |
23 | {props.children} 24 |
25 |
26 | ) 27 | } 28 | 29 | export default ReachUSLayout; -------------------------------------------------------------------------------- /3_next-example/app/reach/portfolio/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const page = () => { 4 | return ( 5 |
6 |
7 |
8 |

John Doe

9 |

Frontend Developer

10 |
11 | 12 |
13 |

14 | Passionate about building beautiful and functional user interfaces. Loves Tailwind CSS and modern web technologies. 15 |

16 |
17 |

Skills

18 |
19 | JavaScript 20 | React 21 | Tailwind CSS 22 | Node.js 23 |
24 |
25 |
26 | 29 |
30 |
31 |
32 |

Follow me on Twitter

33 |
34 |
35 |
36 | ) 37 | } 38 | 39 | export default page; -------------------------------------------------------------------------------- /3_next-example/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Form = () => { 4 | return ( 5 | 6 | Please enter all data 7 |
8 |
9 |
10 | 11 | 12 | ) 13 | } 14 | 15 | export default Form; -------------------------------------------------------------------------------- /3_next-example/components/main-header/MainHeader.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import classes from './main-header.module.css'; 4 | 5 | const MainHeader = () => { 6 | return ( 7 |
8 |

First Next Example

9 | 19 |
20 | ) 21 | } 22 | 23 | export default MainHeader -------------------------------------------------------------------------------- /3_next-example/components/main-header/main-header.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | justify-content: space-between; 4 | background-color: aquamarine; 5 | padding: 20px 30px; 6 | } 7 | 8 | .main-nav>ul { 9 | display: flex; 10 | column-gap: 10px; 11 | list-style-type: none; 12 | padding: 0; 13 | margin: 0; 14 | } 15 | 16 | .main-nav>ul>li { 17 | padding: 0; 18 | margin: 10px 0; 19 | padding-right: 10px; 20 | border-right: 1px solid #aaa; 21 | } -------------------------------------------------------------------------------- /3_next-example/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /3_next-example/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /3_next-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^5.6.1", 13 | "@ant-design/nextjs-registry": "^1.0.2", 14 | "@tailwindcss/postcss": "^4.0.3", 15 | "antd": "^5.23.4", 16 | "next": "15.1.6", 17 | "postcss": "^8.5.1", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "tailwindcss": "^4.0.3" 21 | }, 22 | "devDependencies": { 23 | "@eslint/eslintrc": "^3", 24 | "@types/node": "^20", 25 | "@types/react": "^19", 26 | "@types/react-dom": "^19", 27 | "eslint": "^9", 28 | "eslint-config-next": "15.1.6", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /3_next-example/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {} 4 | }, 5 | }; 6 | export default config; 7 | -------------------------------------------------------------------------------- /3_next-example/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3_next-example/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3_next-example/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3_next-example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3_next-example/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3_next-example/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | export default { 4 | content: [ 5 | './app/**/*.{js,ts,jsx,tsx,mdx}' 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } satisfies Config -------------------------------------------------------------------------------- /3_next-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /4_news_app/.env: -------------------------------------------------------------------------------- 1 | # YOU MUST NEVER COMMIT THIS FILE TO GIT 2 | JWT_SECRET = 'as;falklk;fasdoiuew$%#TF#$SDafdafsd@#4324231FGA43' -------------------------------------------------------------------------------- /4_news_app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | # .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /4_news_app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /4_news_app/app/(auth)/layout.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | padding-top: 50px; 3 | } -------------------------------------------------------------------------------- /4_news_app/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './layout.module.css'; 3 | 4 | export default function AuthLayout({ children }: Readonly<{ children: React.ReactNode; }>) { 5 | return ( 6 |
7 | {children} 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /4_news_app/app/(auth)/user/login/login.module.css: -------------------------------------------------------------------------------- 1 | .loginContainer { 2 | max-width: 450px; 3 | margin: 2rem auto; 4 | padding: 2.5rem; 5 | background-color: white; 6 | border-radius: 8px; 7 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 8 | } 9 | 10 | .loginTitle { 11 | color: #71b2ab; 12 | margin-bottom: 0.5rem; 13 | font-size: 2rem; 14 | font-weight: 600; 15 | border-bottom: 2px solid #a3d0ca; 16 | padding-bottom: 0.5rem; 17 | } 18 | 19 | .loginDescription { 20 | margin-bottom: 1.5rem; 21 | color: #666; 22 | font-size: 1rem; 23 | } -------------------------------------------------------------------------------- /4_news_app/app/(auth)/user/login/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from "./login.module.css"; 3 | import LoginForm from '@/components/auth/login-form/LoginForm'; 4 | 5 | interface IProps { 6 | searchParams: Promise<{ msg: string }>; 7 | } 8 | 9 | const Page = async (props: IProps) => { 10 | const { msg } = (await props.searchParams); 11 | 12 | return ( 13 |
14 |

{msg}

15 |

Welcome Back

16 |

Please enter your credentials to login

17 | 18 |
19 | ) 20 | } 21 | 22 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(auth)/user/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from "./signup.module.css"; 3 | 4 | const Page = () => { 5 | return ( 6 |
7 |

Signup to new account in GSG News APP

8 |
9 | ) 10 | } 11 | 12 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(auth)/user/signup/signup.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/app/(auth)/user/signup/signup.module.css -------------------------------------------------------------------------------- /4_news_app/app/(main)/@latestgb/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LatestNews from '@/components/latest-news/LatestNews'; 3 | 4 | import { fetchNews } from '@/services/news.service'; 5 | 6 | const Page = async () => { 7 | const latestNews: News.Item[] = await fetchNews('politics', 'gb') as News.Item[]; 8 | 9 | if (latestNews.length === 0) return null; 10 | 11 | return ( 12 |
13 | 17 |
18 | ) 19 | } 20 | 21 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(main)/@latestus/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LatestNews from '@/components/latest-news/LatestNews'; 3 | 4 | import { fetchNews } from '@/services/news.service'; 5 | 6 | const Page = async () => { 7 | const latestNews: News.Item[] = await fetchNews('politics', 'us') as News.Item[]; 8 | 9 | return ( 10 |
11 | 15 |
16 | ) 17 | } 18 | 19 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(main)/add-news/add-article.module.css: -------------------------------------------------------------------------------- 1 | .newsFormContainer { 2 | max-width: 800px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | background-color: white; 6 | border-radius: 8px; 7 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 8 | } 9 | 10 | .formTitle { 11 | color: #71b2ab; 12 | margin-bottom: 0.5rem; 13 | font-size: 2rem; 14 | font-weight: 600; 15 | border-bottom: 2px solid #a3d0ca; 16 | padding-bottom: 0.5rem; 17 | } 18 | 19 | .formDescription { 20 | margin-bottom: 1.5rem; 21 | color: #666; 22 | font-size: 1rem; 23 | } -------------------------------------------------------------------------------- /4_news_app/app/(main)/add-news/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './add-article.module.css'; 3 | import AddArticleForm from '@/components/add-article/AddArticle'; 4 | import { Metadata } from 'next'; 5 | 6 | export const metadata: Metadata = { 7 | title: 'Add new Article!', 8 | description: 'GSG News website, Add new Article!' 9 | } 10 | 11 | const Page = () => { 12 | return ( 13 |
14 |

Add News Page

15 |

Please fill all the required news data

16 | 17 |
18 | ) 19 | } 20 | 21 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(main)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Page = () => { 4 | return ( 5 |
6 |

Admin Page

7 |

This is very important admin stuff, you need to have an admin role to see it.

8 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quam, voluptatem cumque exercitationem, dolores, fugiat aspernatur rem hic incidunt maxime sit officia corporis eveniet quasi eligendi dicta natus aperiam. Velit, quaerat.

9 |
10 | ) 11 | } 12 | 13 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(main)/categories/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Categories from '@/components/categories/Categories'; 3 | import { Metadata } from 'next'; 4 | // import Link from 'next/link'; 5 | import React from 'react'; 6 | 7 | export const metadata: Metadata = { 8 | title: 'Categories!', 9 | description: 'GSG News website, News Categories!' 10 | } 11 | 12 | const Page = () => { 13 | return ( 14 |
15 |

Categories Page

16 | 17 |
18 | ) 19 | } 20 | 21 | export default Page; -------------------------------------------------------------------------------- /4_news_app/app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from "@/components/header/Header"; 3 | 4 | export default function MainLayout({ children }: Readonly<{ children: React.ReactNode; }>) { 5 | return ( 6 |
7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/[[...slug]]/loading-deleted.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './news-list.module.css'; 3 | 4 | const NewLoadingPage = () => { 5 | return ( 6 |
7 |
8 |
9 | ) 10 | } 11 | 12 | export default NewLoadingPage; -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/[[...slug]]/news-list.module.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | border: 8px solid #f3f3f3; 3 | border-top: 8px solid #3498db; 4 | border-radius: 50%; 5 | width: 50px; 6 | height: 50px; 7 | animation: spin 2s linear infinite; 8 | } 9 | 10 | @keyframes spin { 11 | 0% { 12 | transform: rotate(0deg); 13 | } 14 | 15 | 100% { 16 | transform: rotate(360deg); 17 | } 18 | } 19 | 20 | h1.header { 21 | text-transform: capitalize; 22 | font-family: var(--font-roboto); 23 | font-size: 24px; 24 | line-height: 36px; 25 | font-weight: 700; 26 | color: #000; 27 | } 28 | 29 | .newsLoading { 30 | overflow: hidden; 31 | } -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/[[...slug]]/news-list.tsx: -------------------------------------------------------------------------------- 1 | import { getNewsByCategory } from '@/services/news.service'; 2 | import ArticleItem from '@/components/article-item/ArticleItem'; 3 | 4 | const NewsList = async ({ category }: { category: string }) => { 5 | const latestNews: News.Item_[] = getNewsByCategory(category); 6 | 7 | return ( 8 |
9 | { 10 | latestNews.map(item => ) 11 | } 12 |
13 | ) 14 | }; 15 | 16 | export default NewsList; -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/[[...slug]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import img404 from '@/public/404.svg'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | 6 | const NotFound = () => { 7 | return ( 8 |
9 |

The country or category you are looking for doesn't exist!

10 | 404 11 | Go to categories page 12 |
13 | ) 14 | } 15 | 16 | export default NotFound; -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import classes from './news-list.module.css'; 3 | import NewsList from './news-list'; 4 | import { Metadata } from 'next'; 5 | 6 | interface IProps { 7 | params: Promise<{ slug: string[] }> 8 | } 9 | 10 | export const generateMetadata = async (props: IProps): Promise => { 11 | const { slug } = await props.params; 12 | const [category] = slug; 13 | 14 | return { 15 | title: `${category.toUpperCase()} News` 16 | } 17 | } 18 | 19 | const NewsListPage = async (props: IProps) => { 20 | const { slug } = await props.params; 21 | 22 | if (!slug?.length) { 23 | return ( 24 |
25 |

You reached this page without selecting a country or category!

26 |
27 | ); 28 | } 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | const [category, _year] = slug; 32 | 33 | return ( 34 |
35 |

{category} News

36 | }> 37 | 38 | 39 |
40 | ) 41 | } 42 | 43 | export default NewsListPage; 44 | -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/layout.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | display: flex; 3 | align-items: stretch; 4 | column-gap: 30px; 5 | } 6 | 7 | .sideBar { 8 | min-height: 100vh; 9 | flex-basis: 25%; 10 | flex-shrink: 0; 11 | box-shadow: 0px 0px 1px #171a1f1F, 0px 0px 2px #171a1f1F; 12 | } -------------------------------------------------------------------------------- /4_news_app/app/(main)/news-list/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './layout.module.css'; 3 | 4 | export default function NewsLayout({ 5 | children, 6 | }: Readonly<{ 7 | children: React.ReactNode; 8 | }>) { 9 | return ( 10 |
11 |
12 | side bar 13 |
14 | {children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /4_news_app/app/(main)/news/[slug]/article.module.css: -------------------------------------------------------------------------------- 1 | .articleContainer { 2 | max-width: 800px; 3 | margin: 2rem auto; 4 | padding: 2rem; 5 | background-color: white; 6 | border-radius: 8px; 7 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 8 | } 9 | 10 | .articleTitle { 11 | color: #71b2ab; 12 | font-size: 2.5rem; 13 | font-weight: 700; 14 | margin-bottom: 1rem; 15 | line-height: 1.2; 16 | border-bottom: 2px solid #a3d0ca; 17 | padding-bottom: 0.5rem; 18 | } 19 | 20 | .articleAddress { 21 | font-style: normal; 22 | color: #666; 23 | margin-bottom: 1.5rem; 24 | font-size: 0.9rem; 25 | } 26 | 27 | .articleAddress cite { 28 | font-style: normal; 29 | color: #71b2ab; 30 | font-weight: 500; 31 | } 32 | 33 | .articleImage { 34 | width: 100%; 35 | height: auto; 36 | border-radius: 8px; 37 | margin-bottom: 1.5rem; 38 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 39 | } 40 | 41 | .articleMain { 42 | font-size: 1.1rem; 43 | line-height: 1.6; 44 | color: #333; 45 | } 46 | 47 | .articleParagraph { 48 | margin-bottom: 1.5rem; 49 | } 50 | 51 | .articleParagraph:last-child { 52 | margin-bottom: 0; 53 | } 54 | 55 | /* Responsive adjustments */ 56 | @media (max-width: 768px) { 57 | .articleContainer { 58 | padding: 1.5rem; 59 | margin: 1rem; 60 | } 61 | 62 | .articleTitle { 63 | font-size: 2rem; 64 | } 65 | 66 | .articleMain { 67 | font-size: 1rem; 68 | } 69 | } 70 | 71 | /* Optional: Add some subtle animations */ 72 | .articleContainer { 73 | animation: fadeIn 0.5s ease-in-out; 74 | } 75 | 76 | @keyframes fadeIn { 77 | from { 78 | opacity: 0; 79 | transform: translateY(20px); 80 | } 81 | to { 82 | opacity: 1; 83 | transform: translateY(0); 84 | } 85 | } 86 | 87 | .articleImage { 88 | transition: transform 0.3s ease; 89 | } 90 | 91 | .articleImage:hover { 92 | transform: scale(1.02); 93 | } 94 | 95 | -------------------------------------------------------------------------------- /4_news_app/app/(main)/news/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getNewsArticle } from '@/services/news.service'; 2 | import Image from 'next/image'; 3 | import React from 'react'; 4 | import classes from './article.module.css'; 5 | import { Metadata } from 'next'; 6 | 7 | interface IProps { 8 | params: Promise<{ slug: string }> 9 | } 10 | 11 | export const generateMetadata = async (props: IProps): Promise => { 12 | const { slug } = await props.params; 13 | const article = getNewsArticle(slug); 14 | 15 | return { 16 | title: `${article.title}`, 17 | authors: { name: article.author } 18 | } 19 | } 20 | 21 | const NewArticle = async (props: IProps) => { 22 | const slug = (await props.params).slug; 23 | const article = getNewsArticle(slug); 24 | 25 | return ( 26 |
27 |

{article.title}

28 |
29 | Author: {article.author} | {new Date(article.date).toLocaleDateString()} 30 |
31 | article image 39 |
40 |

{article.content}

41 |
42 |
43 | 44 | ) 45 | } 46 | 47 | export default NewArticle -------------------------------------------------------------------------------- /4_news_app/app/(main)/page.tsx: -------------------------------------------------------------------------------- 1 | import Hero from "@/components/hero/Hero"; 2 | import Categories from "@/components/categories/Categories"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /4_news_app/app/api/auth/login/route.ts: -------------------------------------------------------------------------------- 1 | import { findUserByEmail } from "@/services/auth"; 2 | import { comparePassword, generateToken } from "@/utils/auth"; 3 | import { cookies } from "next/headers"; 4 | import { NextRequest, NextResponse } from "next/server"; 5 | 6 | const POST = async (request: NextRequest) => { 7 | const { email, password } = await request.json() as { email: string, password: string }; 8 | 9 | if (!email || !password) { 10 | return new NextResponse('Email and Password are required', { status: 400 }) 11 | } 12 | 13 | const user = findUserByEmail(email); 14 | if (!user) { 15 | return new NextResponse('Invalid Credentials!', { status: 401 }); 16 | } 17 | 18 | const isValidPassword = comparePassword(password, user.password || ''); 19 | 20 | if (!isValidPassword) { 21 | return new NextResponse('Invalid Credentials!', { status: 401 }); 22 | } 23 | 24 | delete user.password; 25 | const token = await generateToken(user); 26 | 27 | (await cookies()).set('auth-token', token, { 28 | httpOnly: true, 29 | secure: process.env.NODE_ENV === 'production', 30 | maxAge: 3600 // 1 hour 31 | }); 32 | 33 | return new NextResponse(token, { status: 200 }); 34 | } 35 | 36 | export { POST }; 37 | 38 | // If you want to make logout api 39 | /* 40 | 41 | (await cookies()).set('auth-token', '', { 42 | httpOnly: true, 43 | secure: process.env.NODE_ENV === 'production', 44 | maxAge: 0 // Expire Immediately 45 | }); 46 | 47 | 48 | */ -------------------------------------------------------------------------------- /4_news_app/app/api/news/[slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { getNewsArticle } from "@/services/news.service"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | const GET = async (request: NextRequest, { params }: { params: { slug: string } }) => { 5 | const slug = (await params).slug; 6 | 7 | const article = getNewsArticle(slug); 8 | if (!article) { 9 | return new NextResponse('Article Not found', { status: 404 }); 10 | } 11 | 12 | return NextResponse.json( 13 | article, 14 | { status: 200 } 15 | ); 16 | } 17 | 18 | export { GET }; -------------------------------------------------------------------------------- /4_news_app/app/api/news/route.ts: -------------------------------------------------------------------------------- 1 | import { ALLOWED_CATEGORIES } from "@/constants/data"; 2 | import { getNewsByCategory } from "@/services/news.service"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | const GET = async (request: NextRequest) => { 6 | const params = request.nextUrl.searchParams; 7 | const category = params.get('category') || 'global'; 8 | 9 | if (!ALLOWED_CATEGORIES.includes(category)) { 10 | return NextResponse.json(null, { status: 400, statusText: "Unknown category" }); 11 | } 12 | 13 | const news = getNewsByCategory(category); 14 | return NextResponse.json( 15 | { results: news }, 16 | { status: 200 } 17 | ); 18 | } 19 | 20 | const POST = async (request: NextRequest) => { 21 | const body = await request.json(); 22 | console.log(body); 23 | return NextResponse.json({ 24 | msg: "Item Added" 25 | }, { 26 | status: 201, 27 | // headers: { 28 | // 'Access-Control-Allow-Origin': '*', 29 | // 'Access-Control-Allow-Methods': 'GET', 30 | // 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 31 | // }, 32 | }); 33 | } 34 | 35 | export { 36 | GET, POST 37 | } -------------------------------------------------------------------------------- /4_news_app/app/api/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET() { 2 | console.log("Hello from the '/' route"); 3 | 4 | // return new Response("Hello from the '/' route", { status: 200 }); 5 | return Response.json({ message: "Hello from the '/' route" }, { 6 | status: 404 7 | }); 8 | } 9 | 10 | 11 | 12 | 13 | // api/ 14 | 15 | // Better convention 16 | // [GET] /api/news 17 | // [GET] /api/news/city-marathon-2025 18 | // [POST] /api/news 19 | // [PUT] /api/news 20 | // [DELETE] /api/news 21 | 22 | // Bad convention 23 | // api/get-news 24 | // api/add-news 25 | // api/update-news 26 | // api/delete-news 27 | 28 | // api/users 29 | -------------------------------------------------------------------------------- /4_news_app/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | interface IProps { 4 | error: Error; 5 | reset: () => void; 6 | } 7 | 8 | const Error = (props: IProps) => { 9 | 10 | return ( 11 |
12 |

Opps!!!

13 |

Something went wrong while processing your request!

14 |

You can or later. 15 |

16 |

Contact the system administrator and provide the following info: {props.error.message}

17 |
18 | ) 19 | } 20 | 21 | export default Error -------------------------------------------------------------------------------- /4_news_app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/app/favicon.ico -------------------------------------------------------------------------------- /4_news_app/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: var(--font-roboto); 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | @media (prefers-color-scheme: dark) { 28 | html { 29 | color-scheme: dark; 30 | } 31 | } 32 | 33 | /* Elements */ 34 | .button { 35 | display: inline-block; 36 | padding: 10px 60px; 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | font-family: var(--font-mulish); 41 | font-size: 16px; 42 | line-height: 28px; 43 | font-weight: 400; 44 | color: #FFF; 45 | background: #71B2ABFF; 46 | opacity: 1; 47 | border: none; 48 | border-radius: 2px; 49 | cursor: pointer; 50 | box-sizing: border-box; 51 | } 52 | 53 | .button.outline { 54 | color: #71B2ABFF; 55 | background: #FFF; 56 | opacity: 1; 57 | border-radius: 2px; 58 | border: 2px #71B2ABFF solid; 59 | box-sizing: border-box; 60 | } 61 | 62 | .button:hover { 63 | color: #FFF; 64 | background: #50958EFF; 65 | } 66 | 67 | .button:active { 68 | color: #FFF; 69 | background: #45807BFF; 70 | } 71 | 72 | .button:disabled { 73 | opacity: 0.4; 74 | } -------------------------------------------------------------------------------- /4_news_app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { Roboto, Mulish } from 'next/font/google'; 4 | import classNames from "classnames"; 5 | import { ToastContainer } from "react-toastify"; 6 | 7 | const robotoFont = Roboto({ 8 | weight: ['400', '700'], 9 | subsets: ['latin'], 10 | style: ['italic', 'normal'], 11 | fallback: ['Arial', 'Helvetica', 'sans-serif'], 12 | display: 'swap', 13 | variable: '--font-roboto' 14 | }); 15 | 16 | const mulishFont = Mulish({ 17 | weight: ['400', '700'], 18 | subsets: ['latin'], 19 | style: ['italic', 'normal'], 20 | fallback: ['Arial', 'Helvetica', 'sans-serif'], 21 | display: 'swap', 22 | variable: '--font-mulish' 23 | }); 24 | 25 | export const metadata: Metadata = { 26 | title: { 27 | template: '%s | GSG News!', 28 | default: '' 29 | }, 30 | description: 'GSG News, get latest news around the world' 31 | }; 32 | 33 | interface IProps { 34 | children: React.ReactNode; 35 | } 36 | 37 | export default function RootLayout({ children }: IProps) { 38 | return ( 39 | // We used the .variable as class name (not .className) to pass the css variable :) 40 | 41 | 42 | 43 | {children} 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /4_news_app/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GlobalLoadingPage = () => { 4 | return ( 5 |
6 |

Loading ...

7 |
8 | ) 9 | } 10 | 11 | export default GlobalLoadingPage; -------------------------------------------------------------------------------- /4_news_app/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import img404 from '@/public/404.svg'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | 6 | const NotFound = () => { 7 | return ( 8 |
9 |

The page you are looking for is not found!

10 | 404 11 | Go to Home Page 12 |
13 | ) 14 | } 15 | 16 | export default NotFound; -------------------------------------------------------------------------------- /4_news_app/components/add-article/AddArticle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useActionState } from 'react'; 4 | import classes from './add-article.module.css'; 5 | import { addArticle } from '@/controllers/news-actions'; 6 | import SubmitArticle from './SubmitArticle'; 7 | 8 | const AddArticleForm = () => { 9 | 10 | const [state, formAction] = useActionState(addArticle, { errors: [] }); 11 | 12 | return ( 13 |
14 |
15 | 18 | 19 |
20 |
21 | 24 | 25 |
26 |
27 | 30 | 31 |
32 |
33 | 36 |