├── 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 | Reset
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 |
setIsOpen(!isOpen)}>
58 | {isOpen ? ∧ Close : ∨ Open }
59 | Add Form
60 |
61 |
62 | Student Name:
63 | handleChange('name', e.target.value)}
68 | />
69 |
70 |
71 | Student Age:
72 | handleChange('age', e.target.value)}
79 | />
80 |
81 |
82 | Is Graduated:
83 | handleChange('isGraduated', e.target.checked)}
88 | />
89 |
90 |
91 |
92 |
96 | Submit
97 |
98 | Clear
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 |
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 |
18 |
19 | Home Page
20 | {
21 | user?.role === Role.ADMIN && (
22 | Add Student
23 | )
24 | }
25 | About App
26 |
27 |
28 | {
29 | user?.userName
30 | ? `Hello ${user.userName}`
31 | : Login
32 | }
33 | {
34 | user?.userName && Logout
35 | }
36 |
37 |
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 |
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 | Stop
63 | Toggle Theme
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 |
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 | Primary Button
29 | }}>
30 | Loading Icon
31 |
32 | {
34 |
35 | console.log(v?.[0]?.toDate());
36 | console.log(v?.[1]?.toDate());
37 | }}
38 | />
39 |
40 |
41 |
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 |
73 | Submit
74 |
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 |
13 |
14 |
15 | About US
16 |
17 |
18 | Contact US
19 |
20 |
21 |
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 |
27 | Contact Me
28 |
29 |
30 |
31 |
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 |
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 |
10 |
11 |
12 | Home
13 |
14 |
15 | Reach US
16 |
17 |
18 |
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 |
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 |
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 |
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 props.reset()}>try again or window.location.reload()}>refresh the page 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 |
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 |
69 | )
70 | }
71 |
72 | export default AddArticleForm
--------------------------------------------------------------------------------
/4_news_app/components/add-article/SubmitArticle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import classes from './add-article.module.css';
5 | import { useFormStatus } from 'react-dom';
6 |
7 | const SubmitArticle = () => {
8 | const { pending } = useFormStatus();
9 | return (
10 |
15 | {pending ? 'Submitting...' : 'Submit News'}
16 |
17 | )
18 | }
19 |
20 | export default SubmitArticle;
--------------------------------------------------------------------------------
/4_news_app/components/add-article/add-article.module.css:
--------------------------------------------------------------------------------
1 | .newsForm {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 1.5rem;
5 | }
6 |
7 | .formGroup {
8 | display: flex;
9 | flex-direction: column;
10 | gap: 0.5rem;
11 | }
12 |
13 | .formLabel {
14 | font-weight: 500;
15 | color: #333333;
16 | font-size: 0.95rem;
17 | }
18 |
19 | .formInput,
20 | .formTextarea,
21 | .formSelect {
22 | padding: 0.75rem;
23 | border: 1px solid #dddddd;
24 | border-radius: 4px;
25 | font-size: 1rem;
26 | transition: all 0.3s ease;
27 | }
28 |
29 | .formInput:focus,
30 | .formTextarea:focus,
31 | .formSelect:focus {
32 | outline: none;
33 | border-color: #71b2ab;
34 | box-shadow: 0 0 0 2px rgba(113, 178, 171, 0.2);
35 | }
36 |
37 | .formTextarea {
38 | resize: vertical;
39 | min-height: 120px;
40 | }
41 |
42 | .formSelect {
43 | background-color: white;
44 | cursor: pointer;
45 | }
46 |
47 | .formSelect option {
48 | padding: 0.5rem;
49 | }
50 |
51 | .formHidden {
52 | display: none;
53 | }
54 |
55 | .submitButton {
56 | background-color: #71b2ab;
57 | color: white;
58 | border: none;
59 | padding: 0.75rem 1.5rem;
60 | font-size: 1rem;
61 | font-weight: 500;
62 | border-radius: 4px;
63 | cursor: pointer;
64 | transition: background-color 0.3s ease;
65 | align-self: flex-start;
66 | margin-top: 0.5rem;
67 | }
68 |
69 | .submitButton:hover {
70 | background-color: #5a8f89;
71 | }
72 |
73 | .submitButton:disabled {
74 | background-color: #9b9c9c;
75 | cursor: not-allowed;
76 | }
77 |
78 | .submitButton:focus {
79 | outline: none;
80 | box-shadow: 0 0 0 3px rgba(113, 178, 171, 0.3);
81 | }
82 |
83 | /* Responsive adjustments */
84 | @media (max-width: 768px) {
85 | .newsFormContainer {
86 | padding: 1.5rem;
87 | margin: 1rem;
88 | }
89 |
90 | .formTitle {
91 | font-size: 1.75rem;
92 | }
93 | }
94 |
95 | /* Optional: Add some animation for form elements */
96 | .formInput,
97 | .formTextarea,
98 | .formSelect {
99 | transform: translateY(0);
100 | transition: transform 0.2s ease, border-color 0.3s ease, box-shadow 0.3s ease;
101 | }
102 |
103 | .formInput:focus,
104 | .formTextarea:focus,
105 | .formSelect:focus {
106 | transform: translateY(-2px);
107 | }
108 |
109 |
110 | .errors {
111 | font-size: 14px;
112 | color: firebrick;
113 | list-style-type: disc;
114 | padding-left: 20px;
115 | }
--------------------------------------------------------------------------------
/4_news_app/components/article-item/ArticleItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import Image from 'next/image';
4 |
5 | interface IProps {
6 | item: News.Item_;
7 | }
8 |
9 | const ArticleItem = (props: IProps) => {
10 | const { item } = props;
11 |
12 | return (
13 |
17 | {item.title?.substring(0, 95)}
18 |
19 | {
20 |
26 | }
27 |
28 | {item.summary}
29 |
30 | )
31 | }
32 |
33 | export default ArticleItem;
--------------------------------------------------------------------------------
/4_news_app/components/article-item/article-item.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/components/article-item/article-item.module.css
--------------------------------------------------------------------------------
/4_news_app/components/auth/login-form/login.module.css:
--------------------------------------------------------------------------------
1 | .loginForm {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 1.5rem;
5 | }
6 |
7 | .formGroup {
8 | display: flex;
9 | flex-direction: column;
10 | gap: 0.5rem;
11 | }
12 |
13 | .formLabel {
14 | font-weight: 500;
15 | color: #333333;
16 | font-size: 0.95rem;
17 | }
18 |
19 | .inputWrapper {
20 | position: relative;
21 | display: flex;
22 | align-items: center;
23 | }
24 |
25 | .inputIcon {
26 | position: absolute;
27 | left: 12px;
28 | color: #71b2ab;
29 | }
30 |
31 | .formInput {
32 | padding: 0.75rem;
33 | padding-left: 2.5rem;
34 | border: 1px solid #dddddd;
35 | border-radius: 4px;
36 | font-size: 1rem;
37 | transition: all 0.3s ease;
38 | width: 100%;
39 | }
40 |
41 | .formInput:focus {
42 | outline: none;
43 | border-color: #71b2ab;
44 | box-shadow: 0 0 0 2px rgba(113, 178, 171, 0.2);
45 | transform: translateY(-2px);
46 | }
47 |
48 | .labelWithLink {
49 | display: flex;
50 | justify-content: space-between;
51 | align-items: center;
52 | }
53 |
54 | .forgotPassword {
55 | font-size: 0.85rem;
56 | color: #71b2ab;
57 | text-decoration: none;
58 | transition: color 0.2s ease;
59 | }
60 |
61 | .forgotPassword:hover {
62 | color: #5a8f89;
63 | text-decoration: underline;
64 | }
65 |
66 | .rememberMe {
67 | display: flex;
68 | align-items: center;
69 | gap: 0.5rem;
70 | margin-top: -0.5rem;
71 | }
72 |
73 | .checkbox {
74 | appearance: none;
75 | width: 18px;
76 | height: 18px;
77 | border: 1px solid #dddddd;
78 | border-radius: 3px;
79 | cursor: pointer;
80 | position: relative;
81 | transition: all 0.2s ease;
82 | }
83 |
84 | .checkbox:checked {
85 | background-color: #71b2ab;
86 | border-color: #71b2ab;
87 | }
88 |
89 | .checkbox:checked::after {
90 | content: "✓";
91 | position: absolute;
92 | color: white;
93 | font-size: 12px;
94 | top: 50%;
95 | left: 50%;
96 | transform: translate(-50%, -50%);
97 | }
98 |
99 | .checkbox:focus {
100 | outline: none;
101 | box-shadow: 0 0 0 2px rgba(113, 178, 171, 0.2);
102 | }
103 |
104 | .checkboxLabel {
105 | font-size: 0.9rem;
106 | color: #666;
107 | cursor: pointer;
108 | }
109 |
110 | .loginButton {
111 | background-color: #71b2ab;
112 | color: white;
113 | border: none;
114 | padding: 0.75rem 1.5rem;
115 | font-size: 1rem;
116 | font-weight: 500;
117 | border-radius: 4px;
118 | cursor: pointer;
119 | transition: background-color 0.3s ease;
120 | width: 100%;
121 | margin-top: 0.5rem;
122 | }
123 |
124 | .loginButton:hover {
125 | background-color: #5a8f89;
126 | }
127 |
128 | .loginButton:focus {
129 | outline: none;
130 | box-shadow: 0 0 0 3px rgba(113, 178, 171, 0.3);
131 | }
132 |
133 | .signupPrompt {
134 | text-align: center;
135 | font-size: 0.95rem;
136 | color: #666;
137 | }
138 |
139 | .signupLink {
140 | color: #71b2ab;
141 | text-decoration: none;
142 | font-weight: 500;
143 | margin-left: 0.5rem;
144 | transition: color 0.2s ease;
145 | }
146 |
147 | .signupLink:hover {
148 | color: #5a8f89;
149 | text-decoration: underline;
150 | }
151 |
152 | /* Responsive adjustments */
153 | @media (max-width: 768px) {
154 | .loginContainer {
155 | padding: 1.5rem;
156 | margin: 1rem;
157 | }
158 |
159 | .loginTitle {
160 | font-size: 1.75rem;
161 | }
162 | }
163 |
164 | /* Animation for the form */
165 | .loginContainer {
166 | animation: fadeIn 0.5s ease-in-out;
167 | }
168 |
169 | @keyframes fadeIn {
170 | from {
171 | opacity: 0;
172 | transform: translateY(20px);
173 | }
174 | to {
175 | opacity: 1;
176 | transform: translateY(0);
177 | }
178 | }
179 |
180 |
--------------------------------------------------------------------------------
/4_news_app/components/categories/Categories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classes from './categories.module.css';
3 | import Category from '../category/Category';
4 | import { CATEGORIES } from '@/constants/data';
5 |
6 | const Categories = () => {
7 | return (
8 |
9 | {
10 | CATEGORIES.map(cat => )
11 | }
12 |
13 | )
14 | }
15 |
16 | export default Categories
--------------------------------------------------------------------------------
/4_news_app/components/categories/categories.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | justify-content: space-between;
4 | flex-wrap: wrap;
5 | }
--------------------------------------------------------------------------------
/4_news_app/components/category/Category.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classes from './category.module.css';
3 | import Image from 'next/image';
4 | import Link from 'next/link';
5 |
6 | interface IProps {
7 | data: News.ICategory;
8 | }
9 |
10 | const Category = (props: IProps) => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
{props.data.title}
18 |
{props.data.subtitle}
19 |
20 |
21 | )
22 | }
23 |
24 | export default Category;
--------------------------------------------------------------------------------
/4_news_app/components/category/category.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | width: 330px;
4 | margin: 5px;
5 | background: #FFF;
6 | border-radius: 2px;
7 | box-shadow: 0 0 5px 0px #171a1f7d, 0 0 2px #171a1f38;
8 | display: flex;
9 | flex-direction: column;
10 | row-gap: 10px;
11 | padding-bottom: 10px;
12 | }
13 |
14 | .wrapper a {
15 | text-decoration: none;
16 | }
17 |
18 | .title {
19 | font-family: var(--font-mulish);
20 | font-size: 18px;
21 | font-weight: 700;
22 | color: #9095A0FF;
23 | margin: 0;
24 | padding: 0 10px;
25 | }
26 |
27 | .latest {
28 | font-family: var(--font-mulish);
29 | font-size: 16px;
30 | font-weight: 400;
31 | color: #9095A0FF;
32 | margin: 0;
33 | padding: 0 10px;
34 | }
35 |
36 | .banner {
37 | position: relative;
38 | width: 100%;
39 | height: 160px;
40 | }
41 |
42 | .banner img {
43 | object-fit: cover;
44 | }
--------------------------------------------------------------------------------
/4_news_app/components/header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classes from './header.module.css';
3 | import logo from '@/public/logo.png';
4 | import NavLink from './NavLink';
5 |
6 | const Header = () => {
7 | return (
8 |
9 |
10 |
11 |
Quick News
12 |
13 |
14 | Home
15 | Add News
16 | Categories
17 | Admin
18 |
19 |
20 | )
21 | }
22 |
23 | export default Header
--------------------------------------------------------------------------------
/4_news_app/components/header/NavLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import Link from 'next/link';
4 | import { usePathname } from 'next/navigation';
5 | import classes from './header.module.css';
6 |
7 | interface IProps {
8 | href: string;
9 | children: React.ReactNode;
10 | }
11 |
12 | const NavLink = (props: IProps) => {
13 | const path = usePathname();
14 | return (
15 |
19 | {props.children}
20 |
21 | )
22 | }
23 |
24 | export default NavLink
--------------------------------------------------------------------------------
/4_news_app/components/header/header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | box-sizing: border-box;
3 | width: 100%;
4 | height: 56px;
5 | background: #FFFFFFFF;
6 | /* white */
7 | border-radius: 0px;
8 | box-shadow: 0px 0px 1px #171a1f4a, 0px 0px 2px #171a1f1F;
9 | display: flex;
10 | align-items: center;
11 | justify-content: space-between;
12 | padding: 0 20px;
13 | }
14 |
15 | .header .logo {
16 | display: flex;
17 | align-items: center;
18 | justify-content: flex-start;
19 | column-gap: 10px;
20 | }
21 |
22 | .header .logo img {
23 | height: 36px;
24 | border-radius: 0px;
25 | }
26 |
27 | .header .logo h1 {
28 | font-family: var(--font-roboto);
29 | font-size: 28px;
30 | line-height: 42px;
31 | font-weight: 700;
32 | color: #000000;
33 | }
34 |
35 | .header nav {
36 | display: flex;
37 | align-items: center;
38 | font-family: var(--font-mulish);
39 | font-size: 14px;
40 | line-height: 22px;
41 | font-weight: 400;
42 | opacity: 1;
43 | }
44 |
45 | .header nav a {
46 | display: flex;
47 | align-items: center;
48 | justify-content: center;
49 | padding: 15px 12px;
50 | color: #565E6CFF;
51 | border-radius: 2px;
52 | cursor: pointer;
53 | white-space: nowrap;
54 | text-decoration: none;
55 | }
56 |
57 | .header nav a.selected {
58 | border-bottom: 4px solid #71B2ABFF;
59 | }
--------------------------------------------------------------------------------
/4_news_app/components/hero/Hero.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classes from './hero.module.css';
3 |
4 | const Hero = () => {
5 | return (
6 |
7 |
Stay Informed, Stay Ahead
8 |
Your go-to platform for the latest and most relevant news articles.
9 |
10 |
Post a News
11 |
Read News
12 | {/* outline */}
13 |
14 |
15 | )
16 | }
17 |
18 | export default Hero
--------------------------------------------------------------------------------
/4_news_app/components/hero/hero.module.css:
--------------------------------------------------------------------------------
1 | .hero {
2 | width: 100%;
3 | height: 350px;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | row-gap: 30px;
9 | }
10 |
11 | .hero h2 {
12 | font-family: var(--font-roboto);
13 | font-size: 64px;
14 | line-height: 84px;
15 | font-weight: 700;
16 | color: #171A1FFF;
17 | display: flex;
18 | justify-content: center;
19 | margin: 0;
20 | }
21 |
22 | .hero span {
23 | font-family: var(--font-mulish);
24 | font-size: 20px;
25 | line-height: 30px;
26 | font-weight: 400;
27 | color: #1D2128FF;
28 | }
29 |
30 | .hero .actions {
31 | display: flex;
32 | column-gap: 30px;
33 | }
--------------------------------------------------------------------------------
/4_news_app/components/latest-news/LatestNews.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import classes from './latest-news.module.css';
4 | import Item from './item/Item';
5 |
6 | interface IProps {
7 | subTitle?: string;
8 | newsList: News.Item[];
9 | }
10 |
11 | const LatestNews = (props: IProps) => {
12 | const [highlightedIndex, setHighlightedIndex] = useState(0);
13 |
14 | useEffect(() => {
15 | const sliderInt = setInterval(() => {
16 | setHighlightedIndex(old => (old + 1) % 3);
17 | }, 3000);
18 |
19 | return () => {
20 | clearInterval(sliderInt);
21 | }
22 | }, []);
23 |
24 | return (
25 |
26 |
Latest News Articles
27 | {props.subTitle &&
{props.subTitle} }
28 |
29 | {
30 | props.newsList.map((data, index) => (
31 |
36 | ))
37 | }
38 |
39 |
40 | )
41 | }
42 |
43 | export default LatestNews;
--------------------------------------------------------------------------------
/4_news_app/components/latest-news/item/Item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classes from './item.module.css';
3 | import Image from 'next/image';
4 |
5 | interface IProps {
6 | isHighlighted: boolean;
7 | data: News.Item;
8 | }
9 |
10 | const Item = (props: IProps) => {
11 | const item = props.data;
12 |
13 | return (
14 |
15 |
{item.title?.substring(0, 95)}
16 |
17 | {
18 |
24 | }
25 |
26 |
{item.content?.substring(0, 150)}...
27 |
28 | )
29 | }
30 |
31 | export default Item;
--------------------------------------------------------------------------------
/4_news_app/components/latest-news/item/item.module.css:
--------------------------------------------------------------------------------
1 | .newsItem {
2 | flex: 0 0 23%;
3 | align-items: stretch;
4 | padding: 10px;
5 | cursor: pointer;
6 | box-sizing: border-box;
7 | box-shadow: 0px 0px 4px #ddd, 0px 0px 5px #eee;
8 | transition: transform 0.1s linear;
9 | background: #fff;
10 | }
11 |
12 | .newsItem.highlighted {
13 | transform: scale(1.1);
14 | box-shadow: 0 0 5 5 #dcdcdc;
15 | }
16 |
17 | .newsItem .info .button {
18 | padding: 0px 10px;
19 | font-size: 12px;
20 | }
--------------------------------------------------------------------------------
/4_news_app/components/latest-news/latest-news.module.css:
--------------------------------------------------------------------------------
1 | .latestNews {
2 | background: #FAFAFBFF;
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | padding: 50px 0;
7 | border-bottom: 1px solid #eee;
8 | }
9 |
10 | .latestNews>h2 {
11 | font-family: var(--font-roboto);
12 | font-size: 24px;
13 | font-weight: 700;
14 | color: #000;
15 | margin: 10px 0;
16 | }
17 |
18 | .latestNews>h3 {
19 | font-family: var(--font-roboto);
20 | font-size: 20px;
21 | font-weight: 700;
22 | color: #999;
23 | margin: 0;
24 | margin-bottom: 20px;
25 | }
26 |
27 | .items {
28 | display: flex;
29 | justify-content: center;
30 | gap: 20px;
31 | flex-wrap: wrap;
32 | }
--------------------------------------------------------------------------------
/4_news_app/constants/data.ts:
--------------------------------------------------------------------------------
1 | const CATEGORIES: News.ICategory[] = [
2 | {
3 | title: 'finance',
4 | imageURL: '/cats/finance.webp',
5 | subtitle: ''
6 | },
7 | {
8 | title: 'gaza',
9 | imageURL: '/cats/gaza.webp',
10 | subtitle: ''
11 | },
12 | {
13 | title: 'global',
14 | imageURL: '/cats/global.webp',
15 | subtitle: ''
16 | },
17 | {
18 | title: 'palestine',
19 | imageURL: '/cats/palestine.webp',
20 | subtitle: ''
21 | },
22 | {
23 | title: 'sports',
24 | imageURL: '/cats/sports.webp',
25 | subtitle: ''
26 | },
27 | {
28 | title: 'weather',
29 | imageURL: '/cats/weather.webp',
30 | subtitle: ''
31 | },
32 | {
33 | title: 'westbank',
34 | imageURL: '/cats/westbank.webp',
35 | subtitle: ''
36 | }
37 | ];
38 |
39 | const ALLOWED_CATEGORIES = [
40 | 'global',
41 | 'palestine',
42 | 'gaza',
43 | 'finance',
44 | 'westbank',
45 | 'weather',
46 | 'sports',
47 | ]
48 |
49 | export { CATEGORIES, ALLOWED_CATEGORIES };
--------------------------------------------------------------------------------
/4_news_app/controllers/news-actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { ALLOWED_CATEGORIES } from "@/constants/data";
4 | import { insertArticle } from "@/services/news.service";
5 | import { redirect } from "next/navigation";
6 | import slugify from "slugify";
7 | import xss from "xss";
8 | import fs from 'node:fs';
9 |
10 | const addArticle = async (prevState: { errors: string[] }, formData: FormData) => {
11 | const title = xss(formData.get('title')?.toString() || '');
12 |
13 | const newArticle: News.Item_ = {
14 | title,
15 | image: '',
16 | summary: xss(formData.get('summary')?.toString() || ''),
17 | content: xss(formData.get('content')?.toString() || ''),
18 | date: new Date(formData.get('date')?.toString() || '').getTime(),
19 | author: formData.get('author')?.toString() || '',
20 | author_email: formData.get('author_email')?.toString() || '',
21 | category: formData.get('category')?.toString() || 'global',
22 | slug: slugify(title, { lower: true })
23 | };
24 |
25 | const imgFile = formData.get('image') as File;
26 | const fileExtension = imgFile.name.split('.').pop();
27 | const fileName = `${newArticle.slug}.${fileExtension}`;
28 | const stream = fs.createWriteStream(`public/images/${fileName}`);
29 | const bufferedImage = await imgFile.arrayBuffer()
30 | stream.write(Buffer.from(bufferedImage), (error) => {
31 | if (error) {
32 | throw new Error('Error uploading Image!');
33 | }
34 | });
35 |
36 | newArticle.image = `/images/${fileName}`;
37 |
38 | const errors: string[] = [];
39 |
40 | if (!newArticle.title.length) {
41 | errors.push("The title should not be empty");
42 | }
43 |
44 | if (newArticle.title.length > 300) {
45 | errors.push("The title should be less than 300 chars length");
46 | }
47 |
48 | if (!ALLOWED_CATEGORIES.includes(newArticle.category)) {
49 | errors.push("The category you've provided is not allowed!");
50 | }
51 |
52 | if (newArticle.date > Date.now()) {
53 | errors.push("The date should not be in the future!");
54 | }
55 |
56 | if (errors.length) {
57 | return {
58 | errors
59 | }
60 | }
61 |
62 | await new Promise((resolve) => setTimeout(resolve, 2000));
63 | insertArticle(newArticle);
64 | redirect(`/news/${newArticle.slug}`);
65 | }
66 |
67 | export {
68 | addArticle
69 | };
--------------------------------------------------------------------------------
/4_news_app/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 | //@typescript-eslint/no-unused-vars
15 | ];
16 |
17 | export default eslintConfig;
18 |
--------------------------------------------------------------------------------
/4_news_app/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { verifyToken } from "./utils/auth";
3 | import { cookies } from "next/headers";
4 |
5 | const authenticateUser = async (token: string | undefined, request: NextRequest) => {
6 |
7 | // verify there is a token
8 | if (!token) {
9 | // else redirect to login page!
10 | return NextResponse.redirect(new URL(`/user/login?msg=${encodeURIComponent('You must be logged in to access this!')}`, request.url));
11 | }
12 |
13 | // verify the token is valid
14 | const user = await verifyToken(token);
15 |
16 | if (!user) {
17 | // else redirect to login page!
18 | (await cookies()).delete('auth-token');
19 | return NextResponse.redirect(new URL(`/user/login?msg=${encodeURIComponent('Your session is expired!')}`, request.url));
20 | }
21 | return user;
22 | }
23 |
24 | export const middleware = async (request: NextRequest) => {
25 | // using the middle for logging
26 | console.log(`A request is made to ${request.nextUrl.pathname}`);
27 | // read cookies from the request
28 | const token = request.cookies.get('auth-token')?.value;
29 |
30 | switch (request.nextUrl.pathname) {
31 | case '/admin':
32 | const res = await authenticateUser(token, request);
33 | if (res instanceof (NextResponse)) {
34 | return res;
35 | }
36 |
37 | // check the role stored the in token
38 | if (res.role !== 'admin') {
39 | // else redirect to home page!
40 | return NextResponse.redirect(new URL('/', request.url));
41 | }
42 | // if role is admin => move to next page (admin page)
43 | break;
44 | case '/add-news': {
45 | const res = await authenticateUser(token, request) as News.IUser;
46 | if (res instanceof (NextResponse)) {
47 | return res;
48 | }
49 |
50 | if (!['editor', 'admin'].includes(res.role)) {
51 | return NextResponse.redirect(new URL('/', request.url));
52 | }
53 | break;
54 | }
55 |
56 | default:
57 | break;
58 | }
59 |
60 |
61 | return NextResponse.next();
62 | }
63 |
64 | export const config = {
65 | matcher: ['/news/:path*', '/admin/:path*', '/:path*']
66 | }
--------------------------------------------------------------------------------
/4_news_app/news.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/news.db
--------------------------------------------------------------------------------
/4_news_app/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | images: {
5 | remotePatterns: [ // This will allow all external hosts/domains
6 | {
7 | hostname: "**",
8 | },
9 | ],
10 | domains: ['loremflickr.com'] // This to allow specific domains only
11 | },
12 | async headers() {
13 | return [
14 | {
15 | source: "/api/:path*",
16 | headers: [
17 | {
18 | key: "Access-Control-Allow-Origin",
19 | value: "*",
20 | },
21 | {
22 | key: "Access-Control-Allow-Methods",
23 | value: "GET, POST, PUT, DELETE, OPTIONS",
24 | },
25 | {
26 | key: "Access-Control-Allow-Headers",
27 | value: "Content-Type, Authorization",
28 | }
29 | ],
30 | },
31 | ];
32 | },
33 | };
34 |
35 | export default nextConfig;
36 |
--------------------------------------------------------------------------------
/4_news_app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "4_news_app",
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 | "@phosphor-icons/react": "^2.1.7",
13 | "bcryptjs": "^3.0.2",
14 | "better-sqlite3": "^11.8.1",
15 | "classnames": "^2.5.1",
16 | "jose": "^6.0.10",
17 | "jsonwebtoken": "^9.0.2",
18 | "next": "15.1.6",
19 | "react": "^19.0.0",
20 | "react-dom": "^19.0.0",
21 | "react-toastify": "^11.0.5",
22 | "slugify": "^1.6.6",
23 | "xss": "^1.0.15"
24 | },
25 | "devDependencies": {
26 | "@eslint/eslintrc": "^3",
27 | "@types/bcryptjs": "^3.0.0",
28 | "@types/better-sqlite3": "^7.6.12",
29 | "@types/jsonwebtoken": "^9.0.9",
30 | "@types/node": "^20",
31 | "@types/react": "^19",
32 | "@types/react-dom": "^19",
33 | "eslint": "^9",
34 | "eslint-config-next": "15.1.6",
35 | "typescript": "^5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/4_news_app/public/cat1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cat1.png
--------------------------------------------------------------------------------
/4_news_app/public/cat2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cat2.png
--------------------------------------------------------------------------------
/4_news_app/public/cat3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cat3.jpg
--------------------------------------------------------------------------------
/4_news_app/public/cats/finance.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/finance.webp
--------------------------------------------------------------------------------
/4_news_app/public/cats/gaza.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/gaza.webp
--------------------------------------------------------------------------------
/4_news_app/public/cats/global.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/global.webp
--------------------------------------------------------------------------------
/4_news_app/public/cats/palestine.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/palestine.webp
--------------------------------------------------------------------------------
/4_news_app/public/cats/sports.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/sports.webp
--------------------------------------------------------------------------------
/4_news_app/public/cats/weather.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/weather.webp
--------------------------------------------------------------------------------
/4_news_app/public/cats/westbank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/cats/westbank.webp
--------------------------------------------------------------------------------
/4_news_app/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/4_news_app/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/4_news_app/public/images/corporate-communications-strategist.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/images/corporate-communications-strategist.jpg
--------------------------------------------------------------------------------
/4_news_app/public/images/customer-accounts-officer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/images/customer-accounts-officer.jpg
--------------------------------------------------------------------------------
/4_news_app/public/images/international-identity-orchestrator.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/images/international-identity-orchestrator.jpg
--------------------------------------------------------------------------------
/4_news_app/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/logo.png
--------------------------------------------------------------------------------
/4_news_app/public/n1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kldoon/GSG_React_Next/ba3ac75d98d3986480f8902ce42a3a3e7e8b18d4/4_news_app/public/n1.jpg
--------------------------------------------------------------------------------
/4_news_app/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/4_news_app/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/4_news_app/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/4_news_app/services/auth.ts:
--------------------------------------------------------------------------------
1 | import sql from 'better-sqlite3';
2 | const db = sql('news.db');
3 |
4 | const findUserByEmail = (email: string) => {
5 | const results = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
6 | return results as News.IUser;
7 | }
8 |
9 | const createUser = (user: News.IUser) => {
10 | console.log(user);
11 | /*
12 | This function will
13 | 1- Validate
14 | 2- Check if email already exists
15 | 3- Hash the password
16 | 4- insert into db
17 | */
18 | }
19 |
20 | export {
21 | findUserByEmail,
22 | createUser
23 | }
--------------------------------------------------------------------------------
/4_news_app/services/news.service.ts:
--------------------------------------------------------------------------------
1 | import sql from 'better-sqlite3';
2 | const db = sql('news.db');
3 |
4 | const getNewsByCategory = (category: string): News.Item_[] => {
5 | const results = db.prepare('SELECT * FROM articles WHERE category = ?').all(category);
6 | return results as News.Item_[];
7 | }
8 |
9 | const getNewsArticle = (slug: string): News.Item_ => {
10 | return db.prepare('SELECT * FROM articles WHERE slug = ?').get(slug) as News.Item_;
11 | }
12 |
13 | const insertArticle = (newArticle: News.Item_) => {
14 | db.prepare(`
15 | INSERT INTO articles
16 | (slug, title, image, summary, content, author, author_email, date, category)
17 | VALUES (
18 | @slug,
19 | @title,
20 | @image,
21 | @summary,
22 | @content,
23 | @author,
24 | @author_email,
25 | @date,
26 | @category
27 | )`)
28 | .run(newArticle);
29 | }
30 |
31 | const api_key = 'pub_701076cdd4cdeaa56df41b17fae04f1ce8350';
32 |
33 | /**
34 | * @deprecated we will use our db to fetch data
35 | */
36 | const fetchNews = async (category: string, country: string) => {
37 | const res = await fetch(
38 | `https://newsdata.io/api/1/latest?apikey=${api_key}&category=${category}&country=${country}`,
39 | { method: 'GET', cache: 'no-store' }
40 | );
41 |
42 | const newsRes = (await res.json()) as News.IResponse;
43 | let latestNews: News.Item[] = [];
44 | if (newsRes.status === 'success') {
45 | latestNews = newsRes.results.map(item => (
46 | {
47 | id: item.article_id,
48 | title: item.title,
49 | img: item.image_url,
50 | content: item.description
51 | }
52 | ));
53 | } else {
54 | // triggering notFound manually
55 | // notFound();
56 | }
57 |
58 | // throw new Error("Something went wrong while fetching the News from server [xa2658]");
59 |
60 | // The goal of the promise below is to make the response slower (just to demo loading status)
61 | return new Promise((resolve) => setTimeout(() => {
62 | resolve(latestNews);
63 | }, 1000));
64 | }
65 |
66 | export {
67 | fetchNews,
68 | getNewsByCategory,
69 | getNewsArticle,
70 | insertArticle
71 | }
--------------------------------------------------------------------------------
/4_news_app/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": "NodeNext",
11 | "moduleResolution": "nodenext",
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/types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace News {
2 | export interface IResponseNewsItem {
3 | article_id: string;
4 | description: string;
5 | title: string;
6 | image_url: string;
7 | }
8 |
9 | export interface IResponse {
10 | status: string;
11 | totalResults: string;
12 | results: IResponseNewsItem[];
13 | }
14 |
15 | export interface Item {
16 | id: string;
17 | title: string;
18 | img: string | null;
19 | content: string;
20 | }
21 |
22 | export interface ICategory {
23 | title: string;
24 | subtitle: string;
25 | imageURL: string;
26 | }
27 |
28 | export interface Item_ {
29 | id?: string;
30 | title: string;
31 | slug: string;
32 | image: string;
33 | summary: string;
34 | content: string;
35 | author: string;
36 | author_email: string;
37 | date: number;
38 | category: string;
39 | }
40 |
41 | export interface IUser {
42 | email: string;
43 | password?: string;
44 | role: string;
45 | displayName: string;
46 | }
47 | }
--------------------------------------------------------------------------------
/4_news_app/utils/auth.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { compareSync, hashSync } from "bcryptjs";
4 | import * as jose from 'jose';
5 | const JWT_SECRET = process.env.JWT_SECRET || '';
6 |
7 | const comparePassword = (password: string, hashedPassword: string): boolean => {
8 | return compareSync(password, hashedPassword);
9 | }
10 |
11 | const hashPassword = (password: string): string => {
12 | return hashSync(password);
13 | }
14 |
15 | const generateToken = async (user: News.IUser) => {
16 | const token = await new jose.SignJWT({ email: user.email, role: user.role, displayName: user.displayName })
17 | .setExpirationTime('1w')
18 | .setProtectedHeader({ alg: 'HS256' })
19 | .sign(new TextEncoder().encode(JWT_SECRET));
20 |
21 | return token;
22 | }
23 |
24 | const verifyToken = async (token: string): Promise => {
25 | try {
26 | const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(JWT_SECRET));
27 | return payload as unknown as News.IUser;
28 | } catch (err) {
29 | console.log(err);
30 | return null;
31 | }
32 | }
33 |
34 | export {
35 | comparePassword,
36 | hashPassword,
37 | generateToken,
38 | verifyToken
39 | }
--------------------------------------------------------------------------------
/memory-card-game/.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 |
--------------------------------------------------------------------------------
/memory-card-game/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 |
--------------------------------------------------------------------------------
/memory-card-game/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 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/memory-card-game/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/memory-card-game/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "memory-card-game",
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.3"
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 | }
31 |
--------------------------------------------------------------------------------
/memory-card-game/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/memory-card-game/screens.txt:
--------------------------------------------------------------------------------
1 | - Login Screen
2 | - Levels Screen
3 | - Level Component
4 | - Tutorial Component
5 | - Game Screen
6 | - Card Component
7 | - Cards List Component
8 | - Status Component (name, level, score, elapsed time, moves, retry)
9 | - Score Board
10 | - Score List Component
11 | - Score Item Component
12 | - Top 3 Component
13 |
14 |
--------------------------------------------------------------------------------
/memory-card-game/src/App.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Rocher';
3 | src: url(https://assets.codepen.io/9632/RocherColorGX.woff2);
4 | }
5 |
6 | * {
7 | font-family: 'Courier New', Courier, monospace;
8 | }
9 |
10 | body {
11 | background: linear-gradient(174deg, #d9d9d9, #bfbeff);
12 | height: 100vh;
13 | overflow: auto;
14 | margin: 0;
15 | padding: 0;
16 | color: #333;
17 | }
18 |
19 | #root {
20 | max-width: 1280px;
21 | margin: 0 auto;
22 | padding: 2rem;
23 | text-align: center;
24 | }
25 |
26 | .placeholder {
27 | border: 1px solid #cdcdcd;
28 | }
29 |
30 | /* Copied form the web :-)*/
31 | button {
32 | align-items: center;
33 | appearance: none;
34 | background-color: #FCFCFD;
35 | border-radius: 4px;
36 | border-width: 0;
37 | box-shadow: rgba(45, 35, 66, 0.4) 0 2px 4px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #D6D6E7 0 -3px 0 inset;
38 | box-sizing: border-box;
39 | color: #36395A;
40 | cursor: pointer;
41 | display: inline-flex;
42 | font-family: "JetBrains Mono", monospace;
43 | height: 48px;
44 | justify-content: center;
45 | line-height: 1;
46 | list-style: none;
47 | overflow: hidden;
48 | padding-left: 16px;
49 | padding-right: 16px;
50 | position: relative;
51 | text-align: left;
52 | text-decoration: none;
53 | transition: box-shadow .15s, transform .15s;
54 | user-select: none;
55 | -webkit-user-select: none;
56 | touch-action: manipulation;
57 | white-space: nowrap;
58 | will-change: box-shadow, transform;
59 | font-size: 18px;
60 | }
61 |
62 | button:focus {
63 | box-shadow: #D6D6E7 0 0 0 1.5px inset, rgba(45, 35, 66, 0.4) 0 2px 4px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #D6D6E7 0 -3px 0 inset;
64 | }
65 |
66 | button:hover {
67 | box-shadow: rgba(45, 35, 66, 0.4) 0 4px 8px, rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #D6D6E7 0 -3px 0 inset;
68 | transform: translateY(-2px);
69 | }
70 |
71 | button:active {
72 | box-shadow: #D6D6E7 0 3px 7px inset;
73 | transform: translateY(2px);
74 | }
--------------------------------------------------------------------------------
/memory-card-game/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom';
2 | import './App.css'
3 | import GameScreen from './screens/game.screen';
4 | import LevelsScreen from './screens/levels.screen';
5 | import ScoreBoardScreen from './screens/score-board.screen';
6 | import NotFound from './screens/not-found.screen';
7 | import { GameModeProvider } from './providers/modeProvider';
8 |
9 | function App() {
10 | const routes: RouteObject[] = [
11 | {
12 | path: '/',
13 | element:
14 | },
15 | {
16 | path: '/game',
17 | element:
18 | },
19 | {
20 | path: '/score-board',
21 | element:
22 | },
23 | {
24 | path: '*',
25 | element:
26 | }
27 | ];
28 |
29 | const browserRouter = createBrowserRouter(routes);
30 |
31 | return (
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default App;
39 |
--------------------------------------------------------------------------------
/memory-card-game/src/components/card-list/card-list.css:
--------------------------------------------------------------------------------
1 | .card-list {
2 | display: grid;
3 | gap: 10px;
4 | width: 100%;
5 | max-width: 75vmin;
6 | }
7 |
8 | .card-list.level_2 {
9 | grid-template-columns: repeat(2, 1fr);
10 | grid-template-rows: repeat(2, 1fr);
11 | }
12 |
13 | .card-list.level_4 {
14 | grid-template-columns: repeat(4, 1fr);
15 | grid-template-rows: repeat(4, 1fr);
16 | }
17 |
18 | .card-list.level_6 {
19 | grid-template-columns: repeat(6, 1fr);
20 | grid-template-rows: repeat(6, 1fr);
21 | }
--------------------------------------------------------------------------------
/memory-card-game/src/components/card-list/card-list.tsx:
--------------------------------------------------------------------------------
1 | import { ICard } from '../../types/@types';
2 | import Card from '../card/card';
3 | import './card-list.css';
4 | import { Action } from '../../providers/reducer';
5 | import { useContext } from 'react';
6 | import { GameModeContext } from '../../providers/modeProvider';
7 |
8 | interface IProps {
9 | cards: ICard[];
10 | dispatch: React.Dispatch;
11 | }
12 |
13 | const CardList = (props: IProps) => {
14 | const { gameMode } = useContext(GameModeContext);
15 |
16 | return (
17 |
18 | {
19 | props.cards.map((card, index) => (
20 |
26 | ))
27 | }
28 |
29 | )
30 | }
31 |
32 | export default CardList;
--------------------------------------------------------------------------------
/memory-card-game/src/components/card/card.css:
--------------------------------------------------------------------------------
1 | /* .card {
2 | aspect-ratio: 1;
3 | border: 1px solid gray;
4 | border-radius: 5px;
5 | background-size: contain;
6 | background-position: center;
7 | background-repeat: no-repeat;
8 | cursor: pointer;
9 | } */
10 |
11 | .card {
12 | aspect-ratio: 1;
13 | border-radius: 5px;
14 | background-size: contain;
15 | background-position: center;
16 | background-repeat: no-repeat;
17 | cursor: pointer;
18 | position: relative;
19 | }
20 |
21 | .card:hover {
22 | border-width: 3px;
23 | }
24 |
25 | .card-front {
26 | background-color: white;
27 | background-image: url(https://api.clipart.com/img/previews/icon-set-98.png);
28 | }
29 |
30 | .card-back {
31 | transform: rotateY(180deg);
32 | background-color: white;
33 | }
34 |
35 | .card-inner {
36 | width: 100%;
37 | height: 100%;
38 | position: relative;
39 | transform-style: preserve-3d;
40 | transition: transform 0.4s ease-in-out;
41 | border-radius: 15px;
42 | box-shadow: 1px 1px 4px 0px #4a4a4a;
43 | }
44 |
45 | .card.flipped .card-inner {
46 | transform: rotateY(180deg);
47 | }
48 |
49 | .card-front,
50 | .card-back {
51 | position: absolute;
52 | width: 100%;
53 | height: 100%;
54 | backface-visibility: hidden;
55 | border-radius: 6px;
56 | background-size: 100%;
57 | background-repeat: no-repeat;
58 | background-position: center;
59 | border-radius: 15px;
60 | }
61 |
62 | .card-front {
63 | background-size: 60%;
64 | }
--------------------------------------------------------------------------------
/memory-card-game/src/components/card/card.tsx:
--------------------------------------------------------------------------------
1 | import { Action } from '../../providers/reducer';
2 | import { ICard } from '../../types/@types';
3 | import './card.css';
4 |
5 | interface IProps {
6 | data: ICard;
7 | index: number;
8 | dispatch: React.Dispatch;
9 | }
10 |
11 | const Card = (props: IProps) => {
12 | const handleFlip = () => {
13 | props.dispatch({ type: 'flip-card', payload: { id: props.data.id, index: props.index } });
14 | }
15 |
16 | return (
17 |
29 | )
30 | }
31 |
32 | export default Card;
--------------------------------------------------------------------------------
/memory-card-game/src/components/congrats/congrats.css:
--------------------------------------------------------------------------------
1 | .congrats {
2 | display: flex;
3 | position: absolute;
4 | font-family: 'Courier New', Courier, monospace;
5 | z-index: 5;
6 | color: #fff34b;
7 | background-color: rgb(5 19 40);
8 | font-size: 22px;
9 | align-content: center;
10 | align-items: center;
11 | flex-direction: column;
12 | row-gap: 30px;
13 | padding: 70px;
14 | max-width: 75vw;
15 |
16 | color: #fff;
17 | text-shadow: 0 0 7px #000000,
18 | 0 0 10px #8c4a4a,
19 | 0 0 21px #a8a7de,
20 | 0 0 42px #bc13fe,
21 | 0 0 82px #bc13fe,
22 | 0 0 92px #bc13fe,
23 | 0 0 102px #bc13fe,
24 | 0 0 151px #bc13fe;
25 | box-shadow: 0px 0px 18px 7px #1c2866;
26 | }
--------------------------------------------------------------------------------
/memory-card-game/src/components/congrats/congrats.tsx:
--------------------------------------------------------------------------------
1 | import './congrats.css'
2 |
3 | const Congrats = () => {
4 | return (
5 |
6 |
Congratulations You WON the game 🤑😛
7 |
You will be redirected to the ScoreBoard shortly
8 |
9 | )
10 | }
11 |
12 | export default Congrats;
--------------------------------------------------------------------------------
/memory-card-game/src/components/score-list/score-list.css:
--------------------------------------------------------------------------------
1 | ul.score-list {
2 | list-style-type: none;
3 | padding: 0;
4 | margin: 0;
5 | text-align: left;
6 | }
7 |
8 | ul.score-list li {
9 | display: flex;
10 | justify-content: space-between;
11 | }
--------------------------------------------------------------------------------
/memory-card-game/src/components/score-list/score-list.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { IScore } from '../../types/@types';
3 | import './score-list.css';
4 |
5 | interface IProps {
6 | scores: IScore[];
7 | }
8 | const ScoreList = (props: IProps) => {
9 | return (
10 |
11 | {
12 | props.scores.map((score, index) => (
13 |
14 | Name: {score.playerName}
15 | | Level: {Array.from({ length: score.level / 2 }).map(_ => '⭐')}
16 | | Time: {score.finishTime}
17 | | Wrong Moves: {score.wrongMoves}
18 |
19 | )
20 | )
21 | }
22 |
23 | )
24 | }
25 |
26 | export default ScoreList;
--------------------------------------------------------------------------------
/memory-card-game/src/components/status-bar/status-bar.css:
--------------------------------------------------------------------------------
1 | .status-bar {
2 | display: flex;
3 | justify-content: space-between;
4 | font-size: 25px;
5 | font-weight: bold;
6 | font-family: 'Courier New', Courier, monospace;
7 | background: linear-gradient(360deg, #d9d9d9, #d8d7ffb0);
8 | width: 75vmin;
9 | padding: 23px;
10 | border-radius: 20px;
11 | box-shadow: 2px 2px 1px 0px #c6c6c6;
12 | }
--------------------------------------------------------------------------------
/memory-card-game/src/components/status-bar/status-bar.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 |
3 | import './status-bar.css';
4 | import { useContext } from 'react';
5 | import { GameModeContext } from '../../providers/modeProvider';
6 |
7 | const StatusBar = () => {
8 | const { gameMode } = useContext(GameModeContext);
9 |
10 | return (
11 |
12 | Level: {Array.from({ length: gameMode.level / 2 }).map(_ => '⭐')}
13 | Time: {gameMode.time}s
14 | Wrong Moves: {gameMode.wrongMoves}
15 |
16 | )
17 | }
18 |
19 | export default StatusBar;
--------------------------------------------------------------------------------
/memory-card-game/src/hooks/game-logic.hook.ts:
--------------------------------------------------------------------------------
1 | import { useReducer, useContext, useRef, useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { GameModeContext } from "../providers/modeProvider";
4 | import { gameReducer } from "../providers/reducer";
5 | import { checkedFinished } from "../utils/game.util";
6 | import { IScore } from "../types/@types";
7 |
8 | const storeScore = (score: IScore) => {
9 | const scores: IScore[] = JSON.parse(localStorage.getItem('flip-cards-scores') || '[]');
10 | scores.push(score);
11 | localStorage.setItem('flip-cards-scores', JSON.stringify(scores));
12 | }
13 |
14 | const useGameLogic = () => {
15 | const [state, dispatch] = useReducer(gameReducer, { cards: [], initialized: false, openCards: [] });
16 | const { gameMode, setGameMode, resetGame } = useContext(GameModeContext);
17 | const timerRef = useRef(0);
18 | const navigate = useNavigate();
19 |
20 | useEffect(() => {
21 | if (!gameMode.playerName) {
22 | navigate('/');
23 | return;
24 | }
25 |
26 | if (!state.initialized) {
27 | dispatch({ type: 'init', payload: { level: gameMode.level } });
28 | resetGame();
29 | timerRef.current = setInterval(() => {
30 | setGameMode(old => ({ ...old, time: old.time + 1 }));
31 | }, 1000);
32 | }
33 |
34 | return () => {
35 | clearInterval(timerRef.current);
36 | }
37 | }, []);
38 |
39 | useEffect(() => {
40 | if (!state.initialized) return;
41 |
42 | if (state.openCards.length === 2) {
43 | setGameMode(old => ({ ...old, wrongMoves: old.wrongMoves + 1 }));
44 | setTimeout(() => {
45 | dispatch({ type: 'hide-mismatch' });
46 | }, 1500);
47 | }
48 |
49 | const isFinished = checkedFinished(state.cards);
50 | if (isFinished) {
51 | setGameMode(old => ({ ...old, finished: true }));
52 | clearInterval(timerRef.current);
53 |
54 | const score: IScore = {
55 | finishTime: gameMode.time,
56 | level: gameMode.level,
57 | playerName: gameMode.playerName,
58 | wrongMoves: gameMode.wrongMoves
59 | }
60 | storeScore(score);
61 |
62 | setTimeout(() => {
63 | navigate('/score-board');
64 | }, 3000);
65 | }
66 | }, [state.openCards]);
67 |
68 | return {
69 | state,
70 | gameMode,
71 | dispatch
72 | }
73 |
74 | }
75 |
76 | export default useGameLogic;
--------------------------------------------------------------------------------
/memory-card-game/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import App from './App.tsx'
3 |
4 | createRoot(document.getElementById('root')!).render( )
5 |
--------------------------------------------------------------------------------
/memory-card-game/src/providers/modeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 | import { ELevels } from "../types/@types";
3 |
4 | export interface IGameMode {
5 | playerName: string;
6 | level: ELevels;
7 | finished: boolean;
8 | time: number;
9 | wrongMoves: number;
10 | }
11 |
12 | export interface IGameModeContext {
13 | gameMode: IGameMode;
14 | setGameMode: React.Dispatch>;
15 | resetGame: () => void;
16 | }
17 |
18 | const INIT_STATE: IGameMode = { playerName: '', level: ELevels.EASY, finished: false, time: 0, wrongMoves: 0 };
19 |
20 | export const GameModeContext = createContext(
21 | { gameMode: INIT_STATE, setGameMode: () => { }, resetGame: () => { } }
22 | );
23 |
24 | export const GameModeProvider = (props: { children: React.ReactNode }) => {
25 | const [gameMode, setGameMode] = useState(INIT_STATE);
26 |
27 | const resetGame = () => {
28 | setGameMode(old => ({ ...INIT_STATE, level: old.level, playerName: old.playerName }));
29 | }
30 |
31 | return {props.children}
32 | }
--------------------------------------------------------------------------------
/memory-card-game/src/providers/reducer.ts:
--------------------------------------------------------------------------------
1 | import { ELevels, ICard } from "../types/@types";
2 | import { createGameBoard } from "../utils/game.util";
3 |
4 | export interface IGameState {
5 | initialized: boolean;
6 | cards: ICard[];
7 | openCards: number[];
8 | }
9 |
10 | export type Action =
11 | { type: 'init', payload: { level: ELevels } }
12 | | { type: 'flip-card', payload: { id: number, index: number } }
13 | | { type: 'hide-mismatch' };
14 |
15 | export const gameReducer = (state: IGameState, action: Action): IGameState => {
16 | switch (action.type) {
17 | case 'init': {
18 | const cards = createGameBoard(action.payload.level);
19 | return { ...state, initialized: true, cards };
20 | }
21 |
22 | case 'flip-card': {
23 | if (state.openCards.includes(action.payload.index) ||
24 | state.openCards.length === 2 ||
25 | state.cards[action.payload.index].revealed
26 | || state.cards[action.payload.index].visible) {
27 | return state;
28 | }
29 |
30 | let openCards = [...state.openCards, action.payload.index];
31 | const cards: ICard[] = [...state.cards];
32 | if (openCards.length === 2) {
33 | cards[openCards[0]].visible = true;
34 | cards[openCards[1]].visible = true;
35 | if (cards[openCards[0]].id === cards[openCards[1]].id) {
36 | cards[openCards[0]].revealed = true;
37 | cards[openCards[1]].revealed = true;
38 | openCards = [];
39 | }
40 | } else {
41 | if (!cards[action.payload.index].visible) {
42 | cards[action.payload.index].visible = true;
43 | }
44 | }
45 | return { ...state, openCards, cards };
46 | }
47 | case 'hide-mismatch': {
48 | if (state.cards[state.openCards[0]].id !== state.cards[state.openCards[1]].id) {
49 | const cards: ICard[] = [...state.cards];
50 | cards[state.openCards[0]].visible = false;
51 | cards[state.openCards[1]].visible = false;
52 | return { ...state, openCards: [], cards };
53 | } else {
54 | return state;
55 | }
56 | }
57 | default:
58 | return state
59 | }
60 | }
--------------------------------------------------------------------------------
/memory-card-game/src/screens/game.screen.tsx:
--------------------------------------------------------------------------------
1 | import './screens.css';
2 | import CardList from '../components/card-list/card-list';
3 | import Congrats from '../components/congrats/congrats';
4 |
5 | import StatusBar from '../components/status-bar/status-bar';
6 | import useGameLogic from '../hooks/game-logic.hook';
7 |
8 | const GameScreen = () => {
9 | const { state, dispatch, gameMode } = useGameLogic();
10 |
11 | return (
12 |
13 |
14 |
18 | {gameMode.finished && }
19 |
20 | )
21 | }
22 |
23 | export default GameScreen;
--------------------------------------------------------------------------------
/memory-card-game/src/screens/levels.screen.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from "react";
2 | import { GameModeContext } from "../providers/modeProvider";
3 | import { Link, useNavigate } from "react-router-dom";
4 | import { ELevels } from "../types/@types";
5 |
6 | const LevelsScreen = () => {
7 | const { gameMode, setGameMode } = useContext(GameModeContext);
8 | const [error, setError] = useState('');
9 | const navigate = useNavigate();
10 |
11 | const handleSelectLevel = (level: ELevels) => {
12 | if (gameMode.playerName.length < 3) {
13 | setError("Please enter your name (at least 3 chars)");
14 | return;
15 | }
16 | setGameMode(old => ({ ...old, level }));
17 | navigate("/game");
18 | };
19 |
20 | const handleChangeName = (event: React.ChangeEvent) => {
21 | const name = event.currentTarget.value || '';
22 | setGameMode(old => ({ ...old, playerName: name }));
23 | }
24 |
25 | return (
26 |
27 |
Flip Card Game
28 |
Score Board
29 |
30 |
31 |
Please enter your name
32 |
33 | {Boolean(error) &&
{error}
}
34 |
35 |
Please select your level
36 |
37 | handleSelectLevel(ELevels.EASY)}>Easy
38 | handleSelectLevel(ELevels.MEDIUM)}>Medium
39 | handleSelectLevel(ELevels.HARD)}>Hard
40 |
41 |
42 | )
43 | }
44 |
45 | export default LevelsScreen;
--------------------------------------------------------------------------------
/memory-card-game/src/screens/not-found.screen.tsx:
--------------------------------------------------------------------------------
1 | const NotFound = () => {
2 | return (
3 | The page you requested doesn't exists
4 | )
5 | }
6 |
7 | export default NotFound;
--------------------------------------------------------------------------------
/memory-card-game/src/screens/score-board.screen.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { IScore } from '../types/@types';
3 | import ScoreList from '../components/score-list/score-list';
4 | import { Link } from 'react-router-dom';
5 |
6 | const getScores = () => {
7 | const scores: IScore[] = JSON.parse(localStorage.getItem('flip-cards-scores') || '[]');
8 | return scores;
9 | }
10 |
11 | function getTopPlayers(scores: IScore[]): IScore[] {
12 | return scores
13 | .sort((a, b) =>
14 | b.level - a.level || // Higher level first
15 | a.finishTime - b.finishTime || // Lower finish time first
16 | a.wrongMoves - b.wrongMoves // Lower wrong moves first
17 | )
18 | .slice(0, 3);
19 | }
20 |
21 |
22 | const ScoreBoardScreen = () => {
23 | const [savedScores, setSavedScores] = useState([]);
24 | const [topPlayers, setTopPlayers] = useState([]);
25 |
26 | useEffect(() => {
27 | const ss = getScores();
28 | setSavedScores(ss);
29 | const tp = getTopPlayers(ss);
30 | setTopPlayers(tp);
31 | }, []);
32 |
33 | const handleClearAll = () => {
34 | if (confirm('Sure?')) {
35 | localStorage.removeItem('flip-cards-scores');
36 | }
37 | }
38 |
39 | return (
40 |
41 |
Score Board
42 |
New Game
43 |
44 |
Clear all Scores
45 | {
46 | !savedScores.length
47 | ?
No Scores Found!
48 | : (
49 |
50 |
51 |
All Games
52 |
53 |
54 |
55 |
Top 3 Games
56 |
57 |
58 |
59 | )
60 | }
61 |
62 |
63 | )
64 | }
65 |
66 | export default ScoreBoardScreen;
--------------------------------------------------------------------------------
/memory-card-game/src/screens/screens.css:
--------------------------------------------------------------------------------
1 | .screen.game-screen {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | flex-direction: column;
6 | row-gap: 20px;
7 | margin: 0 auto;
8 | }
9 |
10 | .screen.levels-screen .player {
11 | margin-bottom: 30px;
12 | }
13 |
14 | .screen.levels-screen .player input {
15 | padding: 10px;
16 | font-size: 20px;
17 | border-radius: 10px;
18 | outline: none;
19 | border: 1px solid #aaa;
20 | }
21 |
22 | .screen.levels-screen .levels {
23 | display: flex;
24 | width: 60vmin;
25 | justify-content: space-between;
26 | align-items: center;
27 | column-gap: 30px;
28 | margin: 0 auto;
29 | }
30 |
31 | .screen.levels-screen .levels .level {
32 | padding: 50px;
33 | box-sizing: border-box;
34 | }
35 |
36 | .screen.levels-screen .levels .level:hover {
37 | border: 3px solid gray;
38 | }
39 |
40 | .screen.score-screen .boards>:first-child {
41 | border-right: 2px solid gray;
42 | }
43 |
44 | .screen.score-screen .boards {
45 | display: flex;
46 | flex-direction: row;
47 | justify-content: space-between;
48 | align-items: flex-start;
49 | margin-top: 50px;
50 | }
51 |
52 | .screen.score-screen .boards>div {
53 | padding: 0 30px;
54 | width: 50%;
55 | }
56 |
57 | .screen.score-screen .boards>div h2 {
58 | margin-top: 0;
59 | }
--------------------------------------------------------------------------------
/memory-card-game/src/types/@types.ts:
--------------------------------------------------------------------------------
1 | export enum ELevels {
2 | EASY = 2,
3 | MEDIUM = 4,
4 | HARD = 6
5 | }
6 |
7 | export interface ICard {
8 | id: number;
9 | image: string;
10 | visible: boolean;
11 | revealed: boolean;
12 | }
13 |
14 | export interface IScore {
15 | playerName: string;
16 | finishTime: number;
17 | wrongMoves: number;
18 | level: ELevels;
19 | }
--------------------------------------------------------------------------------
/memory-card-game/src/utils/game.util.ts:
--------------------------------------------------------------------------------
1 | import { ELevels, ICard } from "../types/@types";
2 |
3 | const EMPTY_CARD: ICard = {
4 | id: 0, image: '', visible: false, revealed: false
5 | }
6 |
7 | export const createGameBoard = (level: ELevels): ICard[] => {
8 | let cards: ICard[] = Array.from({ length: level * level },
9 | (_, index) => (index % 2 === 0) ? { ...EMPTY_CARD, id: index } : { ...EMPTY_CARD, id: index - 1, image: '' })
10 | .sort(() => Math.random() - 0.5);
11 |
12 | // fill cards images
13 | cards = cards.map(c => ({ ...c, image: `https://api.clipart.com/img/previews/education-${c.id + 1}.jpg` }));
14 | return cards;
15 | }
16 |
17 | export const checkedFinished = (cards: ICard[]) => {
18 | return cards.every(c => c.revealed);
19 | }
--------------------------------------------------------------------------------
/memory-card-game/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/memory-card-game/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 |
--------------------------------------------------------------------------------
/memory-card-game/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/memory-card-game/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 |
--------------------------------------------------------------------------------
/memory-card-game/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 |
--------------------------------------------------------------------------------
/testing-ui/.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 |
--------------------------------------------------------------------------------
/testing-ui/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 | ```js
15 | export default tseslint.config({
16 | extends: [
17 | // Remove ...tseslint.configs.recommended and replace with this
18 | ...tseslint.configs.recommendedTypeChecked,
19 | // Alternatively, use this for stricter rules
20 | ...tseslint.configs.strictTypeChecked,
21 | // Optionally, add this for stylistic rules
22 | ...tseslint.configs.stylisticTypeChecked,
23 | ],
24 | languageOptions: {
25 | // other options...
26 | parserOptions: {
27 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
28 | tsconfigRootDir: import.meta.dirname,
29 | },
30 | },
31 | })
32 | ```
33 |
34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
35 |
36 | ```js
37 | // eslint.config.js
38 | import reactX from 'eslint-plugin-react-x'
39 | import reactDom from 'eslint-plugin-react-dom'
40 |
41 | export default tseslint.config({
42 | plugins: {
43 | // Add the react-x and react-dom plugins
44 | 'react-x': reactX,
45 | 'react-dom': reactDom,
46 | },
47 | rules: {
48 | // other rules...
49 | // Enable its recommended typescript rules
50 | ...reactX.configs['recommended-typescript'].rules,
51 | ...reactDom.configs.recommended.rules,
52 | },
53 | })
54 | ```
55 |
--------------------------------------------------------------------------------
/testing-ui/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 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/testing-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/testing-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testing-ui",
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": "^19.0.0",
14 | "react-dom": "^19.0.0"
15 | },
16 | "devDependencies": {
17 | "@eslint/js": "^9.21.0",
18 | "@types/react": "^19.0.10",
19 | "@types/react-dom": "^19.0.4",
20 | "@vitejs/plugin-react": "^4.3.4",
21 | "eslint": "^9.21.0",
22 | "eslint-plugin-react-hooks": "^5.1.0",
23 | "eslint-plugin-react-refresh": "^0.4.19",
24 | "globals": "^15.15.0",
25 | "typescript": "~5.7.2",
26 | "typescript-eslint": "^8.24.1",
27 | "vite": "^6.2.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/testing-ui/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testing-ui/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/testing-ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import './App.css'
4 |
5 | function App() {
6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
7 | const [newsList, setNewsList] = useState([]);
8 |
9 | const fetchNews = () => {
10 | fetch('http://127.0.0.1:3000/api/news?category=palestine')
11 | .then(res => res.json())
12 | .then(res => setNewsList(res.results));
13 | }
14 |
15 | const submitNews = () => {
16 | fetch('http://127.0.0.1:3000/api/news?category=palestine', {
17 | method: 'POST',
18 | body: JSON.stringify({
19 | "message": "hi from FE"
20 | })
21 | })
22 | .then(res => res.json())
23 | .then(res => setNewsList(res.results));
24 | }
25 |
26 | const loadArticle = (slug: string) => {
27 | fetch(`http://127.0.0.1:3000/api/news/${slug}123`)
28 | .then(res => res.json())
29 | .then(res => console.log(res));
30 | }
31 |
32 | return (
33 |
34 |
UI for testing the APIs
35 |
36 |
37 | Fetch News
38 |
39 |
40 | Submit News
41 |
42 |
43 |
44 | {
45 | newsList.map(item => loadArticle(item.slug)}>{item.title} )
46 | }
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default App
54 |
--------------------------------------------------------------------------------
/testing-ui/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testing-ui/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: 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 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/testing-ui/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.tsx'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/testing-ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/testing-ui/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 |
--------------------------------------------------------------------------------
/testing-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/testing-ui/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 |
--------------------------------------------------------------------------------
/testing-ui/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 |
--------------------------------------------------------------------------------