├── src ├── constants │ ├── README.md │ └── article-const.ts ├── helpers │ ├── README.md │ └── article-helper.ts ├── hooks │ └── README.md ├── providers │ └── README.md ├── tests │ └── README.md ├── contexts │ └── README.md ├── redux │ ├── README.md │ ├── features │ │ ├── README.md │ │ ├── reducer.ts │ │ ├── articleSlice.ts │ │ └── userSlice.ts │ ├── store.ts │ └── saga.ts ├── pages │ ├── api │ │ ├── README.md │ │ ├── hello.ts │ │ ├── imagekit-auth.ts │ │ ├── article-detail.ts │ │ ├── article-tag.ts │ │ └── cloudinary-auth.ts │ ├── user │ │ ├── profile.tsx │ │ ├── recent.tsx │ │ └── favorite.tsx │ ├── index.tsx │ ├── _app.tsx │ └── articles │ │ ├── [slug].tsx │ │ └── tag │ │ └── [tag].tsx ├── utils │ ├── get-router-param.ts │ ├── get-unique-ary.ts │ ├── get-id-from-slug.ts │ ├── README.md │ ├── console-log.ts │ ├── upload-to-imagekit.ts │ └── upload-to-cloudinary.ts ├── types │ ├── README.md │ └── article-types.ts ├── configs │ ├── README.md │ └── main-config.ts ├── assets │ ├── styles │ │ ├── globals.css │ │ └── Home.module.css │ └── README.md ├── components │ ├── README.md │ ├── ActionToaster.tsx │ ├── SearchBar.tsx │ ├── FavoriteItems.tsx │ ├── FavoriteItemHeartIcon.tsx │ ├── ArticleImageList.tsx │ └── ArticleDetail.tsx ├── layouts │ ├── README.md │ ├── MainLayout.tsx │ ├── MainFooter.tsx │ ├── HeadMeta.tsx │ └── MainHeader.tsx ├── apis │ ├── README.md │ ├── user-api.ts │ └── article-api.ts ├── i18n │ └── README.md └── services │ └── README.md ├── public ├── favicon.ico ├── vercel.svg └── savedData │ ├── article-1246659.json │ ├── article-1245732.json │ ├── article-1232816.json │ ├── article-1228953.json │ ├── article-1213427.json │ ├── article-1053472.json │ ├── article-1246592.json │ ├── article-1223039.json │ ├── article-1246952.json │ ├── article-1240354.json │ ├── article-1196320.json │ ├── article-1208531.json │ ├── article-1246293.json │ ├── article-1209550.json │ ├── article-1246616.json │ ├── article-1246380.json │ ├── article-1021330.json │ ├── article-1230015.json │ ├── article-1242777.json │ ├── article-1246943.json │ ├── article-1241056.json │ ├── article-1246947.json │ ├── article-1237647.json │ ├── article-1241941.json │ ├── article-1246326.json │ ├── article-1241348.json │ ├── article-1221876.json │ ├── article-1210441.json │ ├── article-1244300.json │ └── article-1246730.json ├── .vscode └── settings.json ├── next-env.d.ts ├── next.config.js ├── tsconfig.json ├── .env.local.example ├── LICENSE ├── .eslintrc.js ├── README.md ├── package.json └── .gitignore /src/constants/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **constants** folder files save constants used throughout the application. 4 | -------------------------------------------------------------------------------- /src/helpers/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **helpers** folder files save xxx-helpers.tx used throughout the application. 4 | -------------------------------------------------------------------------------- /src/hooks/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **hooks** folder files save React custom hooks used throughout the application. 4 | -------------------------------------------------------------------------------- /src/providers/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **providers** folder files save providers used throughout the application. 4 | -------------------------------------------------------------------------------- /src/tests/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **tests** folder files save react test files used throughout the application. 4 | -------------------------------------------------------------------------------- /src/contexts/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **context** folder files save react contexts used throughout the application. 4 | -------------------------------------------------------------------------------- /src/redux/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **redux** folder files save Redux actions and reducers used throughout the application. 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexStack/nextjs-redux-toolkit-saga-react-query-hook-form-materia-mui-typescript-scaffold/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/pages/api/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **pages/api** folder files save NextJs' Server side API used throughout the application. can be access by /api/{filename} 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | 4 | 5 | "source.fixAll.eslint": true 6 | }, 7 | "editor.formatOnSave": false, 8 | "editor.formatOnSaveMode": "modifications" 9 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/utils/get-router-param.ts: -------------------------------------------------------------------------------- 1 | const getRouterParam = ( 2 | val: string | string[] | undefined, 3 | defaultVal: string = '', 4 | ):string => (val && Array.isArray(val) ? val.join(',') : val || defaultVal); 5 | 6 | export default getRouterParam; 7 | -------------------------------------------------------------------------------- /src/types/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **types** folder files save shared typescript interfaces and types used throughout the application. 4 | 5 | - If the type/interface only used in one place, then no need to put it here. e.g. the props interface used for the main function. 6 | -------------------------------------------------------------------------------- /src/configs/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **configs** folder files save important configurations used throughout the application. 4 | 5 | - This folder consists of a configuration file where we store environment variables. e.g. We can use this files to set up multi-environment configurations in your application. 6 | -------------------------------------------------------------------------------- /src/utils/get-unique-ary.ts: -------------------------------------------------------------------------------- 1 | const getUniqueAryByKey = (arr: T[], key:string):T[] => { 2 | const filtered = arr.filter( 3 | (v, i, a) => a.findIndex( 4 | (v2) => (v2[key as keyof T] === v[key as keyof T]), 5 | ) === i, 6 | ); 7 | 8 | return filtered; 9 | }; 10 | 11 | export default getUniqueAryByKey; 12 | -------------------------------------------------------------------------------- /src/assets/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/get-id-from-slug.ts: -------------------------------------------------------------------------------- 1 | const getIdFromSlug = ( 2 | slug: string, 3 | from: 'last' | 'first' = 'last', 4 | ):number | null => { 5 | const slugAry = slug.trim().split('-'); 6 | const lastStr = (from === 'last' ? slugAry.pop() : slugAry.shift()) || ''; 7 | const slugId = parseInt(lastStr.trim(), 10); 8 | return Number.isNaN(slugId) ? null : slugId; 9 | }; 10 | 11 | export default getIdFromSlug; 12 | -------------------------------------------------------------------------------- /src/pages/user/profile.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import React from 'react'; 3 | import MainLayout from '../../layouts/MainLayout'; 4 | import ProfileForm from '../../components/ProfileForm'; 5 | import HeadMeta from '../../layouts/HeadMeta'; 6 | 7 | const UserProfile: NextPage = () => ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default UserProfile; 15 | -------------------------------------------------------------------------------- /src/pages/user/recent.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import React from 'react'; 3 | import MainLayout from '../../layouts/MainLayout'; 4 | import RecentItems from '../../components/RecentItems'; 5 | import HeadMeta from '../../layouts/HeadMeta'; 6 | 7 | const UserRecent: NextPage = () => ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default UserRecent; 15 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **components** folder files save react small components used throughout the application. 4 | 5 | - Components are the building blocks of any react project. This folder consists of a collection of UI components like buttons, modals, inputs, loader, etc., that can be used across various files in the project. Each component should consist of a test file to do a unit test as it will be widely used in the project. 6 | -------------------------------------------------------------------------------- /src/assets/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **assets** folder files save assets used throughout the application. 4 | 5 | - it contains assets of our project. It consists of images and styling files. Here we can store our global styles. We are centralizing the project so we can store the page-based or component-based styles over here. But we can even keep style according to the pages folder or component folder also. But that depends on developer conformability. 6 | -------------------------------------------------------------------------------- /src/pages/user/favorite.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import React from 'react'; 3 | import MainLayout from '../../layouts/MainLayout'; 4 | import FavoriteItems from '../../components/FavoriteItems'; 5 | import HeadMeta from '../../layouts/HeadMeta'; 6 | 7 | const UserFavorite: NextPage = () => ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default UserFavorite; 15 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **layouts** folder files save layout components used throughout the application. 4 | 5 | - Contains reusable Layout Components. A Layout Component is a component that composes the layout of a page. It will often import components such as app-bar, app-footer, app-side-nav. 6 | 7 | - If your project is likely to only have a single layout, this directory might not be necessary, and the Layout Component can live in the components directory. 8 | -------------------------------------------------------------------------------- /src/apis/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **apis** folder files save CLIENT SIDE API functions(e.g. code snippets for React Query) used throughout the application. 4 | 5 | - The api directory contains all services that take care of the communication between the React application (frontend) and an API (backend). A single service provides multiple functions to retrieve data from or post data to an external service using the HTTP protocol. 6 | 7 | ## Example files 8 | 9 | - axios.tx 10 | - auth-api.tx 11 | - article-api.tx 12 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | - **utils** folder files have very short and specific functions used throughout functions for other files, e.g. can be used for helper.ts . 4 | 5 | - It is a place where you can place small snippets you can use throughout the application. Small functions to build bigger things with. 6 | 7 | - Utils folder consists of some repeatedly used functions that are commonly used in the project. It should contain only common js functions & objects like dropdown options, regex condition, data formatting, etc. 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | // Note: StrictMode=true will render the components twice only on the dev mode not production. 4 | reactStrictMode: true, 5 | swcMinify: true, 6 | images: { 7 | domains: ['res.cloudinary.com'], 8 | }, 9 | i18n: { 10 | locales: ["en"], 11 | defaultLocale: "en", 12 | }, 13 | 14 | webpack5: true, 15 | webpack: (config) => { 16 | config.resolve.fallback = { fs: false }; 17 | 18 | return config; 19 | }, 20 | }; 21 | 22 | module.exports = nextConfig; 23 | -------------------------------------------------------------------------------- /src/i18n/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **i18n** folder files save internationalization files used throughout the application. 4 | 5 | - i18n stands for internationalization and takes care of the language support of the application. The including JSON files are basically objects contains fixed constants as keys and their associated translations as values. 6 | 7 | - Therefore, the keys should be equal for each language file. Only the values (translations) differ from each other. You can easily query those language files later on by writing your own custom hook or component. 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # get keys from https://cloudinary.com/console 2 | CLOUDINARY_API_KEY="CLOUDINARY_API_KEY" 3 | CLOUDINARY_API_SECRET="CLOUDINARY_API_SECRET" 4 | CLOUDINARY_API_BASE_URL="https://api.cloudinary.com/v1_1/CLOUD_NAME" 5 | CLOUDINARY_API_EAGER="c_limit,w_1600" 6 | CLOUDINARY_API_UPLOAD_URL="https://api.cloudinary.com/v1_1/CLOUD_NAME/image/upload" 7 | 8 | # get keys from https://imagekit.io/dashboard/developer/api-keys 9 | IMAGEKIT_API_PUBLIC_KEY="public_IMAGEKIT_API_PUBLIC_KEY=" 10 | IMAGEKIT_API_PRIVATE_KEY="private_IMAGEKIT_API_PRIVATE_KEY=" 11 | IMAGEKIT_API_END_POINT="https://ik.imagekit.io/YourImagekitID" 12 | IMAGEKIT_API_UPLOAD_URL="https://upload.imagekit.io/api/v1/files/upload" -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CssBaseline from '@mui/material/CssBaseline'; 3 | import Box from '@mui/material/Box'; 4 | import Container from '@mui/material/Container'; 5 | import MainHeader from './MainHeader'; 6 | import MainFooter from './MainFooter'; 7 | import { ChildrenProps } from '../types/article-types'; 8 | 9 | const MainLayout = ({ children }: ChildrenProps) => ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | export default MainLayout; 26 | -------------------------------------------------------------------------------- /src/utils/console-log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { IS_DEV } from '../constants/article-const'; 4 | 5 | /* eslint-disable import/prefer-default-export */ 6 | export const consoleLog = ( 7 | var1: any, 8 | var2: any = 'DEF_VAR_2', 9 | var3: any = 'DEF_VAR_3', 10 | var4: any = 'DEF_VAR_4', 11 | var5: any = 'DEF_VAR_5', 12 | ) => { 13 | if (IS_DEV) { 14 | const newVar1 = typeof var1 === 'string' ? `🚀 🚀 🚀 ${var1}` : var1; 15 | 16 | if (var5 !== 'DEF_VAR_5') { 17 | console.log(newVar1, var2, var3, var4, var5); 18 | } else if (var4 !== 'DEF_VAR_4') { 19 | console.log(newVar1, var2, var3, var4); 20 | } else if (var3 !== 'DEF_VAR_3') { 21 | console.log(newVar1, var2, var3); 22 | } else if (var2 !== 'DEF_VAR_2') { 23 | console.log(newVar1, var2); 24 | } else { 25 | console.log(newVar1); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/apis/user-api.ts: -------------------------------------------------------------------------------- 1 | import { AvatarResponse, UploadFileParams } from '../types/article-types'; 2 | import { uploadFileToCloudinary } from '../utils/upload-to-cloudinary'; 3 | import { uploadFileToImageKit } from '../utils/upload-to-imagekit'; 4 | import { consoleLog } from '../utils/console-log'; 5 | 6 | interface ReactQueryFnProps { 7 | queryKey: [string, T]; 8 | } 9 | 10 | export const uploadAvatar = async ({ file, provider }:UploadFileParams):Promise => { 11 | const uploadRes = provider === 'cloudinary' 12 | ? await uploadFileToCloudinary({ file, folder: 'avatars2', eager: 'c_limit,w_120' }) 13 | : await uploadFileToImageKit({ file, folder: 'avatars3' }); 14 | 15 | consoleLog('🚀 ~ file: user-api.ts ~ line 12 ~ uploadAvatar ~ uploadRes', uploadRes); 16 | return { avatarUrl: `${uploadRes.url}?tr=w-150,c-at_max` }; 17 | }; 18 | 19 | export const reactQueryFn = { 20 | uploadAvatar: async (params:ReactQueryFnProps) 21 | :Promise => uploadAvatar(params.queryKey[1]), 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ActionToaster.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AlertColor, Snackbar, Alert } from '@mui/material'; 3 | 4 | const ActionToaster = ({ 5 | showToaster = false, 6 | setShowToaster, 7 | message = 'Action performed successfully!', 8 | alertColor = 'success', 9 | }: 10 | { showToaster: boolean; 11 | setShowToaster: (showToaster:boolean) => void; 12 | message?:string; 13 | alertColor?: AlertColor; 14 | }) => { 15 | const onCloseSnackbar = (event?: React.SyntheticEvent | Event, reason?: string) => { 16 | if (reason === 'clickaway') { 17 | return; 18 | } 19 | setShowToaster(false); 20 | }; 21 | return ( 22 | 28 | 29 | {message} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ActionToaster; 36 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Zeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { mainConfig } from '../../configs/main-config'; 4 | import { Article } from '../../types/article-types'; 5 | 6 | const fs = require('fs'); 7 | 8 | // type Data = { 9 | // name: string 10 | // }; 11 | 12 | export default function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse, 15 | ) { 16 | const files:string[] = fs.readdirSync(mainConfig.dataFilePath); 17 | if (!mainConfig.isClientSide) { 18 | const allSlugs = files.map((file) => { 19 | if (!file.includes('tag-')) { 20 | return []; 21 | } 22 | const articleData = JSON.parse(fs.readFileSync(`${mainConfig.dataFilePath}/${file}`, 'utf8')); 23 | if (Array.isArray(articleData) && articleData.length && 'id' in articleData[0] && articleData[0].cover_image) { 24 | return articleData.map((article:Article) => `${article.slug}-${article.id}`); 25 | } 26 | 27 | return []; 28 | }); 29 | 30 | res.status(200).json(allSlugs.flat()); 31 | } 32 | 33 | res.status(200).json({ name: 'John Doe' }); 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import React from 'react'; 3 | import Typography from '@mui/material/Typography'; 4 | import Box from '@mui/material/Box'; 5 | import Link from 'next/link'; 6 | import MainLayout from '../layouts/MainLayout'; 7 | import HeadMeta from '../layouts/HeadMeta'; 8 | import SearchBar from '../components/SearchBar'; 9 | 10 | const Home:NextPage = () => ( 11 | 12 | 16 | 17 | Welcome to Next.js! 18 | 19 | 20 | 21 | React Articles 22 | 23 | 24 | 25 | 26 | 27 | 28 | The scaffold for NextJs 12.x,Redux Toolkit,Redux Saga,React Query,React Hook Form, 29 | Material UI(mui),Typescript and ESLint 30 | 31 | 32 | ); 33 | 34 | export default Home; 35 | -------------------------------------------------------------------------------- /src/services/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **services** directory is less essential than components, but if you're making a plain JavaScript module that the rest of the application is using, it can be handy. 4 | 5 | - Contains reusable code for interacting with an API, often in the form of hooks, and ideally utilizing a server-cache tool like React Query or RTK Query. 6 | 7 | - Do not mistake this with an Angular-esque service which is meant to handle injecting functionality into components. React handles this scenario using contexts and hooks. This directory should only contain code for interacting with an API. 8 | 9 | - Inspired by RTK Query’s recommendation to keep your API definition in a central location. This is the only example of where we purposely break the local-first rule. I like to think of API definitions as their own modular feature. In fact it’s not uncommon to have a features/api directory in lieu of a services directory. 10 | “Our perspective is that it’s much easier to keep track of how requests, cache invalidation, and general app configuration behave when they’re all in one central location in comparison to having X number of custom hooks in different files throughout your application.” — RTK 11 | -------------------------------------------------------------------------------- /src/constants/article-const.ts: -------------------------------------------------------------------------------- 1 | export const SITE_NAME = 'NextJs Redux Starter'; 2 | 3 | export const BASE_API_URI = 'https://dev.to/api'; 4 | 5 | export const ITEMS_PER_PAGE = 30; 6 | 7 | export const DEFAULT_KEYWORD = 'React'; 8 | 9 | export const DEFAULT_IMAGE_URL = 'https://via.placeholder.com/960x400.png?text='; 10 | 11 | export const TOP_MENU_TAGS = ['Redux', 'ReactNative', 'MUI', 'Typescript', 'NextJs', 'ReactQuery', 'ESLint', 'Javascript', 'Jest', 'Node', 'SEO', 'AWS', 'React', 'CSS']; 12 | 13 | export const USER_MENU_LINKS = [ 14 | { title: 'My Favorites', url: '/user/favorite' }, 15 | { title: 'Recent Viewed', url: '/user/recent' }, 16 | { title: 'Change Profile', url: '/user/profile' }, 17 | { title: 'Logout ', url: '/user/favorite' }, 18 | ]; 19 | 20 | export const PROFILE_STAR_LABELS: { [index: number]: string } = { 21 | 0.5: 'Useless', 22 | 1 : 'Useless+', 23 | 1.5: 'Poor', 24 | 2 : 'Poor+', 25 | 2.5: 'Ok', 26 | 3 : 'Ok+', 27 | 3.5: 'Good', 28 | 4 : 'Good+', 29 | 4.5: 'Excellent', 30 | 5 : 'Excellent+', 31 | }; 32 | 33 | export const PROFILE_SLIDER_MARKS = [ 34 | { 35 | value: 1, 36 | label: '1 year', 37 | }, 38 | { 39 | value: 5, 40 | label: '5 year', 41 | }, 42 | { 43 | value: 10, 44 | label: '10 years', 45 | }, 46 | ]; 47 | 48 | export const IS_DEV = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; 49 | -------------------------------------------------------------------------------- /src/redux/features/README.md: -------------------------------------------------------------------------------- 1 | # What's this folder used for? 2 | 3 | **features** Within a given feature folder, the Redux logic for that feature should be written as a single "slice" file, preferably using the Redux Toolkit createSlice API. 4 | 5 | 6 | - Structure Files as Feature Folders with Single-File Logic 7 | Redux itself does not care about how your application's folders and files are structured. However, co-locating logic for a given feature in one place typically makes it easier to maintain that code. 8 | 9 | - Because of this, we recommend that most applications should structure files using a "feature folder" approach (all files for a feature in the same folder). Within a given feature folder, the Redux logic for that feature should be written as a single "slice" file, preferably using the Redux Toolkit createSlice API. (This is also known as the "ducks" pattern). While older Redux codebases often used a "folder-by-type" approach with separate folders for "actions" and "reducers", keeping related logic together makes it easier to find and update that code. 10 | 11 | 12 | - /features has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts is a "duck"-style file that contains a call to RTK's createSlice() function, and exports the slice reducer and action creators. 13 | 14 | - Doc: https://redux.js.org/style-guide/#structure-files-as-feature-folders-with-single-file-logic 15 | -------------------------------------------------------------------------------- /public/savedData/article-1246659.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246659,"title":"Hi","description":"new here, learning about DEV so i can work :)","readable_publish_date":"Nov 7","slug":"hi-48j1","path":"/saymon_lacerda/hi-48j1","url":"https://dev.to/saymon_lacerda/hi-48j1","comments_count":1,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T15:55:08Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1246659.png","canonical_url":"https://dev.to/saymon_lacerda/hi-48j1","created_at":"2022-11-07T15:55:09Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T15:55:08Z","last_comment_at":"2022-11-07T20:12:39Z","reading_time_minutes":1,"tag_list":"","tags":[],"body_html":"

new here, learning about DEV so i can work :)

\n\n","body_markdown":"new here, learning about DEV so i can work :)","user":{"name":"saymon","username":"saymon_lacerda","twitter_username":"saymon_shanks","github_username":null,"user_id":967860,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--Bz5eR8F3--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/967860/637a0cde-3de2-4f21-a130-3d26b0add91b.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--k2KpGM-1--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/967860/637a0cde-3de2-4f21-a130-3d26b0add91b.jpg"}} -------------------------------------------------------------------------------- /src/redux/features/reducer.ts: -------------------------------------------------------------------------------- 1 | import { HYDRATE } from 'next-redux-wrapper'; 2 | import { combineReducers } from '@reduxjs/toolkit'; 3 | import type { AnyAction, CombinedState } from '@reduxjs/toolkit'; 4 | import { mainConfig } from '../../configs/main-config'; 5 | 6 | import userSlice from './userSlice'; 7 | import articleSlice from './articleSlice'; 8 | 9 | interface ReducerState { 10 | [userSlice.name] : ReturnType; 11 | [articleSlice.name]: ReturnType; 12 | } 13 | 14 | const sliceReducer = { 15 | [userSlice.name] : userSlice.reducer, 16 | [articleSlice.name]: articleSlice.reducer, 17 | }; 18 | 19 | const rootReducer = (state: any, action: AnyAction): CombinedState => { 20 | // Doc: https://github.com/kirill-konshin/next-redux-wrapper#state-reconciliation-during-hydration 21 | if (action.type === HYDRATE) { 22 | // consoleLog('🚀 ~ file: reducer.ts ~ line 20 ~ from hydration', action, mainConfig.isClientSide, state); 23 | 24 | const nextState = { 25 | ...state, // use previous state 26 | ...action.payload, // apply delta from hydration 27 | }; 28 | 29 | nextState.user = state.user; // preserve user data on client side navigation 30 | 31 | // if (mainConfig.isClientSide) { 32 | // nextState.article = state.article; 33 | // } 34 | 35 | return nextState; 36 | } 37 | 38 | return combineReducers(sliceReducer)(state, action); 39 | }; 40 | 41 | export default rootReducer; 42 | -------------------------------------------------------------------------------- /public/savedData/article-1245732.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1245732,"title":"#How to launch an EC2 instance using Terraform","description":"Test!!!","readable_publish_date":"Nov 6","slug":"how-to-launch-an-ec2-instance-using-terraform-1pfp","path":"/ramanan23/how-to-launch-an-ec2-instance-using-terraform-1pfp","url":"https://dev.to/ramanan23/how-to-launch-an-ec2-instance-using-terraform-1pfp","comments_count":0,"public_reactions_count":2,"collection_id":null,"published_timestamp":"2022-11-06T16:53:50Z","positive_reactions_count":2,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1245732.png","canonical_url":"https://dev.to/ramanan23/how-to-launch-an-ec2-instance-using-terraform-1pfp","created_at":"2022-11-06T16:53:51Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-06T16:53:50Z","last_comment_at":"2022-11-06T16:53:50Z","reading_time_minutes":1,"tag_list":"aws","tags":["aws"],"body_html":"

Test!!!

\n\n","body_markdown":"Test!!!","user":{"name":"ramanan23","username":"ramanan23","twitter_username":null,"github_username":"ramanan23","user_id":966968,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--HOiw2NOB--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/966968/6686a4a7-8cb1-424d-8b8e-adef1fc0881b.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--nrZotPAk--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/966968/6686a4a7-8cb1-424d-8b8e-adef1fc0881b.png"}} -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021 : true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'next/core-web-vitals', 9 | 'airbnb', 10 | 'airbnb-typescript', 11 | ], 12 | parser : '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | tsx: true, 17 | }, 18 | ecmaVersion: 'latest', 19 | sourceType : 'module', 20 | project: ['./tsconfig.json'], 21 | }, 22 | plugins: [ 23 | 'react', 24 | '@typescript-eslint', 25 | 'align-assignments', 26 | ], 27 | rules: { 28 | 'react/jsx-filename-extension' : [1, { extensions: ['.tsx', '.jsx'] }], 29 | 'react/function-component-definition': [1, { 30 | namedComponents : 'arrow-function', 31 | unnamedComponents: 'arrow-function', 32 | }], 33 | 'max-lines': [ 34 | 'warn', 35 | { max: 300, skipBlankLines: true, skipComments: true }, 36 | ], 37 | indent : ['error', 2, { VariableDeclarator: { var: 2, let: 2, const: 3 } }], 38 | 'no-multi-spaces' : ['error', { exceptions: { VariableDeclarator: true, Property: true } }], 39 | 'align-assignments/align-assignments': [2, { requiresOnly: false }], 40 | 'key-spacing' : ['error', { align: 'colon' }], 41 | 'no-param-reassign': ["error", { "props": false }], 42 | "react/prop-types": "off" , 43 | "react/require-default-props": "off", 44 | "no-unused-vars": "off", 45 | "@typescript-eslint/no-unused-vars": ["error"], 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { Box, InputAdornment, TextField } from '@mui/material'; 4 | import { Search } from '@mui/icons-material'; 5 | import CloseIcon from '@mui/icons-material/Close'; 6 | import { useDebounce } from 'react-use'; 7 | 8 | const SearchBar = () => { 9 | const nextJsRouter = useRouter(); 10 | const [keyword, setKeyword] = useState(''); 11 | 12 | const onKeywordChange = () => { 13 | const trimmedKeyword = keyword.trim(); 14 | if (trimmedKeyword.length > 1) { 15 | const keywordUpcase = trimmedKeyword.charAt(0).toUpperCase() + trimmedKeyword.slice(1); 16 | nextJsRouter.push(`/articles/tag/${keywordUpcase}`); 17 | } 18 | }; 19 | 20 | useDebounce(onKeywordChange, 800, [keyword]); 21 | 22 | return ( 23 | 24 | 25 | 32 | {keyword 33 | ? setKeyword('')} sx={{ cursor: 'pointer' }} /> 34 | : } 35 | 36 | ), 37 | }} 38 | variant="standard" 39 | value={keyword} 40 | onChange={(e: React.ChangeEvent) => { 41 | setKeyword(e.target.value); 42 | }} 43 | /> 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default SearchBar; 50 | -------------------------------------------------------------------------------- /src/layouts/MainFooter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import BottomNavigation from '@mui/material/BottomNavigation'; 3 | import BottomNavigationAction from '@mui/material/BottomNavigationAction'; 4 | import RestoreIcon from '@mui/icons-material/Restore'; 5 | import FavoriteIcon from '@mui/icons-material/Favorite'; 6 | import HomeIcon from '@mui/icons-material/Home'; 7 | import styled from '@emotion/styled'; 8 | import router from 'next/router'; 9 | import { Paper } from '@mui/material'; 10 | 11 | const MainFooter = () => { 12 | const StyledBottomNavigationAction = styled(BottomNavigationAction)(` 13 | color: #7e235a; 14 | &.Mui-selected { 15 | color: red; 16 | }; 17 | `); 18 | 19 | const onIconClick = (url:string) => { 20 | router.push(url); 21 | }; 22 | 23 | return ( 24 | 30 | 31 | 32 | } 35 | onClick={() => onIconClick('/')} 36 | /> 37 | } 40 | onClick={() => onIconClick('/user/favorite')} 41 | /> 42 | } 45 | onClick={() => onIconClick('/user/recent')} 46 | /> 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default MainFooter; 54 | -------------------------------------------------------------------------------- /src/configs/main-config.ts: -------------------------------------------------------------------------------- 1 | import { IS_DEV } from '../constants/article-const'; 2 | 3 | const isClientSide = typeof window !== 'undefined'; 4 | 5 | export const mainConfig = { 6 | // set to false will not fetch the dynamic data from the client side 7 | // then we can see what's the NextJs static pages looks like - for SEO 8 | isStaticPageDebugDisabled: true, 9 | 10 | // set to false will use React Query for getStaticProps instead of use Redux 11 | // client side still check ReduxState first, then use React Query 12 | isReduxForStaticPropsEnabled: true, 13 | 14 | // Redux PersistConfig props 15 | // set enabled to false can disable persist (requires remove PersistGate in _app.tx) 16 | reduxPersistConfigs: { 17 | key : 'AlexAppData', 18 | version: 0.4, 19 | debug : IS_DEV, 20 | enabled: true, 21 | }, 22 | 23 | // max number of recent visited articles to save in local storage 24 | maxRecentItems: 40, 25 | 26 | // article list page re-generate every xxx seconds 27 | listPageRefreshInterval: IS_DEV ? 600 : 3600 * 24 * 2, 28 | 29 | // article detail page re-generate every xxx seconds 30 | detailPageRefreshInterval: IS_DEV ? 600 : 3600 * 24 * 30, 31 | 32 | // is dev env, we can force it to true to debug -> isDevEnv:true 33 | isDevEnv: IS_DEV, 34 | 35 | // is client side, we can force it to true to debug -> isClientSide:true 36 | isClientSide, 37 | 38 | // cached json file path from api 39 | dataFilePath: './public/savedData', 40 | 41 | // URI for the site 42 | siteUri: IS_DEV ? 'http://localhost:3000' : 'https://redux.10168.tech', 43 | }; 44 | 45 | export default mainConfig; 46 | -------------------------------------------------------------------------------- /src/components/FavoriteItems.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button, 4 | } from '@mui/material'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import FavoriteIcon from '@mui/icons-material/Favorite'; 7 | import type { FavoriteItem, UserSliceType } from '../types/article-types'; 8 | import { ReduxState } from '../redux/store'; 9 | import userSlice from '../redux/features/userSlice'; 10 | import { PageBreadcrumbs, PageListItems } from './RecentItems'; 11 | import ActionToaster from './ActionToaster'; 12 | 13 | const FavoriteItems = () => { 14 | const reduxDispatch = useDispatch(); 15 | 16 | const reduxUserData:UserSliceType = useSelector((reduxState: ReduxState) => reduxState.user); 17 | 18 | const dataItems = reduxUserData.favoriteItems; 19 | 20 | const [showToaster, setShowToaster] = React.useState(false); 21 | 22 | const onClickFavorite = (item: FavoriteItem) => { 23 | reduxDispatch(userSlice.actions.favoriteItemRequest(item)); 24 | setShowToaster(true); 25 | }; 26 | 27 | return ( 28 | <> 29 | 30 | 33 | 34 | 35 | 40 | 41 | 47 | 48 | ); 49 | }; 50 | 51 | export default FavoriteItems; 52 | -------------------------------------------------------------------------------- /src/pages/api/imagekit-auth.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import ImageKit from 'imagekit'; 3 | 4 | /** 5 | * 6 | * API endpoint: http://localhost:3000/api/imagekit-auth 7 | * 8 | * ============ Example of .env.local ============ 9 | * IMAGEKIT_API_PUBLIC_KEY="public_4TznVN3Qst1FY51fJL8=" 10 | * IMAGEKIT_API_PRIVATE_KEY="private_GDcEsLRTRpMvf5t1WII=" 11 | * IMAGEKIT_API_END_POINT="https://ik.imagekit.io/ksos9gww" 12 | * IMAGEKIT_API_UPLOAD_URL="https://upload.imagekit.io/api/v1/files/upload" 13 | */ 14 | 15 | export interface ImageKitAuthType { 16 | token: string; 17 | expire: number; 18 | signature: string; 19 | publicKey: string ; 20 | urlEndpoint: string; 21 | uploadUrl: string ; 22 | } 23 | 24 | const imageKitAuth = ( 25 | req: NextApiRequest, 26 | res: NextApiResponse, 27 | ) => { 28 | const keys = { 29 | publicKey : process.env.IMAGEKIT_API_PUBLIC_KEY || 'Missing IMAGEKIT_API_PUBLIC_KEY', 30 | privateKey : process.env.IMAGEKIT_API_PRIVATE_KEY || 'Missing IMAGEKIT_API_PRIVATE_KEY', 31 | urlEndpoint: process.env.IMAGEKIT_API_END_POINT || 'Missing IMAGEKIT_API_END_POINT', 32 | uploadUrl : process.env.IMAGEKIT_API_UPLOAD_URL || 'Missing IMAGEKIT_API_UPLOAD_URL', 33 | }; 34 | const imagekit = new ImageKit(keys); 35 | const auth = imagekit.getAuthenticationParameters(); 36 | const rs = { ...keys, ...auth, ...{ privateKey: undefined } }; 37 | res.status(200).json(rs); 38 | }; 39 | 40 | export default function handler( 41 | req: NextApiRequest, 42 | res: NextApiResponse, 43 | ) { 44 | if (req.method !== 'POST') { 45 | res.status(405); 46 | return; 47 | } 48 | imageKitAuth(req, res); 49 | } 50 | -------------------------------------------------------------------------------- /public/savedData/article-1232816.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1232816,"title":"What Is SEO / Search Engine Optimization?","description":"A post by Omnia Ahmed","readable_publish_date":"Oct 27","slug":"what-is-seo-search-engine-optimization-f5n","path":"/topweb4all/what-is-seo-search-engine-optimization-f5n","url":"https://dev.to/topweb4all/what-is-seo-search-engine-optimization-f5n","comments_count":0,"public_reactions_count":2,"collection_id":null,"published_timestamp":"2022-10-27T16:51:03Z","positive_reactions_count":2,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1232816.png","canonical_url":"https://dev.to/topweb4all/what-is-seo-search-engine-optimization-f5n","created_at":"2022-10-27T16:51:03Z","edited_at":null,"crossposted_at":null,"published_at":"2022-10-27T16:51:03Z","last_comment_at":"2022-10-27T16:51:03Z","reading_time_minutes":1,"tag_list":"seo, blogger","tags":["seo","blogger"],"body_html":"

\n\n","body_markdown":"[](https://www.topweb4all.com/2022/09/what-is-seo-search-engine-optimization.html)","user":{"name":"Omnia Ahmed","username":"topweb4all","twitter_username":null,"github_username":"topweb4all","user_id":957301,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--bmP9LGrb--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/957301/a8ef3aba-229d-4018-94d1-d6aaa493cfb9.jpeg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--Dhovm90Z--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/957301/a8ef3aba-229d-4018-94d1-d6aaa493cfb9.jpeg"}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2022-2023 React medium-to-large project scaffold 2 | 3 | This is a scaffold for React medium-to-large project. 4 | 5 | ## Main modules 6 | 7 | * React SSR framework [Next.js](https://nextjs.org/). 8 | 9 | * Central **client side** state management [Redux](https://redux.js.org/). 10 | * Redux Toolkit [Redux Toolkit](https://redux-toolkit.js.org/). 11 | * Async middleware for Redux [Redux-Saga](https://redux-saga.js.org/). 12 | * Next and Redux connector [next-redux-wrapper](https://github.com/kirill-konshin/next-redux-wrapper). 13 | * Redux Persist - Persist and rehydrate a redux store. save to localStorage by default. [redux-persist](https://github.com/rt2zz/redux-persist) 14 | 15 | * React Query for **server side** query [react-query](https://tanstack.com/query/v4/docs/adapters/react-query) 16 | * Form library [React Hook Form](https://react-hook-form.com/). 17 | 18 | * MUI design system [Material UI](https://Mui.com/). 19 | * Translation and i18n [next-translate](https://github.com/vinissimus/next-translate). 20 | * Date-fns (similar to MomentJs) [date-fns](https://date-fns.org/) 21 | 22 | * Typescript [TypeScript](https://www.typescriptlang.org/). 23 | * Eslint [ESLint](https://eslint.org/). 24 | 25 | ## Code conduct & typescript 26 | 27 | * Enhances Airbnb's ESLint config with TypeScript support [eslint-airbnb-typescript](https://github.com/iamturns/eslint-config-airbnb-typescript) 28 | 29 | ## Deploy on Vercel 30 | 31 | 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. 32 | 33 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-redux-saga-mui-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.9.3", 13 | "@emotion/styled": "^11.9.3", 14 | "@mui/icons-material": "^5.8.4", 15 | "@mui/material": "^5.9.1", 16 | "@reduxjs/toolkit": "^1.8.3", 17 | "@tanstack/react-query": "^4.0.5", 18 | "@tanstack/react-query-devtools": "^4.0.5", 19 | "axios": "^0.27.2", 20 | "date-fns": "^2.29.1", 21 | "dayjs": "^1.11.5", 22 | "imagekit": "^4.1.0", 23 | "next": "^12.2.2", 24 | "next-redux-wrapper": "^7.0.5", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-hook-form": "^7.33.1", 28 | "react-redux": "^8.0.2", 29 | "react-use": "^17.4.0", 30 | "redux-persist": "^6.0.0", 31 | "redux-saga": "^1.1.3" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^18.0.6", 35 | "@types/react": "^18.0.15", 36 | "@types/react-dom": "^18.0.6", 37 | "@types/react-redux": "^7.1.24", 38 | "@typescript-eslint/eslint-plugin": "^5.30.7", 39 | "@typescript-eslint/parser": "^5.30.7", 40 | "eslint": "^8.20.0", 41 | "eslint-config-airbnb": "^19.0.4", 42 | "eslint-config-airbnb-typescript": "^17.0.0", 43 | "eslint-config-next": "^12.2.2", 44 | "eslint-config-prettier": "8.5.0", 45 | "eslint-plugin-align-assignments": "^1.1.2", 46 | "eslint-plugin-import": "^2.26.0", 47 | "eslint-plugin-jsx-a11y": "^6.6.0", 48 | "eslint-plugin-react": "^7.30.1", 49 | "eslint-plugin-react-hooks": "^4.6.0", 50 | "prettier": "2.7.1", 51 | "typescript": "^4.7.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/api/article-detail.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { mainConfig } from '../../configs/main-config'; 4 | import { BASE_API_URI } from '../../constants/article-const'; 5 | import { consoleLog } from '../../utils/console-log'; 6 | import { Article } from '../../types/article-types'; 7 | 8 | const fs = require('fs'); 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse
, 13 | ) { 14 | const id = req.query.id as string; 15 | const jsonFile = `${mainConfig.dataFilePath}/article-${id}.json`; 16 | try { 17 | // read data from local json file first 18 | const fsStats = fs.statSync(jsonFile, { throwIfNoEntry: false }); 19 | if (fsStats && 'mtime' in fsStats) { 20 | // consoleLog('fs stats', fsStats); 21 | const articleData = JSON.parse(fs.readFileSync(jsonFile, 'utf8')); 22 | if ('id' in articleData) { 23 | consoleLog('🚀 fetch article data form local:', jsonFile); 24 | res.status(200).json(articleData as Article); 25 | return; 26 | } 27 | } 28 | 29 | const rs = await fetch(`${BASE_API_URI}/articles/${id}`); 30 | const data = await rs.json(); 31 | 32 | // save data to json file 33 | if (('id' in data) && 'body_html' in data) { 34 | fs.writeFileSync(jsonFile, JSON.stringify(data), 'utf8'); 35 | consoleLog('🚀 Save article data to local:', jsonFile); 36 | 37 | res.status(200).json(data as Article); 38 | return; 39 | } 40 | 41 | consoleLog('🚀 wrong article formate', data); 42 | } catch (error) { 43 | consoleLog('🚀 ~ file: article-detail.ts ~ line 41 ~ error', error); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, configureStore, ThunkAction, Store, 3 | } from '@reduxjs/toolkit'; 4 | import createSagaMiddleware, { Task } from 'redux-saga'; 5 | import { createWrapper } from 'next-redux-wrapper'; 6 | import { persistStore, persistReducer, Persistor } from 'redux-persist'; 7 | import storage from 'redux-persist/lib/storage'; 8 | import { mainConfig } from '../configs/main-config'; 9 | import rootSaga from './saga'; 10 | import rootReducer from './features/reducer'; 11 | 12 | export interface SagaStore extends Store { 13 | sagaTask: Task; 14 | reduxPersistData: Persistor; 15 | } 16 | 17 | let newRootReducer = rootReducer; 18 | 19 | export const enableReduxPersist = mainConfig.reduxPersistConfigs.enabled && mainConfig.isClientSide; 20 | 21 | if (enableReduxPersist) { 22 | newRootReducer = persistReducer({ 23 | ...mainConfig.reduxPersistConfigs, 24 | storage, 25 | }, rootReducer); 26 | } 27 | 28 | const createReduxStore = ():SagaStore => { 29 | const sagaMiddleware = createSagaMiddleware(); 30 | const store = configureStore({ 31 | reducer : newRootReducer, 32 | middleware: [sagaMiddleware], 33 | devTools : mainConfig.isDevEnv, 34 | }); 35 | 36 | (store as SagaStore).sagaTask = sagaMiddleware.run(rootSaga); 37 | 38 | (store as SagaStore).reduxPersistData = enableReduxPersist 39 | ? persistStore(store) 40 | : persistStore(store); 41 | 42 | return store as SagaStore; 43 | }; 44 | 45 | export type ReduxStore = ReturnType; 46 | export type ReduxState = ReturnType; 47 | export type ReduxThunk = ThunkAction; 48 | 49 | export const reduxWrapper = createWrapper(createReduxStore, { 50 | debug: mainConfig.isDevEnv, 51 | }); 52 | -------------------------------------------------------------------------------- /src/layouts/HeadMeta.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import { useRouter } from 'next/router'; 4 | import { Article, PageMeta } from '../types/article-types'; 5 | import { SITE_NAME } from '../constants/article-const'; 6 | 7 | interface Props extends PageMeta { 8 | article?: Article; 9 | } 10 | 11 | const HeadMeta = ({ 12 | title, 13 | url, 14 | article, 15 | description = article?.description ? article.description : undefined, 16 | type = article?.title ? 'article' : 'website', 17 | image = article?.social_image ? article.social_image : undefined, 18 | keywords = Array.isArray(article?.tags) ? article?.tags : undefined, 19 | locale = 'en', 20 | }: Props) => { 21 | const { asPath } = useRouter(); 22 | const siteName = SITE_NAME; 23 | const pageTitle = title ? `${title} - ${siteName}` : siteName; 24 | const currentUrl = `${url || asPath}`; 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | {pageTitle} 32 | {description && } 33 | {keywords && } 34 | 35 | {/* Open Graph */} 36 | {url && } 37 | {image && } 38 | {siteName && } 39 | {title && } 40 | {description && } 41 | {type && } 42 | {locale && } 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default HeadMeta; 49 | -------------------------------------------------------------------------------- /src/utils/upload-to-imagekit.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ImageKitAuthType } from '../pages/api/imagekit-auth'; 3 | 4 | export interface ImagekitResType { 5 | fileId: string; 6 | filePath: string; 7 | fileType: string; 8 | width: number; 9 | height: number; 10 | name: string; 11 | size: number; 12 | thumbnailUrl: string; 13 | url: string; 14 | versionInfo: { 15 | id: string; 16 | name: string; 17 | }; 18 | file: File; 19 | folder: string; 20 | filename: string; 21 | } 22 | 23 | export interface UploadFileToImageKitProps { 24 | file: File; 25 | folder?: string; 26 | fileName?: string; 27 | auth?:ImageKitAuthType; 28 | authApiEndpoint?:string; 29 | } 30 | export const uploadFileToImageKit = async ({ 31 | file, folder = 'avatars', fileName, auth, 32 | authApiEndpoint = '/api/imagekit-auth', 33 | }:UploadFileToImageKitProps):Promise => { 34 | let imageKitAuth = auth; 35 | if (!auth) { 36 | const authRes = await axios.post(authApiEndpoint); 37 | 38 | imageKitAuth = authRes.data as ImageKitAuthType; 39 | } 40 | if (!imageKitAuth?.token) { 41 | throw new Error('Get imageKitAuth failed'); 42 | } 43 | 44 | const formData = new FormData(); 45 | formData.append('file', file); 46 | formData.append('fileName', fileName || file.name); 47 | formData.append('folder', folder); 48 | formData.append('publicKey', imageKitAuth.publicKey); 49 | formData.append('signature', imageKitAuth.signature); 50 | formData.append('expire', imageKitAuth.expire.toString()); 51 | formData.append('token', imageKitAuth.token); 52 | 53 | const uploadRes = await axios.post(imageKitAuth.uploadUrl, formData); 54 | if (uploadRes.status !== 200) { 55 | throw new Error('Upload failed'); 56 | } 57 | return { 58 | ...uploadRes.data, file, folder, fileName, 59 | } as ImagekitResType; 60 | }; 61 | 62 | export default uploadFileToImageKit; 63 | -------------------------------------------------------------------------------- /public/savedData/article-1228953.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1228953,"title":"Get Unlimited Keyword Ideas With Free Keyword Finder","description":"https://www.mastertechdz.online/2022/10/get-unlimited-keyword-ideas-with-free.html","readable_publish_date":"Oct 25","slug":"get-unlimited-keyword-ideas-with-free-keyword-finder-4j4f","path":"/chadi28/get-unlimited-keyword-ideas-with-free-keyword-finder-4j4f","url":"https://dev.to/chadi28/get-unlimited-keyword-ideas-with-free-keyword-finder-4j4f","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-10-25T05:33:49Z","positive_reactions_count":3,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1228953.png","canonical_url":"https://dev.to/chadi28/get-unlimited-keyword-ideas-with-free-keyword-finder-4j4f","created_at":"2022-10-25T05:33:50Z","edited_at":null,"crossposted_at":null,"published_at":"2022-10-25T05:33:49Z","last_comment_at":"2022-10-25T05:33:49Z","reading_time_minutes":1,"tag_list":"seo, adsense, blogger, design","tags":["seo","adsense","blogger","design"],"body_html":"

https://www.mastertechdz.online/2022/10/get-unlimited-keyword-ideas-with-free.html

\n\n","body_markdown":"https://www.mastertechdz.online/2022/10/get-unlimited-keyword-ideas-with-free.html","user":{"name":"enterment world","username":"chadi28","twitter_username":null,"github_username":null,"user_id":954381,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--MJBTXZi_--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/954381/09922cb0-e998-4e24-af8a-a4a9e3bd8d48.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--vhKsWsdK--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/954381/09922cb0-e998-4e24-af8a-a4a9e3bd8d48.png"}} -------------------------------------------------------------------------------- /src/redux/features/articleSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { Article, ArticleFilterParams, ArticleSliceType } from '../../types/article-types'; 3 | import getUniqueAryByKey from '../../utils/get-unique-ary'; 4 | 5 | const initialState: ArticleSliceType = { 6 | lists : [], 7 | status : '', 8 | detail : null, 9 | searchTag: '', 10 | }; 11 | 12 | const articleSlice = createSlice({ 13 | name : 'article', 14 | initialState, 15 | reducers: { 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | getArticlesRequest: (state, action: PayloadAction) => { 18 | // consoleLog('🚀 ~ file: articleSlice.ts ~ line 16 ~ action', action); 19 | state.status = 'loading'; 20 | 21 | state.searchTag = action.payload.tag as string; 22 | }, 23 | getArticlesSuccess: (state, action: PayloadAction<{ 24 | data:Article[], params:ArticleFilterParams 25 | }>) => { 26 | const { data, params } = action.payload; 27 | // consoleLog('🚀 ~ file: articleSlice.ts ~ line 23 ~ params', params); 28 | 29 | state.lists = params.page && params.page > 1 30 | ? getUniqueAryByKey([...state.lists, ...data], 'id') 31 | : data; 32 | 33 | state.status = 'loaded'; 34 | }, 35 | getArticlesFailure: (state, action: PayloadAction) => { 36 | // eslint-disable-next-line no-multi-spaces 37 | state.status = 'error'; 38 | state.message = action.payload; 39 | }, 40 | getArticleDetailRequest: (state, action: PayloadAction<{ id:number }>) => { 41 | state.detail = state.lists.find((article) => article.id === action.payload.id) || null; 42 | state.status = 'loading'; 43 | }, 44 | getArticleDetailSuccess: (state, action: PayloadAction
) => { 45 | state.detail = action.payload; 46 | state.status = 'loaded'; 47 | }, 48 | }, 49 | }); 50 | 51 | export default articleSlice; 52 | -------------------------------------------------------------------------------- /public/savedData/article-1213427.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1213427,"title":"React-Naitve with Android Studio","description":"I wanted to know if I can create Splash Screen in Android studio and rest of functionality in...","readable_publish_date":"Oct 7","slug":"react-naitve-with-android-studio-23d3","path":"/anukool42/react-naitve-with-android-studio-23d3","url":"https://dev.to/anukool42/react-naitve-with-android-studio-23d3","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-10-07T08:25:08Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1213427.png","canonical_url":"https://dev.to/anukool42/react-naitve-with-android-studio-23d3","created_at":"2022-10-07T08:25:08Z","edited_at":null,"crossposted_at":null,"published_at":"2022-10-07T08:25:08Z","last_comment_at":"2022-10-07T08:25:08Z","reading_time_minutes":1,"tag_list":"reactnative, android","tags":["reactnative","android"],"body_html":"

I wanted to know if I can create Splash Screen in Android studio and rest of functionality in ReactNative. Is it possible to develop some functionality using Android studio while using ReactNative.

\n\n","body_markdown":"I wanted to know if I can create Splash Screen in Android studio and rest of functionality in ReactNative. Is it possible to develop some functionality using Android studio while using ReactNative.","user":{"name":"Anukul B","username":"anukool42","twitter_username":null,"github_username":"anukool42","user_id":638552,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--8qgynxTJ--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/638552/f29b3fb2-ec5e-42ed-97d4-56db415f74a0.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--hLATE8zh--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/638552/f29b3fb2-ec5e-42ed-97d4-56db415f74a0.png"}} -------------------------------------------------------------------------------- /src/apis/article-api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { mainConfig } from '../configs/main-config'; 3 | import { ITEMS_PER_PAGE } from '../constants/article-const'; 4 | import { Article, ArticleFilterParams, ArticleDetailParams } from '../types/article-types'; 5 | import { consoleLog } from '../utils/console-log'; 6 | 7 | interface ReactQueryFnProps { 8 | queryKey: [string, T]; 9 | } 10 | 11 | export const getArticles = async ({ tag = 'react', page = 1 }:ArticleFilterParams):Promise => { 12 | const apiEndpoint = `${mainConfig.siteUri}/api/article-tag?tag=${tag}&page=${page}&per_page=${ITEMS_PER_PAGE}`; 13 | 14 | try { 15 | const response = await axios.get(apiEndpoint); 16 | 17 | return response.data; 18 | } catch (error) { 19 | consoleLog('🚀 ~ file: article-api.ts ~ line 46 ~ getArticles ~ apiEndpoint', apiEndpoint, error); 20 | if (axios.isAxiosError(error)) { 21 | if (error.code === 'ECONNABORTED') { 22 | // Do something for timeout ... 23 | } 24 | } 25 | } 26 | return []; 27 | }; 28 | 29 | export const getArticleDetail = async ({ id }:ArticleDetailParams):Promise
=> { 30 | const apiEndpoint = `${mainConfig.siteUri}/api/article-detail?id=${id}`; 31 | try { 32 | const response = await axios.get(apiEndpoint); 33 | 34 | return response.data; 35 | } catch (error) { 36 | consoleLog('🚀 ~ file: article-api.ts ~ line 27 ~ getArticleDetail ~ apiEndpoint', apiEndpoint, error); 37 | if (axios.isAxiosError(error)) { 38 | if (error.code === 'ECONNABORTED') { 39 | // Do something for timeout ... 40 | } 41 | } 42 | } 43 | 44 | return null; 45 | }; 46 | 47 | export const reactQueryFn = { 48 | getArticles: async (params:ReactQueryFnProps) 49 | :Promise => getArticles(params.queryKey[1]), 50 | 51 | getArticleDetail: async (params:ReactQueryFnProps) 52 | :Promise
=> getArticleDetail(params.queryKey[1]), 53 | }; 54 | -------------------------------------------------------------------------------- /public/savedData/article-1053472.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1053472,"title":"How to control ReactJS MUI table column height and width","description":"Recently I used ReactJS MUI Table component, but I feel it's height is too large for my project...","readable_publish_date":"Apr 13","slug":"how-to-control-reactjs-mui-table-column-height-and-width-4f2n","path":"/shyamoec/how-to-control-reactjs-mui-table-column-height-and-width-4f2n","url":"https://dev.to/shyamoec/how-to-control-reactjs-mui-table-column-height-and-width-4f2n","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-04-13T04:27:54Z","positive_reactions_count":3,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1053472.png","canonical_url":"https://dev.to/shyamoec/how-to-control-reactjs-mui-table-column-height-and-width-4f2n","created_at":"2022-04-13T04:27:54Z","edited_at":null,"crossposted_at":null,"published_at":"2022-04-13T04:27:54Z","last_comment_at":"2022-04-13T04:27:54Z","reading_time_minutes":1,"tag_list":"react, mui","tags":["react","mui"],"body_html":"

Recently I used ReactJS MUI Table component, but I feel it's height is too large for my project nature. I am unable to properly control cell width and height of MUI Table component. Please anyone help me.

\n\n","body_markdown":"Recently I used ReactJS MUI Table component, but I feel it's height is too large for my project nature. I am unable to properly control cell width and height of MUI Table component. Please anyone help me.","user":{"name":"shyam-oec","username":"shyamoec","twitter_username":null,"github_username":"shyam-oec","user_id":846619,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--03WCUtl8--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/846619/66b96d7a-963c-44dc-a1cb-9bc0dbba59a4.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--7INBJroT--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/846619/66b96d7a-963c-44dc-a1cb-9bc0dbba59a4.png"}} -------------------------------------------------------------------------------- /src/pages/api/article-tag.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { mainConfig } from '../../configs/main-config'; 4 | import { BASE_API_URI, ITEMS_PER_PAGE } from '../../constants/article-const'; 5 | import { consoleLog } from '../../utils/console-log'; 6 | import { Article } from '../../types/article-types'; 7 | 8 | const fs = require('fs'); 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse, 13 | ) { 14 | const page = (req.query.page as string) || '1'; 15 | const tag = req.query.tag as string; 16 | 17 | const apiEndpoint = `${BASE_API_URI}/articles?tag=${tag}&page=${page}&per_page=${ITEMS_PER_PAGE}`; 18 | const fileName = tag.toLowerCase().trim().replace(/[^a-zA-Z0-9]/g, '_'); 19 | const jsonFile = `${mainConfig.dataFilePath}/tag-${fileName}-${page}-${ITEMS_PER_PAGE}.json`; 20 | 21 | try { 22 | // read data from local json file first 23 | const fsStats = fs.statSync(jsonFile, { throwIfNoEntry: false }); 24 | if (fsStats && 'mtime' in fsStats) { 25 | // consoleLog('fs stats', fsStats); 26 | const articleData = JSON.parse(fs.readFileSync(jsonFile, 'utf8')); 27 | if (Array.isArray(articleData) && articleData.length && 'id' in articleData[0]) { 28 | consoleLog('🚀 fetch article tag list form local:', jsonFile); 29 | res.status(200).json(articleData as Article[]); 30 | return; 31 | } 32 | } 33 | 34 | const rs = await fetch(`${apiEndpoint}`); 35 | const data = await rs.json(); 36 | 37 | // save data to json file 38 | if (Array.isArray(data) && data.length && 'id' in data[0] && 'title' in data[0]) { 39 | fs.writeFileSync(jsonFile, JSON.stringify(data), 'utf8'); 40 | consoleLog('🚀 Save article tag list to local:', jsonFile); 41 | 42 | res.status(200).json(data as Article[]); 43 | return; 44 | } 45 | 46 | consoleLog('🚀 wrong article tag list formate', data); 47 | } catch (error) { 48 | consoleLog('🚀 error', error); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | .env.local 106 | -------------------------------------------------------------------------------- /public/savedData/article-1246592.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246592,"title":"Critical situation for a coder","description":"A post by Developer","readable_publish_date":"Nov 7","slug":"critical-situation-for-a-coder-40fl","path":"/develop92001047/critical-situation-for-a-coder-40fl","url":"https://dev.to/develop92001047/critical-situation-for-a-coder-40fl","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T13:57:22Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1246592.png","canonical_url":"https://dev.to/develop92001047/critical-situation-for-a-coder-40fl","created_at":"2022-11-07T13:57:23Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T13:57:22Z","last_comment_at":"2022-11-07T13:57:22Z","reading_time_minutes":1,"tag_list":"webdev, beginners, javascript, react","tags":["webdev","beginners","javascript","react"],"body_html":"

\"Image

\n\n","body_markdown":"\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mxzrji3qhq7u2ysr6jdd.jpeg)","user":{"name":"Developer","username":"develop92001047","twitter_username":"Develop92001047","github_username":null,"user_id":965619,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--h5vGjH0_--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/965619/45e79843-df31-4383-8121-24f12a5b9019.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--w3sJ7rR2--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/965619/45e79843-df31-4383-8121-24f12a5b9019.jpg"}} -------------------------------------------------------------------------------- /src/utils/upload-to-cloudinary.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { CloudinaryAuthType } from '../pages/api/cloudinary-auth'; 3 | 4 | export interface CloudinaryResType { 5 | fileId: string; 6 | filePath: string; 7 | fileType: string; 8 | width: number; 9 | height: number; 10 | name: string; 11 | size: number; 12 | thumbnailUrl: string; 13 | url: string; 14 | versionInfo: { 15 | id: string; 16 | name: string; 17 | }; 18 | file: File; 19 | folder: string; 20 | filename: string; 21 | } 22 | 23 | export interface UploadFileToCloudinaryProps { 24 | file: File; 25 | folder?: string; 26 | fileName?: string; 27 | auth?:CloudinaryAuthType; 28 | authApiEndpoint?:string; 29 | eager?:string; 30 | } 31 | export const uploadFileToCloudinary = async ({ 32 | file, folder = 'default_folder', fileName, auth, 33 | authApiEndpoint = '/api/cloudinary-auth', 34 | eager = 'c_limit,w_1500', 35 | }:UploadFileToCloudinaryProps):Promise => { 36 | let cloudinaryAuth = auth; 37 | if (!auth) { 38 | const publicId = fileName || file.name.slice(0, file.name.lastIndexOf('.')).replace(/\s+/g, '_'); 39 | const authRes = await axios.post(authApiEndpoint, { 40 | folder, publicId, eager, 41 | }); 42 | 43 | cloudinaryAuth = authRes.data as CloudinaryAuthType; 44 | } 45 | if (!cloudinaryAuth?.signature) { 46 | throw new Error('Get cloudinaryAuth failed'); 47 | } 48 | 49 | const formData = new FormData(); 50 | formData.append('file', file); 51 | // formData.append('fileName', fileName || file.name); 52 | formData.append('folder', folder); 53 | formData.append('api_key', cloudinaryAuth.apiKey); 54 | formData.append('signature', cloudinaryAuth.signature); 55 | formData.append('timestamp', cloudinaryAuth.timestamp.toString()); 56 | formData.append('public_id', cloudinaryAuth.publicId); 57 | formData.append('eager', cloudinaryAuth.eager); 58 | 59 | const uploadRes = await axios.post(cloudinaryAuth.uploadUrl, formData); 60 | if (uploadRes.status !== 200) { 61 | throw new Error('Upload failed'); 62 | } 63 | return { 64 | ...uploadRes.data, file, folder, fileName, 65 | } as CloudinaryResType; 66 | }; 67 | 68 | export default uploadFileToCloudinary; 69 | -------------------------------------------------------------------------------- /src/helpers/article-helper.ts: -------------------------------------------------------------------------------- 1 | import { format, parseISO } from 'date-fns'; 2 | import { DEFAULT_IMAGE_URL, PROFILE_STAR_LABELS } from '../constants/article-const'; 3 | import { Article, FavoriteItem } from '../types/article-types'; 4 | 5 | export const getArticleLink = (article: Article | FavoriteItem) => `/articles/${article.slug}-${article.id}`; 6 | 7 | export const getTagLink = (tag: string) => `/articles/tag/${tag}`; 8 | 9 | export const getArticleTags = (article: Article | FavoriteItem) => { 10 | let tagAry:string[] = article?.tags && Array.isArray(article.tags) ? article.tags : []; 11 | if (tagAry.length === 0) { 12 | tagAry = ( 13 | article?.tag_list && (Array.isArray(article.tag_list) 14 | ? article.tag_list : article.tag_list.split(',')) 15 | ) || []; 16 | } 17 | return tagAry.map((item) => { 18 | const w = item.trim(); 19 | return w.charAt(0).toUpperCase() + w.slice(1); 20 | }); 21 | }; 22 | 23 | export const getArticleImgUrl = (article: Article | FavoriteItem) => { 24 | const tagStr = getArticleTags(article).join(', '); 25 | 26 | const newImg = (article.cover_image ? `${article.cover_image}?w=248&fit=crop&auto=format` : DEFAULT_IMAGE_URL + (tagStr || 'No cover image')); 27 | 28 | const cloudinaryUrl = newImg.includes('cloudinary.com') 29 | ? newImg : `https://res.cloudinary.com/demo/image/fetch/${encodeURIComponent(newImg)}`; 30 | 31 | return cloudinaryUrl; 32 | }; 33 | 34 | export const convertArticleToFavoriteItem = (article: Article) => { 35 | const item = { 36 | id : article.id, 37 | title : article.title, 38 | description : article.description, 39 | tags : article.tags, 40 | tag_list : Array.isArray(article.tags) ? article.tags.join(', ') : article.tags || '', 41 | cover_image : article.cover_image, 42 | slug : article.slug, 43 | published_at : article.published_at, 44 | visited_at : new Date().toISOString(), 45 | author : article.user.name, 46 | author_avatar: article.user.profile_image_90, 47 | }; 48 | return item; 49 | }; 50 | 51 | export const getFormattedDate = (dateIso: string, formatStr = 'MM/dd/yyyy') => format(parseISO(dateIso), formatStr); 52 | 53 | export const getStarLabelText = (value: number) => `${value} Star${value !== 1 ? 's' : ''}, ${PROFILE_STAR_LABELS[value]}`; 54 | -------------------------------------------------------------------------------- /src/pages/api/cloudinary-auth.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import crypto from 'crypto'; 3 | /** 4 | * 5 | * API endpoint: http://localhost:3000/api/cloudinary-auth 6 | * 7 | * ============ Example of .env.local ============ 8 | * CLOUDINARY_API_KEY="98624656233" 9 | * CLOUDINARY_API_SECRET="eQq7lcKKGcNUr5fB_8aqNIk" 10 | * CLOUDINARY_API_BASE_URL="https://api.cloudinary.com/v1_1/pereey" 11 | * CLOUDINARY_API_EAGER="c_limit,w_1600" 12 | * CLOUDINARY_API_UPLOAD_URL="https://api.cloudinary.com/v1_1/pereey/image/upload" 13 | 14 | */ 15 | 16 | export interface CloudinaryAuthType { 17 | eager: string; 18 | publicId: string; 19 | timestamp: number; 20 | signature: string; 21 | uploadUrl: string; 22 | apiKey: string; 23 | urlEndpoint: string; 24 | } 25 | 26 | const cloudinaryAuth = ( 27 | req: NextApiRequest, 28 | res: NextApiResponse, 29 | ) => { 30 | const keys = { 31 | apiKey : process.env.CLOUDINARY_API_KEY || 'Missing CLOUDINARY_API_KEY', 32 | apiSecret : process.env.CLOUDINARY_API_SECRET || 'Missing CLOUDINARY_API_SECRET', 33 | urlEndpoint: process.env.CLOUDINARY_API_BASE_URL || 'Missing CLOUDINARY_API_BASE_URL', 34 | }; 35 | 36 | const timestamp = Math.round(new Date().getTime() / 1000); 37 | const eager = req.body.eager as string || 'c_limit,w_1600'; 38 | const publicId = req.body.publicId as string || `public_id_${timestamp}`; 39 | const folder = req.body.folder as string || 'default_folder'; 40 | 41 | const serializedSortedParameters = `eager=${eager}&folder=${folder}&public_id=${publicId}×tamp=${timestamp}${keys.apiSecret}`; 42 | 43 | const signature = crypto.createHash('sha1').update(serializedSortedParameters, 'binary').digest('hex'); 44 | 45 | const auth = { 46 | eager, 47 | publicId, 48 | timestamp, 49 | signature, 50 | uploadUrl: process.env.CLOUDINARY_API_UPLOAD_URL || 'Missing CLOUDINARY_API_UPLOAD_URL', 51 | }; 52 | const rs = { ...keys, ...auth, ...{ apiSecret: undefined } }; 53 | 54 | res.status(200).json(rs); 55 | }; 56 | 57 | export default function handler( 58 | req: NextApiRequest, 59 | res: NextApiResponse, 60 | ) { 61 | if (req.method !== 'POST') { 62 | res.status(405); 63 | return; 64 | } 65 | cloudinaryAuth(req, res); 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /public/savedData/article-1223039.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1223039,"title":"Looking for React Material UI iOS custom theming","description":"Hi guys, as I see here: https://mui.com/material-ui/customization/theming/ it's possible to customize...","readable_publish_date":"Oct 18","slug":"looking-for-react-material-ui-ios-custom-theming-255a","path":"/vitalijalbu/looking-for-react-material-ui-ios-custom-theming-255a","url":"https://dev.to/vitalijalbu/looking-for-react-material-ui-ios-custom-theming-255a","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-10-18T12:21:20Z","positive_reactions_count":3,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1223039.png","canonical_url":"https://dev.to/vitalijalbu/looking-for-react-material-ui-ios-custom-theming-255a","created_at":"2022-10-18T12:21:21Z","edited_at":"2022-10-18T12:22:13Z","crossposted_at":null,"published_at":"2022-10-18T12:21:20Z","last_comment_at":"2022-10-18T12:21:20Z","reading_time_minutes":1,"tag_list":"javascript, react, ios, mui","tags":["javascript","react","ios","mui"],"body_html":"

Hi guys, as I see here: https://mui.com/material-ui/customization/theming/ it's possible to customize MUI.
\nI'm coming from framework7 (https://framework7.io/react/appbar) and really want to have an ios like theme with MUI. If anyone has it, and not for free it's not a problem. Thanks

\n\n","body_markdown":"Hi guys, as I see here: https://mui.com/material-ui/customization/theming/ it's possible to customize MUI.\nI'm coming from framework7 (https://framework7.io/react/appbar) and really want to have an ios like theme with MUI. If anyone has it, and not for free it's not a problem. Thanks ","user":{"name":"vitalie","username":"vitalijalbu","twitter_username":"vitalijalbu","github_username":"vitalijalbu","user_id":222619,"website_url":"https://ceebo.com/","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--ewfhMOL3--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/222619/bb504ea9-030b-4363-b7dd-60715904b357.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--C-Nx9bNO--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/222619/bb504ea9-030b-4363-b7dd-60715904b357.jpg"}} -------------------------------------------------------------------------------- /public/savedData/article-1246952.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246952,"title":"Multiple Laravel developers Required","description":"We are looking for outstanding Laravel Developers to join our growing software development team. If...","readable_publish_date":"Nov 7","slug":"multiple-laravel-developers-required-320g","path":"/softsolex/multiple-laravel-developers-required-320g","url":"https://dev.to/softsolex/multiple-laravel-developers-required-320g","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T18:34:41Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1246952.png","canonical_url":"https://dev.to/softsolex/multiple-laravel-developers-required-320g","created_at":"2022-11-07T18:34:42Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T18:34:41Z","last_comment_at":"2022-11-07T18:34:41Z","reading_time_minutes":1,"tag_list":"job, laravel, webdev, javascript","tags":["job","laravel","webdev","javascript"],"body_html":"

We are looking for outstanding Laravel Developers to join our growing software development team. If you have a strong background in Laravel Development and have a passion for innovation and technology, come and join our team! You will collaborate with the team to create high-quality online apps, services, and tools for our company.
\nAt SoftSolex, we offer above-industry compensation, perks, benefits, and career growth prospects.

\n\n","body_markdown":"We are looking for outstanding Laravel Developers to join our growing software development team. If you have a strong background in Laravel Development and have a passion for innovation and technology, come and join our team! You will collaborate with the team to create high-quality online apps, services, and tools for our company.\nAt SoftSolex, we offer above-industry compensation, perks, benefits, and career growth prospects.\n","user":{"name":"Soft Solex","username":"softsolex","twitter_username":"SoftSolex","github_username":null,"user_id":968007,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--OPej-upg--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/968007/13830d1a-9ce1-4399-b1ec-2b8d54a6b826.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--b-PYIe-C--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/968007/13830d1a-9ce1-4399-b1ec-2b8d54a6b826.jpg"}} -------------------------------------------------------------------------------- /src/redux/saga.ts: -------------------------------------------------------------------------------- 1 | import { call, takeLatest, put } from 'redux-saga/effects'; 2 | import { PayloadAction } from '@reduxjs/toolkit'; 3 | import articleSlice from './features/articleSlice'; 4 | import * as articleApi from '../apis/article-api'; 5 | import * as userApi from '../apis/user-api'; 6 | 7 | import { 8 | Article, ArticleFilterParams, AvatarResponse, UploadFileParams, 9 | } from '../types/article-types'; 10 | import userSlice from './features/userSlice'; 11 | import { consoleLog } from '../utils/console-log'; 12 | 13 | export function* fetchArticles(action: PayloadAction) { 14 | try { 15 | const response:Article[] = yield call(articleApi.getArticles, action.payload); 16 | 17 | const newData = { 18 | params: action.payload, 19 | data : response, 20 | }; 21 | 22 | // const response:Article[] = yield call(() => articleApi.getArticles(action.payload)); 23 | 24 | yield put(articleSlice.actions.getArticlesSuccess(newData)); 25 | } catch (e: any) { 26 | consoleLog('🚀 ~ file: saga.ts ~ line 19 ~ function*fetchArticles ~ e', e); 27 | 28 | yield put(articleSlice.actions.getArticlesFailure(e.message)); 29 | } 30 | } 31 | 32 | export function* fetchArticleDetail(action: PayloadAction<{ id: number }>) { 33 | try { 34 | const response:Article = yield call(() => articleApi.getArticleDetail(action.payload)); 35 | 36 | yield put(articleSlice.actions.getArticleDetailSuccess(response)); 37 | } catch (e: any) { 38 | // yield put(articleSlice.actions.getArticleDetailFailure(e.message)); 39 | } 40 | } 41 | 42 | export function* uploadAvatar(action: PayloadAction) { 43 | try { 44 | const response:AvatarResponse = yield call(userApi.uploadAvatar, action.payload); 45 | 46 | consoleLog('🚀 ~ file: saga.ts ~ line 40 ~ function*uploadAvatar ~ response', response); 47 | 48 | yield put(userSlice.actions.uploadAvatarSuccess(response)); 49 | } catch (e: any) { 50 | consoleLog('🚀 ~ file: saga.ts ~ line 40 ~ function*uploadAvatar ~ error', e); 51 | 52 | // yield put(articleSlice.actions.getArticleDetailFailure(e.message)); 53 | } 54 | } 55 | 56 | export default function* rootSaga() { 57 | yield takeLatest(articleSlice.actions.getArticlesRequest, fetchArticles); 58 | yield takeLatest(articleSlice.actions.getArticleDetailRequest, fetchArticleDetail); 59 | 60 | // uploadAvatarRequest 61 | yield takeLatest(userSlice.actions.uploadAvatarRequest, uploadAvatar); 62 | 63 | // yield takeLatest(userSlice.actions.visit, fetchNumberSaga) 64 | } 65 | -------------------------------------------------------------------------------- /public/savedData/article-1240354.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1240354,"title":"AWS Cloud Services","description":"Bring the power of AWS cloud services to scale your ever-evolving business needs. Leverage AWS cloud...","readable_publish_date":"Nov 2","slug":"aws-cloud-services-3fe3","path":"/shreyasoftweb/aws-cloud-services-3fe3","url":"https://dev.to/shreyasoftweb/aws-cloud-services-3fe3","comments_count":0,"public_reactions_count":1,"collection_id":null,"published_timestamp":"2022-11-02T13:06:47Z","positive_reactions_count":1,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1240354.png","canonical_url":"https://dev.to/shreyasoftweb/aws-cloud-services-3fe3","created_at":"2022-11-02T13:06:48Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-02T13:06:47Z","last_comment_at":"2022-11-02T13:06:47Z","reading_time_minutes":1,"tag_list":"aws, cloudservice, awsconsultant, awsservices","tags":["aws","cloudservice","awsconsultant","awsservices"],"body_html":"

Bring the power of AWS cloud services to scale your ever-evolving business needs.

\n\n

Leverage AWS cloud services to automate scaling, configuration, monitoring and so on. This improves operational efficiency, creates new revenue streams, reduces business risks and scales your ever-evolving business needs. Our AWS experts help create cloud strategies, develop solutions and implement them to maximize your ROI from various AWS services.

\n\n","body_markdown":"Bring the power of AWS cloud services to scale your ever-evolving business needs.\n\nLeverage [AWS cloud services](https://www.softwebsolutions.com/aws-services.html) to automate scaling, configuration, monitoring and so on. This improves operational efficiency, creates new revenue streams, reduces business risks and scales your ever-evolving business needs. Our AWS experts help create cloud strategies, develop solutions and implement them to maximize your ROI from various AWS services.\n\n\n \n","user":{"name":"Shreya thakar","username":"shreyasoftweb","twitter_username":"ShreyaSoftweb","github_username":"SoftwebSolutionsinc","user_id":850648,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--NA2FOnFt--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/850648/895224ad-b0ac-4b25-8b01-48da3929f3ad.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--qLPjj7NA--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/850648/895224ad-b0ac-4b25-8b01-48da3929f3ad.png"}} -------------------------------------------------------------------------------- /public/savedData/article-1196320.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1196320,"title":"can anyone suggest about a reader narrative button like medium","description":"the great medium dot com has a buttons that reads out the text of the whole website ? is there ant...","readable_publish_date":"Sep 18","slug":"can-anyone-suggest-about-a-reader-button-like-medium-4jhj","path":"/cadentic/can-anyone-suggest-about-a-reader-button-like-medium-4jhj","url":"https://dev.to/cadentic/can-anyone-suggest-about-a-reader-button-like-medium-4jhj","comments_count":0,"public_reactions_count":4,"collection_id":null,"published_timestamp":"2022-09-18T10:33:04Z","positive_reactions_count":4,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1196320.png","canonical_url":"https://dev.to/cadentic/can-anyone-suggest-about-a-reader-button-like-medium-4jhj","created_at":"2022-09-18T10:33:04Z","edited_at":"2022-09-18T22:34:57Z","crossposted_at":null,"published_at":"2022-09-18T10:33:04Z","last_comment_at":"2022-09-18T10:33:04Z","reading_time_minutes":1,"tag_list":"react, nextjs, javascript, mui","tags":["react","nextjs","javascript","mui"],"body_html":"

the great medium dot com has a buttons that reads out the text of the whole website ? is there ant react component available that can read them ? i guess there must be something like that available. if it is not available through any matarial ui / react library can anyone suggest how to create such from the scratch?

\n\n

this button will be most welcomed for the low visioned people so let me know the details about it.

\n\n","body_markdown":"the great medium dot com has a buttons that reads out the text of the whole website ? is there ant react component available that can read them ? i guess there must be something like that available. if it is not available through any matarial ui / react library can anyone suggest how to create such from the scratch? \n\nthis button will be most welcomed for the low visioned people so let me know the details about it. ","user":{"name":"sayantan chakraborty","username":"cadentic","twitter_username":"Neutralist2022","github_username":"cadentic","user_id":927783,"website_url":"https://cadentic.net","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--v_FJzaBd--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/927783/89c7311e-1030-4abf-b18c-ffe944d41cce.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--jncojlv4--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/927783/89c7311e-1030-4abf-b18c-ffe944d41cce.png"}} -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { useState, useEffect } from 'react'; 3 | import type { AppProps } from 'next/app'; 4 | import { 5 | Hydrate, 6 | QueryClient, 7 | QueryClientProvider, 8 | } from '@tanstack/react-query'; 9 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 10 | import { PersistGate } from 'redux-persist/integration/react'; 11 | import { useStore } from 'react-redux'; 12 | import { ReduxStore, reduxWrapper } from '../redux/store'; 13 | import userSlice from '../redux/features/userSlice'; 14 | import { IS_DEV } from '../constants/article-const'; 15 | 16 | const MyApp = ({ Component, pageProps }: AppProps) => { 17 | const reduxStore = useStore(); 18 | const [queryClient] = useState(() => new QueryClient()); 19 | 20 | useEffect( 21 | () => { 22 | reduxStore.dispatch(userSlice.actions.visitRequest()); 23 | }, 24 | [reduxStore], 25 | ); 26 | 27 | // return enableReduxPersist ? ( 28 | // 32 | // 33 | // 34 | // 35 | // 36 | // 37 | // 38 | 39 | // 40 | // ) : ( 41 | // 42 | // 43 | // 44 | // 45 | // 46 | // 47 | // ); 48 | 49 | return ( 50 | 54 | {() => ( 55 | 56 | 57 | 58 | 59 | {IS_DEV && } 60 | 61 | )} 62 | 63 | ); 64 | 65 | /** 66 | * uncomment the below if you want to disable redux persist 67 | */ 68 | 69 | // return ( 70 | // 71 | // 72 | // 73 | // 74 | // 75 | // 76 | // ); 77 | }; 78 | 79 | export default reduxWrapper.withRedux(MyApp); 80 | -------------------------------------------------------------------------------- /public/savedData/article-1208531.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1208531,"title":"(S+C) (EO) = Neil Patel","description":"After dissecting Nell Patel's article covering 5 rules for sub brand keyword strategy, I found it...","readable_publish_date":"Oct 1","slug":"sc-eo-neil-patel-1keo","path":"/tonymai6/sc-eo-neil-patel-1keo","url":"https://dev.to/tonymai6/sc-eo-neil-patel-1keo","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-10-01T19:50:48Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1208531.png","canonical_url":"https://dev.to/tonymai6/sc-eo-neil-patel-1keo","created_at":"2022-10-01T19:50:49Z","edited_at":null,"crossposted_at":null,"published_at":"2022-10-01T19:50:48Z","last_comment_at":"2022-10-01T19:50:48Z","reading_time_minutes":1,"tag_list":"beginners, programming, seo, productivity","tags":["beginners","programming","seo","productivity"],"body_html":"

After dissecting Nell Patel's article covering 5 rules for sub brand keyword strategy, I found it interesting how brands make sub brands to target new audiences and market different products. Also, key to SEO is to be realistic of the key words to rank well on Google. This opened my eyes as to how brands have to see the importance of including SEO in their initial conversation when making their new product/brand.

\n\n

Source: https://neilpatel.com/blog/keyword-research-for-multiple-domains-in-the-same-niche/?ref=dailydevbytes.com

\n\n","body_markdown":"After dissecting Nell Patel's article covering 5 rules for sub brand keyword strategy, I found it interesting how brands make sub brands to target new audiences and market different products. Also, key to SEO is to be realistic of the key words to rank well on Google. This opened my eyes as to how brands have to see the importance of including SEO in their initial conversation when making their new product/brand. \n\nSource: https://neilpatel.com/blog/keyword-research-for-multiple-domains-in-the-same-niche/?ref=dailydevbytes.com\n\n","user":{"name":"tonymai6","username":"tonymai6","twitter_username":null,"github_username":"tonymai6","user_id":935717,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--cGaVdXvs--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/935717/0086ae76-ab86-424a-a14b-38e930b01904.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--KeqYqzWp--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/935717/0086ae76-ab86-424a-a14b-38e930b01904.png"}} -------------------------------------------------------------------------------- /src/components/FavoriteItemHeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | IconButton, SxProps, Theme, Tooltip, 4 | } from '@mui/material'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { Favorite, FavoriteBorder } from '@mui/icons-material'; 7 | import type { Article, FavoriteItem, UserSliceType } from '../types/article-types'; 8 | import { ReduxState } from '../redux/store'; 9 | import userSlice from '../redux/features/userSlice'; 10 | import ActionToaster from './ActionToaster'; 11 | 12 | interface FavoriteItemHeartIconProps { 13 | item: Article | FavoriteItem; 14 | itemName?: string; 15 | iconButtonSx?: SxProps | undefined; 16 | } 17 | 18 | const FavoriteItemHeartIcon = ({ item, itemName = 'article', iconButtonSx }: FavoriteItemHeartIconProps) => { 19 | const reduxDispatch = useDispatch(); 20 | 21 | const reduxUserData:UserSliceType = useSelector((reduxState: ReduxState) => reduxState.user); 22 | 23 | const isFavorite = reduxUserData.favoriteItems.some( 24 | (favoriteItem) => item.id === favoriteItem.id, 25 | ); 26 | 27 | const [showToaster, setShowToaster] = useState(false); 28 | 29 | const onClickFavorite = (event: React.MouseEvent) => { 30 | event.stopPropagation(); 31 | 32 | reduxDispatch(userSlice.actions.favoriteItemRequest(item)); 33 | setShowToaster(true); 34 | }; 35 | 36 | const IconButtonSx = iconButtonSx ?? { 37 | position : 'absolute', 38 | top : '0.5rem', 39 | right : '0.5rem', 40 | background: 'rgba(255, 255, 255, 0.4)', 41 | ':hover' : { 42 | background: 'rgba(255, 255, 255, 0.8)', 43 | }, 44 | }; 45 | 46 | return ( 47 | <> 48 | 62 | onClickFavorite(e)} 66 | sx={IconButtonSx} 67 | > 68 | 69 | {isFavorite ? : } 70 | 71 | 72 | 73 | 79 | 80 | 81 | ); 82 | }; 83 | export default FavoriteItemHeartIcon; 84 | -------------------------------------------------------------------------------- /src/types/article-types.ts: -------------------------------------------------------------------------------- 1 | export interface ArticleFilterParams { 2 | tag?: string; 3 | page?: number; 4 | } 5 | 6 | export interface ArticleDetailParams { 7 | id: number; 8 | } 9 | 10 | export interface UploadFileParams { 11 | file: File; 12 | provider: 'cloudinary' | 'imagekit'; 13 | } 14 | 15 | export interface PageMeta { 16 | title: string; 17 | description?: string; 18 | image?: string; 19 | url?: string; 20 | keywords?: string[]; 21 | type?: string; 22 | locale?: string; 23 | } 24 | 25 | export interface FavoriteItem 26 | extends Pick { 27 | visited_at: string; 28 | favorite_at?: string; 29 | author: string; 30 | author_avatar: string; 31 | tags?: string[]; 32 | tag_list: string; 33 | } 34 | 35 | export interface Article { 36 | id: number; 37 | title: string; 38 | description: string; 39 | cover_image: string; 40 | readable_publish_date: string; 41 | social_image: string; 42 | tag_list: string[] | string; 43 | slug: string; 44 | url: string; 45 | canonical_url: string; 46 | comments_count: number; 47 | positive_reactions_count: number; 48 | public_reactions_count: number; 49 | collection_id: null; 50 | created_at: string; 51 | edited_at: string; 52 | body_html?: string; 53 | tags?: string[]; 54 | published_at: string; 55 | last_comment_at: string; 56 | published_timestamp: string; 57 | reading_time_minutes: number; 58 | user: { 59 | name: string; 60 | username: string; 61 | twitter_username: string; 62 | github_username: string; 63 | website_url: string; 64 | profile_image: string; 65 | profile_image_90: string; 66 | }; 67 | } 68 | 69 | export interface ChildrenProps { 70 | children:React.ReactNode; 71 | } 72 | 73 | export interface Profile { 74 | firstName: string; 75 | lastName?: string; 76 | avatarUrl?: string; 77 | uploadProvider: 'cloudinary' | 'imagekit'; 78 | isFemale: boolean; 79 | ageRange: number; 80 | favoriteMaterialUI: boolean; 81 | favoriteChakraUI: boolean; 82 | favoriteSemanticUI: boolean; 83 | favoriteAntDesign: boolean; 84 | starRating: number; 85 | yearsUsingReact: number; 86 | } 87 | 88 | export interface AvatarResponse { 89 | avatarUrl: string; 90 | } 91 | 92 | // redux slice types 93 | export interface UserSliceType { 94 | identityToken: string; 95 | visitedTimes: number; 96 | recentItems: FavoriteItem[]; 97 | favoriteItems: FavoriteItem[]; 98 | profile: Profile; 99 | status: 'loading' | 'loaded' | 'error' | ''; 100 | } 101 | 102 | export interface ArticleSliceType { 103 | lists: Article[]; 104 | detail: Article | null; 105 | status: UserSliceType['status']; 106 | message?: string; 107 | searchTag: string; 108 | } 109 | -------------------------------------------------------------------------------- /public/savedData/article-1246293.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246293,"title":"Day #3: How to build multiple profitable micro-startups as a Solopreneur?","description":"At the beginning of my journey as an entrepreneur, I decided to build multiple micro-products and...","readable_publish_date":"Nov 7","slug":"day-3-how-to-build-multiple-profitable-micro-startups-as-a-solopreneur-2l9i","path":"/journeypreneur/day-3-how-to-build-multiple-profitable-micro-startups-as-a-solopreneur-2l9i","url":"https://dev.to/journeypreneur/day-3-how-to-build-multiple-profitable-micro-startups-as-a-solopreneur-2l9i","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T09:49:18Z","positive_reactions_count":0,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--H3DPpzCc--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yjzztcanouk7ifpze4kl.png","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--dsmVj4BL--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yjzztcanouk7ifpze4kl.png","canonical_url":"https://dev.to/journeypreneur/day-3-how-to-build-multiple-profitable-micro-startups-as-a-solopreneur-2l9i","created_at":"2022-11-07T09:49:18Z","edited_at":"2022-11-07T09:49:29Z","crossposted_at":null,"published_at":"2022-11-07T09:49:18Z","last_comment_at":"2022-11-07T09:49:18Z","reading_time_minutes":1,"tag_list":"entrepreneurship, startup, webdev, react","tags":["entrepreneurship","startup","webdev","react"],"body_html":"

At the beginning of my journey as an entrepreneur, I decided to build multiple micro-products and released them in the market as soon as possible.

\n\n

https://medium.com/@journeypreneur/day-3-how-to-build-multiple-profitable-micro-startups-as-a-solopreneur-43a0801c586d

\n\n","body_markdown":"At the beginning of my journey as an entrepreneur, I decided to build multiple micro-products and released them in the market as soon as possible.\n\nhttps://medium.com/@journeypreneur/day-3-how-to-build-multiple-profitable-micro-startups-as-a-solopreneur-43a0801c586d \n","user":{"name":"David Journeypreneur","username":"journeypreneur","twitter_username":"journeypreneur_","github_username":null,"user_id":967529,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--8kgSH8cp--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/967529/b06c0144-f2cc-43f7-8a4a-a338fb76bad1.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--9JiWL3yi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/967529/b06c0144-f2cc-43f7-8a4a-a338fb76bad1.jpg"}} -------------------------------------------------------------------------------- /public/savedData/article-1209550.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1209550,"title":"Sitemap generator for website🌏","description":"Hey Devs👋, Sitemap is an awesome sitemap generator for building SEO-friendly websites. This is...","readable_publish_date":"Oct 3","slug":"sitemap-generator-for-website-1gef","path":"/amalprasad0/sitemap-generator-for-website-1gef","url":"https://dev.to/amalprasad0/sitemap-generator-for-website-1gef","comments_count":0,"public_reactions_count":1,"collection_id":null,"published_timestamp":"2022-10-03T05:27:08Z","positive_reactions_count":1,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--yyR-lCCZ--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wetmpkpce16fnsi5qws7.png","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--_LMN-lWU--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wetmpkpce16fnsi5qws7.png","canonical_url":"https://dev.to/amalprasad0/sitemap-generator-for-website-1gef","created_at":"2022-10-03T05:27:09Z","edited_at":"2022-10-03T05:29:01Z","crossposted_at":null,"published_at":"2022-10-03T05:27:08Z","last_comment_at":"2022-10-03T05:27:08Z","reading_time_minutes":1,"tag_list":"learnwithamal, sitemap, seo, webdev","tags":["learnwithamal","sitemap","seo","webdev"],"body_html":"

Hey Devs👋,
\n Sitemap is an awesome sitemap generator for building SEO-friendly websites. This is also an easy way to generate sitemaps. It took about few seconds to generate sitemap file for website.

\n\n

Then how to add sitemap.xml to our website?🫤
\nIts easy! , After generating the sitemap, just download the .xml file and upload it to the host server \"ie root folder\". After few minute your sitemap will online.

\n\n

THANK YOU!
\nLearnwithamal

\n\n","body_markdown":"Hey Devs👋,\n [Sitemap](https://www.xml-sitemaps.com/) is an awesome sitemap generator for building SEO-friendly websites. This is also an easy way to generate sitemaps. It took about few seconds to generate sitemap file for website.\n\n\nThen how to add sitemap.xml to our website?🫤\nIts easy! , After generating the sitemap, just download the .xml file and upload it to the host server \"ie root folder\". After few minute your sitemap will online.\n\n\n \nTHANK YOU!\nLearnwithamal\n\n\n\n","user":{"name":"Amal Prasad","username":"amalprasad0","twitter_username":null,"github_username":"amalprasad0","user_id":794257,"website_url":"https://amalprasad0.github.io/amal.prasad/","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--TXJhvB_h--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/794257/c6f487fe-9ef5-41f2-94e5-efce8def9951.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--zmlb0wja--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/794257/c6f487fe-9ef5-41f2-94e5-efce8def9951.jpg"}} -------------------------------------------------------------------------------- /public/savedData/article-1246616.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246616,"title":"Two line code for responsive navigation 🫡","description":"Responsive menu hasn’t been easier for beginners and some intermediate developers. Do it the right...","readable_publish_date":"Nov 7","slug":"two-line-code-for-responsive-navigation-350h","path":"/elliot_brenyasarfo_18749/two-line-code-for-responsive-navigation-350h","url":"https://dev.to/elliot_brenyasarfo_18749/two-line-code-for-responsive-navigation-350h","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T14:49:58Z","positive_reactions_count":0,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--JYoM0mOs--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5koe94vvm0yhicyqm7o0.jpeg","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--r-isLbih--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5koe94vvm0yhicyqm7o0.jpeg","canonical_url":"https://dev.to/elliot_brenyasarfo_18749/two-line-code-for-responsive-navigation-350h","created_at":"2022-11-07T14:49:58Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T14:49:58Z","last_comment_at":"2022-11-07T14:49:58Z","reading_time_minutes":1,"tag_list":"webdev, beginners, javascript, tutorial","tags":["webdev","beginners","javascript","tutorial"],"body_html":"

Responsive menu hasn’t been easier for beginners and some intermediate developers.

\n\n

Do it the right way ….. this is how I did it 👇🏽👇🏽👇🏽

\n\n

youtu.be/K00FyCRwDDQ

\n\n

\"Image

\n\n","body_markdown":"Responsive menu hasn’t been easier for beginners and some intermediate developers. \n\nDo it the right way ….. this is how I did it 👇🏽👇🏽👇🏽\n\n[youtu.be/K00FyCRwDDQ](youtu.be/K00FyCRwDDQ)\n\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/loz7ohs9iiiy8ute4c83.jpeg)","user":{"name":"Elliot Brenya sarfo","username":"elliot_brenyasarfo_18749","twitter_username":null,"github_username":null,"user_id":696524,"website_url":"https://www.anythingprogramming.com/?m=1","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--jn-d5X7o--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/696524/350cbcfa-8877-45a5-88ff-acf703589fef.jpeg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--WCpGs_-g--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/696524/350cbcfa-8877-45a5-88ff-acf703589fef.jpeg"}} -------------------------------------------------------------------------------- /public/savedData/article-1246380.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246380,"title":"React Native RTMP Publisher","description":"Hi everyone! I've improved and re-write my React Native RTMP Publisher package that I shared before!...","readable_publish_date":"Nov 7","slug":"react-native-rtmp-publisher-1ok3","path":"/ezranbayantemur/react-native-rtmp-publisher-1ok3","url":"https://dev.to/ezranbayantemur/react-native-rtmp-publisher-1ok3","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T11:53:26Z","positive_reactions_count":0,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--0MfeMf3G--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5k11aeh36lbmolatln5.png","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--nQwjytH4--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5k11aeh36lbmolatln5.png","canonical_url":"https://dev.to/ezranbayantemur/react-native-rtmp-publisher-1ok3","created_at":"2022-11-07T11:53:26Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T11:53:26Z","last_comment_at":"2022-11-07T11:53:26Z","reading_time_minutes":1,"tag_list":"react, reactnative, live, streaming","tags":["react","reactnative","live","streaming"],"body_html":"

Hi everyone!

\n\n

I've improved and re-write my React Native RTMP Publisher package that I shared before!

\n\n

It is a package that you can broadcast live over RTMP with built-in camera support, bluetooth, wired etc. microphone options and more.

\n\n

I wrote it with Java & Swift.

\n\n

Here is the link if you want to take a look. If you like it you can support with 'star' ⭐️ and share!✌🏻

\n\n

If you want to contribute, I'll wait for your PRs 👍🏻

\n\n

(PS: I made logo on the readme and I'm amateur, feedback me if you have any advice 😁)

\n\n

https://github.com/ezranbayantemur/react-native-rtmp-publisher

\n\n","body_markdown":"Hi everyone!\n\nI've improved and re-write my React Native RTMP Publisher package that I shared before! \n\nIt is a package that you can broadcast live over RTMP with built-in camera support, bluetooth, wired etc. microphone options and more. \n\nI wrote it with Java & Swift.\n\nHere is the link if you want to take a look. If you like it you can support with 'star' ⭐️ and share!✌🏻\n\nIf you want to contribute, I'll wait for your PRs 👍🏻\n\n(PS: I made logo on the readme and I'm amateur, feedback me if you have any advice 😁)\n\nhttps://github.com/ezranbayantemur/react-native-rtmp-publisher","user":{"name":"Ezran Bayantemur","username":"ezranbayantemur","twitter_username":"ezranbayantemur","github_username":"ezranbayantemur","user_id":291591,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--N2HZ6l6e--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/291591/132f7034-cd44-4b92-b6b4-5b68b959b4dc.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--q4EIAXd1--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/291591/132f7034-cd44-4b92-b6b4-5b68b959b4dc.jpg"}} -------------------------------------------------------------------------------- /src/redux/features/userSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { mainConfig } from '../../configs/main-config'; 3 | import { convertArticleToFavoriteItem } from '../../helpers/article-helper'; 4 | import { 5 | Article, AvatarResponse, FavoriteItem, Profile, UploadFileParams, UserSliceType, 6 | } from '../../types/article-types'; 7 | import { consoleLog } from '../../utils/console-log'; 8 | 9 | export const defaultProfileValues: Profile = { 10 | firstName : '', 11 | isFemale : false, 12 | ageRange : 30, 13 | starRating: 2.5, 14 | avatarUrl : '', 15 | 16 | uploadProvider : 'imagekit', 17 | favoriteMaterialUI: true, 18 | favoriteChakraUI : false, 19 | favoriteSemanticUI: false, 20 | favoriteAntDesign : true, 21 | yearsUsingReact : 1.5, 22 | 23 | }; 24 | 25 | const initialState: UserSliceType = { 26 | identityToken: '', 27 | visitedTimes : 1, 28 | recentItems : [], 29 | favoriteItems: [], 30 | profile : defaultProfileValues, 31 | status : '', 32 | }; 33 | 34 | const userSlice = createSlice({ 35 | name : 'user', 36 | initialState, 37 | reducers: { 38 | visitRequest: (state) => { 39 | if (state.identityToken === '') { 40 | const isoDate = new Date().toISOString(); 41 | const token = mainConfig.isClientSide ? navigator.userAgent : Math.random().toString(); 42 | 43 | state.identityToken = `${token}|ISO DATE:${isoDate}`; 44 | } 45 | state.visitedTimes += 1; 46 | }, 47 | 48 | recentItemRequest: (state, action: PayloadAction
) => { 49 | if (state.recentItems[0]?.id === action.payload.id) { 50 | return; 51 | } 52 | const restItems = state.recentItems.filter((item) => item.id !== action.payload.id); 53 | const newItem = convertArticleToFavoriteItem(action.payload); 54 | state.recentItems = [newItem, ...restItems.slice(0, mainConfig.maxRecentItems - 1)]; 55 | }, 56 | 57 | favoriteItemRequest: (state, action: PayloadAction
) => { 58 | if (state.favoriteItems.find((item) => item.id === action.payload.id)) { 59 | state.favoriteItems = state.favoriteItems.filter((item) => item.id !== action.payload.id); 60 | return; 61 | } 62 | const payload = 'visited_at' in action.payload 63 | ? action.payload : convertArticleToFavoriteItem(action.payload); 64 | const newItem = { 65 | ...payload, 66 | favorite_at: new Date().toISOString(), 67 | }; 68 | state.favoriteItems.unshift(newItem); 69 | }, 70 | 71 | uploadAvatarRequest: (state, action: PayloadAction) => { 72 | state.status = 'loading'; 73 | consoleLog('🚀 ~ file: userSlice.ts ~ line 57 ~ uploadAvatarRequest', action, state); 74 | }, 75 | uploadAvatarSuccess: (state, action: PayloadAction) => { 76 | consoleLog('🚀 ~ file: userSlice.ts ~ line 57 ~ uploadAvatarSuccess', action, state); 77 | state.status = 'loaded'; 78 | 79 | state.profile = { ...state.profile, avatarUrl: action.payload.avatarUrl }; 80 | }, 81 | 82 | updateProfileRequest: (state, action: PayloadAction) => { 83 | state.profile = { 84 | ...state.profile, 85 | ...action.payload, 86 | avatarUrl: state.profile.avatarUrl, // avatarUrl is update separately 87 | }; 88 | consoleLog('🚀 ~ file: userSlice.ts ~ line 57 ~ updateProfileRequest', action, state); 89 | }, 90 | }, 91 | }); 92 | 93 | export default userSlice; 94 | -------------------------------------------------------------------------------- /public/savedData/article-1021330.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1021330,"title":"navbar design with MUI","description":"Document https://mui.com/components/app-bar/ modules npm i @emotion/styled npm i...","readable_publish_date":"Mar 13","slug":"navbar-design-with-mui-3obk","path":"/artemismars/navbar-design-with-mui-3obk","url":"https://dev.to/artemismars/navbar-design-with-mui-3obk","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-03-13T14:19:09Z","positive_reactions_count":3,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1021330.png","canonical_url":"https://dev.to/artemismars/navbar-design-with-mui-3obk","created_at":"2022-03-13T14:19:09Z","edited_at":null,"crossposted_at":null,"published_at":"2022-03-13T14:19:09Z","last_comment_at":"2022-03-13T14:19:09Z","reading_time_minutes":1,"tag_list":"mui, uiweekly","tags":["mui","uiweekly"],"body_html":"

Document
\nhttps://mui.com/components/app-bar/

\n\n

modules
\n

\n\n
\n
npm i @emotion/styled\nnpm i @emotion/react\nnpm i @mui/material\nnpm i @mui/icons-material\n
\n
\n
\n Enter fullscreen mode\n \n\n\n Exit fullscreen mode\n \n\n\n
\n
\n
\n\n\n\n

MUI uses emotion to style components.

\n\n

Code
\n\n

\n\n

Components

\n\n
    \n
  1. Box
  2. \n
  3. AppBar
  4. \n
  5. ToolBar
  6. \n
  7. IconButton
  8. \n
  9. MenuIcon
  10. \n
\n\n","body_markdown":"**Document**\nhttps://mui.com/components/app-bar/\n\n**modules**\n```bash\nnpm i @emotion/styled\nnpm i @emotion/react\nnpm i @mui/material\nnpm i @mui/icons-material\n```\nMUI uses `emotion` to style components.\n\n**Code**\n{% codesandbox bgqmv3 %}\n\n**Components**\n1. Box\n2. AppBar\n3. ToolBar\n4. IconButton\n5. MenuIcon\n\n","user":{"name":"artemismars","username":"artemismars","twitter_username":null,"github_username":"artemismars","user_id":818000,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--Ntb8YzKT--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/818000/289fabad-4782-40c9-8d21-e10f6b8d868a.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--qaRY3j0m--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/818000/289fabad-4782-40c9-8d21-e10f6b8d868a.png"}} -------------------------------------------------------------------------------- /public/savedData/article-1230015.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1230015,"title":"How to add schema markup to blogger","description":"Schema markup is structured data added to your website to tell google how to display and what type of...","readable_publish_date":"Oct 25","slug":"how-to-add-schema-markup-to-blogger-1knp","path":"/bensarghin/how-to-add-schema-markup-to-blogger-1knp","url":"https://dev.to/bensarghin/how-to-add-schema-markup-to-blogger-1knp","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-10-25T23:08:33Z","positive_reactions_count":3,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--UiYjlfEB--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7afpkydww90t5ddx1io9.jpg","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--CTuiMubV--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7afpkydww90t5ddx1io9.jpg","canonical_url":"https://dev.to/bensarghin/how-to-add-schema-markup-to-blogger-1knp","created_at":"2022-10-25T23:07:55Z","edited_at":"2022-11-03T14:21:16Z","crossposted_at":null,"published_at":"2022-10-25T23:08:33Z","last_comment_at":"2022-10-25T23:08:33Z","reading_time_minutes":1,"tag_list":"schemamarkup, seo, structureddata, blogger","tags":["schemamarkup","seo","structureddata","blogger"],"body_html":"

Schema markup is structured data added to your website to tell google how to display and what type of content you have.
\nSo to add schema markup to blogger.

\n\n
    \n
  • first, make sure that your blogger theme has structured data (schema markup) you can check by going to schema.org validator and putting one of your page URLs.
  • \n
\n\n

\"Image

\n\n\n\n","body_markdown":"Schema markup is structured data added to your website to tell google how to display and what type of content you have.\nSo to add schema markup to blogger.\n\n- first, make sure that your blogger theme has structured data (schema markup) you can check by going to schema.org validator and putting one of your page URLs.\n\n\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yilbav44sy6krrxjliw.png)\n\n- So if your validator doesn't detect any data:\nfollow this steps on this article [how you can add schema to your blogger](https://tissueweb.com/article/schema-markup).\n\n","user":{"name":"Hamid BENSARGHIN","username":"bensarghin","twitter_username":null,"github_username":"Bensarghin","user_id":955372,"website_url":"https://tissueweb.com","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--odnoy7h0--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/955372/f0d665cc-2a8f-4c4e-b63f-636d438b6411.jpeg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--v1lsCPmm--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/955372/f0d665cc-2a8f-4c4e-b63f-636d438b6411.jpeg"}} -------------------------------------------------------------------------------- /public/savedData/article-1242777.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1242777,"title":"Technical seo","description":"Technical search engine marketing refers to optimising your internet site for the crawling and...","readable_publish_date":"Nov 4","slug":"technical-seo-4p50","path":"/webandappstudio/technical-seo-4p50","url":"https://dev.to/webandappstudio/technical-seo-4p50","comments_count":0,"public_reactions_count":1,"collection_id":null,"published_timestamp":"2022-11-04T05:34:53Z","positive_reactions_count":1,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1242777.png","canonical_url":"https://dev.to/webandappstudio/technical-seo-4p50","created_at":"2022-11-04T05:34:54Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-04T05:34:53Z","last_comment_at":"2022-11-04T05:34:53Z","reading_time_minutes":1,"tag_list":"storybytes, seo, marketing, searchengines","tags":["storybytes","seo","marketing","searchengines"],"body_html":"

\"Image

\n\n

Technical search engine marketing refers to optimising your internet site for the crawling and indexing phase. With technical SEO, you can aid search engines in accessing, crawling, interpreting, and indexing your internet site besides any problems. It is acknowledged as “technical” because it has nothing to do with the desired content material cloth of the internet site or promotion. The fundamental reason for technical search engine optimisation is to optimise the infrastructure of a website. We at Web and App Studio have made some effort and helped many sites to improve their overall ranking since SEO is an essential step in the entire website positioning process. If there are issues with your technical SEO, then it is probably that your SEO efforts will not generate the anticipated results.

\n\n

For more details please visit:https://storybytes.webandappstudio.com/en/why-technical-seo-more-important-than-other/

\n\n","body_markdown":"\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zmus4m7djtgmghbfclb7.png)\n\nTechnical search engine marketing refers to optimising your internet site for the crawling and indexing phase. With technical SEO, you can aid search engines in accessing, crawling, interpreting, and indexing your internet site besides any problems. It is acknowledged as “technical” because it has nothing to do with the desired content material cloth of the internet site or promotion. The fundamental reason for technical search engine optimisation is to optimise the infrastructure of a website. We at Web and App Studio have made some effort and helped many sites to improve their overall ranking since SEO is an essential step in the entire website positioning process. If there are issues with your technical SEO, then it is probably that your SEO efforts will not generate the anticipated results.\n\nFor more details please visit:https://storybytes.webandappstudio.com/en/why-technical-seo-more-important-than-other/ ","user":{"name":"Web and App Studio Private Limited","username":"webandappstudio","twitter_username":"WebAndAppStudio","github_username":"webandappstudio","user_id":858438,"website_url":"https://www.webandappstudio.com","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--2f3kE-Sl--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/858438/a95ff29c-c2d1-40b1-8de1-79952a93fffb.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--kG84Ae7X--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/858438/a95ff29c-c2d1-40b1-8de1-79952a93fffb.png"}} -------------------------------------------------------------------------------- /public/savedData/article-1246943.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246943,"title":"Have you ever thought your project is getting popular?","description":"I wrote a little tool for myself to help me with generating approve messages for Pull Requests,...","readable_publish_date":"Nov 7","slug":"have-you-ever-thought-your-project-is-getting-popular-25kb","path":"/pavelkeyzik/have-you-ever-thought-your-project-is-getting-popular-25kb","url":"https://dev.to/pavelkeyzik/have-you-ever-thought-your-project-is-getting-popular-25kb","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T18:02:25Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1246943.png","canonical_url":"https://dev.to/pavelkeyzik/have-you-ever-thought-your-project-is-getting-popular-25kb","created_at":"2022-11-07T18:02:26Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T18:02:25Z","last_comment_at":"2022-11-07T18:02:25Z","reading_time_minutes":1,"tag_list":"discuss, webdev, javascript, opensource","tags":["discuss","webdev","javascript","opensource"],"body_html":"

I wrote a little tool for myself to help me with generating approve messages for Pull Requests, because I usually check PRs in the evening and my brain can't come up with something and I write \"LGTM\". But I hate LGTM comments as they doesn't provide any feedback, when I see LGTM, I usually think about \"It looks okay, merge it\".

\n\n

I'm using this app often, and at Hacktoberfest, a few people improved a little bit this app and added an ability to generate emojis, wrote few tests and now this project has 13 stars and I feel it's getting more popular and I'm so happy about.

\n\n

I know it's just a small tool, you can code in an hour, but have you ever experienced that feeling? I know it's not a huge startup, or something like that. It's more about fun and I love that someone is supporting this fun 😍

\n\n

P.S. This is the tool I'm talking about https://pr.pavelkeyzik.com

\n\n

\"Image

\n\n","body_markdown":"I wrote a little tool for myself to help me with generating approve messages for Pull Requests, because I usually check PRs in the evening and my brain can't come up with something and I write \"LGTM\". But I hate LGTM comments as they doesn't provide any feedback, when I see LGTM, I usually think about \"It looks okay, merge it\".\n\nI'm using this app often, and at Hacktoberfest, a few people improved a little bit this app and added an ability to generate emojis, wrote few tests and now this project has 13 stars and I feel it's getting more popular and I'm so happy about. \n\nI know it's just a small tool, you can code in an hour, but have you ever experienced that feeling? I know it's not a huge startup, or something like that. It's more about fun and I love that someone is supporting this fun 😍\n\nP.S. This is the tool I'm talking about https://pr.pavelkeyzik.com\n\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fp7iuyhgaarg4ufzvxji.png)\n\n","user":{"name":"Pavel Keyzik","username":"pavelkeyzik","twitter_username":"pavelkeyzik","github_username":"pavelkeyzik","user_id":114893,"website_url":"https://pavelkeyzik.github.io","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--SAVLycpc--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/114893/5d78196d-819b-4eb6-bd70-6109360237c1.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--XAgk80wm--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/114893/5d78196d-819b-4eb6-bd70-6109360237c1.jpg"},"flare_tag":{"name":"discuss","bg_color_hex":"#1ad643","text_color_hex":"#FFFFFF"}} -------------------------------------------------------------------------------- /src/components/ArticleImageList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import Image from 'next/image'; 3 | import { 4 | ImageList, ImageListItem, ImageListItemBar, IconButton, Typography, Button, Box, 5 | } from '@mui/material'; 6 | import Link from 'next/link'; 7 | import ReadMoreIcon from '@mui/icons-material/ReadMore'; 8 | import WarningIcon from '@mui/icons-material/Warning'; 9 | import { useWindowSize } from 'react-use'; 10 | import type { Article } from '../types/article-types'; 11 | import { 12 | getArticleImgUrl, getArticleLink, getFormattedDate, 13 | } from '../helpers/article-helper'; 14 | import FavoriteItemHeartIcon from './FavoriteItemHeartIcon'; 15 | 16 | interface Props { 17 | tag: string; 18 | dataItems: Article[]; 19 | onClickLoadMore: () => void; 20 | loading: boolean; 21 | isEndOfList: boolean; 22 | } 23 | 24 | const ArticleImageList = ({ 25 | tag, dataItems, loading, isEndOfList, onClickLoadMore, 26 | }:Props) => { 27 | const windowSize = useWindowSize(); 28 | 29 | const [isSmallScreen, setIsSmallScreen] = React.useState(true); 30 | 31 | useEffect(() => { 32 | if (windowSize.width < 1000) { 33 | setIsSmallScreen(true); 34 | } else { 35 | setIsSmallScreen(false); 36 | } 37 | }, [windowSize.width]); 38 | 39 | const imageItems = dataItems.filter((item) => item.cover_image && item.cover_image.trim() !== ''); 40 | 41 | return ( 42 | 43 | 44 | 45 | {tag} 46 | 47 | 48 | 49 | {imageItems && imageItems.map((item) => { 50 | const imageUrl = getArticleImgUrl(item); 51 | const subtitle = `${getFormattedDate(item.published_at)}, ${item.reading_time_minutes} minutes to read`; 52 | return ( 53 | 54 | 55 | {item.title} 61 | 69 | 70 | 71 | )} 72 | /> 73 | 74 | 75 | 76 | ); 77 | })} 78 | 79 | 80 | 88 | {`Total ${imageItems.length} ${tag} articles`} 89 | 90 | 91 | 92 | {isEndOfList ? ( 93 | 101 | 102 | ) : ( 103 | 112 | )} 113 | 114 | 115 | 116 | 117 | 118 | ); 119 | }; 120 | 121 | export default ArticleImageList; 122 | -------------------------------------------------------------------------------- /src/pages/articles/[slug].tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | GetStaticPaths, GetStaticProps, InferGetStaticPropsType, NextPage, 3 | } from 'next'; 4 | import React from 'react'; 5 | import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query'; 6 | import { END } from 'redux-saga'; 7 | import { useDispatch } from 'react-redux'; 8 | import { useEffectOnce } from 'react-use'; 9 | import { reactQueryFn } from '../../apis/article-api'; 10 | import MainLayout from '../../layouts/MainLayout'; 11 | import articleSlice from '../../redux/features/articleSlice'; 12 | import { ReduxState, reduxWrapper } from '../../redux/store'; 13 | import getRouterParam from '../../utils/get-router-param'; 14 | import { mainConfig } from '../../configs/main-config'; 15 | import getIdFromSlug from '../../utils/get-id-from-slug'; 16 | import ArticleDetail from '../../components/ArticleDetail'; 17 | import userSlice from '../../redux/features/userSlice'; 18 | import HeadMeta from '../../layouts/HeadMeta'; 19 | import { Article } from '../../types/article-types'; 20 | 21 | const fs = require('fs'); 22 | 23 | const ArticleDetails: NextPage = ({ 24 | serverRedux, 25 | articleId, 26 | } 27 | :InferGetStaticPropsType) => { 28 | const reduxDispatch = useDispatch(); 29 | 30 | const { data:reactQueryData } = useQuery( 31 | ['articles', { id: articleId }], 32 | reactQueryFn.getArticleDetail, 33 | { enabled: !mainConfig.isReduxForStaticPropsEnabled && articleId > 0 }, 34 | ); 35 | 36 | const articleDetail = mainConfig.isReduxForStaticPropsEnabled 37 | ? serverRedux?.article.detail 38 | : reactQueryData; 39 | 40 | useEffectOnce( 41 | () => { 42 | if (mainConfig.isStaticPageDebugDisabled 43 | && mainConfig.isClientSide && articleDetail?.id > 0) { 44 | reduxDispatch(userSlice.actions.recentItemRequest(articleDetail)); 45 | } 46 | }, 47 | ); 48 | 49 | return ( 50 | 51 | 52 | {articleDetail && } 53 | 54 | ); 55 | }; 56 | 57 | export const getStaticPropsFromReactQuery: GetStaticProps = async ({ params }) => { 58 | const queryClient = new QueryClient(); 59 | 60 | const articleId = getIdFromSlug(getRouterParam(params?.slug)) || 0; 61 | 62 | await queryClient.prefetchQuery( 63 | ['articles', { id: articleId }], 64 | reactQueryFn.getArticleDetail, 65 | ); 66 | 67 | return { 68 | props: { 69 | articleId, 70 | dehydratedState: dehydrate(queryClient), 71 | }, 72 | revalidate: mainConfig.detailPageRefreshInterval, 73 | }; 74 | }; 75 | 76 | export const getStaticPropsFromRedux: GetStaticProps = reduxWrapper.getStaticProps( 77 | (store) => async ({ params }) => { 78 | const articleId = getIdFromSlug(getRouterParam(params?.slug)) || 0; 79 | 80 | store.dispatch(articleSlice.actions.getArticleDetailRequest({ id: articleId })); 81 | store.dispatch(END); 82 | await store.sagaTask.toPromise(); 83 | 84 | return { 85 | props: { 86 | articleId, 87 | serverRedux: store.getState() as ReduxState, 88 | }, 89 | revalidate: mainConfig.detailPageRefreshInterval, 90 | notFound : articleId < 1, 91 | }; 92 | }, 93 | ); 94 | 95 | export const getStaticPaths: GetStaticPaths = async () => { 96 | const files:string[] = fs.readdirSync(mainConfig.dataFilePath); 97 | const allSlugs = files.map((file) => { 98 | if (!file.includes('tag-')) { 99 | return []; 100 | } 101 | const articleData = JSON.parse(fs.readFileSync(`${mainConfig.dataFilePath}/${file}`, 'utf8')); 102 | if (Array.isArray(articleData) && articleData.length && 'id' in articleData[0] && 'slug' in articleData[0] && articleData[0].cover_image) { 103 | return articleData.map((article:Article) => `${article.slug}-${article.id}`); 104 | } 105 | 106 | return []; 107 | }).flat(); 108 | 109 | const paths = allSlugs.slice(0, 500).map((slug) => ({ 110 | params: { 111 | slug, 112 | }, 113 | })); 114 | return { paths, fallback: true }; 115 | }; 116 | 117 | export const getStaticProps = mainConfig.isReduxForStaticPropsEnabled 118 | ? getStaticPropsFromRedux 119 | : getStaticPropsFromReactQuery; 120 | 121 | export default ArticleDetails; 122 | -------------------------------------------------------------------------------- /src/pages/articles/tag/[tag].tsx: -------------------------------------------------------------------------------- 1 | import type { GetStaticPaths, GetStaticProps, NextPage } from 'next'; 2 | import React, { useEffect } from 'react'; 3 | import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { END } from 'redux-saga'; 6 | import { useRouter } from 'next/router'; 7 | import { reactQueryFn } from '../../../apis/article-api'; 8 | import ArticleImageList from '../../../components/ArticleImageList'; 9 | import MainLayout from '../../../layouts/MainLayout'; 10 | import articleSlice from '../../../redux/features/articleSlice'; 11 | import { ReduxState, reduxWrapper } from '../../../redux/store'; 12 | import getRouterParam from '../../../utils/get-router-param'; 13 | import { DEFAULT_KEYWORD, TOP_MENU_TAGS, ITEMS_PER_PAGE } from '../../../constants/article-const'; 14 | import { mainConfig } from '../../../configs/main-config'; 15 | import userSlice from '../../../redux/features/userSlice'; 16 | import HeadMeta from '../../../layouts/HeadMeta'; 17 | 18 | const Articles: NextPage = () => { 19 | const router = useRouter(); 20 | const originalTag = getRouterParam(router.query.tag, DEFAULT_KEYWORD); 21 | const tag = originalTag.toLowerCase(); 22 | const pageRef = React.useRef(1); 23 | 24 | // Redux usage 25 | const reduxDispatch = useDispatch(); 26 | const reduxArticle = useSelector((reduxState: ReduxState) => reduxState.article); 27 | 28 | // reset pageRef.current if search different tag 29 | if (reduxArticle.searchTag !== '' && reduxArticle.searchTag !== tag) { 30 | pageRef.current = 1; 31 | } 32 | 33 | // React Query usage 34 | const { data:rqDataItems } = useQuery( 35 | ['articles', { tag, page: pageRef.current }], 36 | reactQueryFn.getArticles, 37 | { enabled: !mainConfig.isReduxForStaticPropsEnabled }, 38 | ); 39 | 40 | const dataItems = mainConfig.isReduxForStaticPropsEnabled ? reduxArticle.lists : rqDataItems; 41 | const isLoading = reduxArticle.status === 'loading'; 42 | const isEndOfList = !isLoading && dataItems.length > 0 43 | && dataItems.length < pageRef.current * ITEMS_PER_PAGE; 44 | 45 | const onClickLoadMore = () => { 46 | reduxDispatch(articleSlice.actions.getArticlesRequest({ tag, page: pageRef.current + 1 })); 47 | pageRef.current += 1; 48 | }; 49 | 50 | useEffect( 51 | () => { 52 | if (mainConfig.isStaticPageDebugDisabled) { 53 | reduxDispatch(articleSlice.actions.getArticlesRequest({ tag, page: pageRef.current })); 54 | } 55 | reduxDispatch(userSlice.actions.visitRequest()); 56 | }, 57 | [reduxDispatch, tag], 58 | ); 59 | 60 | return ( 61 | 62 | 63 | 70 | 71 | ); 72 | }; 73 | 74 | export const getStaticPropsFromReactQuery: GetStaticProps = async ({ params }) => { 75 | const queryClient = new QueryClient(); 76 | 77 | const tag = getRouterParam(params?.tag, DEFAULT_KEYWORD).toLowerCase(); 78 | const page = parseInt(getRouterParam(params?.page, '1'), 10); 79 | 80 | await queryClient.prefetchQuery( 81 | ['articles', { page, tag }], 82 | reactQueryFn.getArticles, 83 | ); 84 | 85 | return { 86 | props: { 87 | dehydratedState: dehydrate(queryClient), 88 | }, 89 | revalidate: mainConfig.listPageRefreshInterval, 90 | }; 91 | }; 92 | 93 | export const getStaticPropsFromRedux: GetStaticProps = reduxWrapper.getStaticProps( 94 | (store) => async ({ params }) => { 95 | const tag = getRouterParam(params?.tag, DEFAULT_KEYWORD).toLowerCase(); 96 | const page = parseInt(getRouterParam(params?.page, '1'), 10); 97 | // consoleLog('🚀 ~ file: [tag].tsx ~ line 75 ~ params', params); 98 | 99 | store.dispatch(articleSlice.actions.getArticlesRequest({ tag, page })); 100 | store.dispatch(END); 101 | await store.sagaTask.toPromise(); 102 | 103 | return { 104 | props: { 105 | tag, 106 | page, 107 | }, 108 | revalidate: mainConfig.listPageRefreshInterval, 109 | }; 110 | }, 111 | ); 112 | 113 | export const getStaticPaths: GetStaticPaths = async () => { 114 | const paths = TOP_MENU_TAGS.map((item: string) => ({ 115 | params: { 116 | tag: item, 117 | }, 118 | })); 119 | return { paths, fallback: true }; 120 | }; 121 | 122 | export const getStaticProps = mainConfig.isReduxForStaticPropsEnabled 123 | ? getStaticPropsFromRedux 124 | : getStaticPropsFromReactQuery; 125 | 126 | export default Articles; 127 | -------------------------------------------------------------------------------- /public/savedData/article-1241056.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1241056,"title":"Application Load Balancer Pricing - FinOps","description":"I would like to thank the people who gave me some feeback on my first article. On this one, I...","readable_publish_date":"Nov 2","slug":"application-load-balancer-pricing-jl4","path":"/wbattou/application-load-balancer-pricing-jl4","url":"https://dev.to/wbattou/application-load-balancer-pricing-jl4","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-11-02T23:23:34Z","positive_reactions_count":3,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1241056.png","canonical_url":"https://dev.to/wbattou/application-load-balancer-pricing-jl4","created_at":"2022-11-02T23:23:34Z","edited_at":"2022-11-03T08:06:26Z","crossposted_at":null,"published_at":"2022-11-02T23:23:34Z","last_comment_at":"2022-11-02T23:23:34Z","reading_time_minutes":1,"tag_list":"aws, finops, costoptimization, costmanagement","tags":["aws","finops","costoptimization","costmanagement"],"body_html":"

\n

\n\n

I would like to thank the people who gave me some feeback on my first article.

\n\n

On this one, I made an animation about the pricing of an Application Load Balancer on AWS and my FinOps recommendations for it.

\n\n

\n \n \n If you still have some Classic Load Balancer in place, you pay for DataProcessing and not LCU. Migrate CLB to ALB to save cost.\n

\n\n

The CLB (Classic Load Balancer) was released in 2009, ALB (Application Load Balancer) was released in 2016. If you started on AWS before 2016, you had no choice but taking CLB.

\n\n

Check your workloads and do the migration.

\n\n

\n \n \n If you don’t use API Gateway features, challenge the migration to an ALB to reduce cost.\n

\n\n

This recommendation applies to a Serverless use case. When you start using Lambda for building an API, the default choice is API Gateway.

\n\n

Since 2018, it has been possible to use an ALB to invoke lambda functions and serve HTTP(S) requests, which can be cheaper.

\n\n

\n \n \n When enabling access log, do not forget to define s3 retention policy.\n

\n\n

When using an ALB, think about defining your observability strategy :

\n\n
    \n
  • How do I check my logs ?
  • \n
  • Which dashboard will I use ?
  • \n
  • I store it on s3 but which period do I need to archive ?
  • \n
\n\n","body_markdown":"{% youtube sKGF00NhWcY %}\n\nI would like to thank the people who gave me some feeback on my first article.\n\nOn this one, I made an animation about the pricing of an Application Load Balancer on AWS and my FinOps recommendations for it.\n\n## If you still have some Classic Load Balancer in place, you pay for DataProcessing and not LCU. Migrate CLB to ALB to save cost.\n\nThe CLB (Classic Load Balancer) was released in 2009, ALB (Application Load Balancer) was released in 2016. If you started on AWS before 2016, you had no choice but taking CLB.\n\nCheck your workloads and do the migration.\n\n## If you don’t use API Gateway features, challenge the migration to an ALB to reduce cost.\n\nThis recommendation applies to a Serverless use case. When you start using Lambda for building an API, the default choice is API Gateway.\n\nSince 2018, it has been possible to use an ALB to invoke lambda functions and serve HTTP(S) requests, which can be cheaper.\n\n## When enabling access log, do not forget to define s3 retention policy.\n\nWhen using an ALB, think about defining your observability strategy :\n\n- How do I check my logs ?\n- Which dashboard will I use ?\n- I store it on s3 but which period do I need to archive ?\n","user":{"name":"Walid BATTOU","username":"wbattou","twitter_username":"WBattou","github_username":null,"user_id":956641,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--JSe4plE---/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/956641/8bfd3469-e299-4132-a538-4fc99892bb66.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--9v6RENHJ--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/956641/8bfd3469-e299-4132-a538-4fc99892bb66.jpg"}} -------------------------------------------------------------------------------- /public/savedData/article-1246947.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246947,"title":"I Created a pure Unicode Code Highlighter :P","description":"It is a simple parser and module that modifies unicode variation of string parts to accomplish code highlighting","readable_publish_date":"Nov 7","slug":"i-created-a-pure-unicode-code-highlighter-p-ong","path":"/felipperegazio/i-created-a-pure-unicode-code-highlighter-p-ong","url":"https://dev.to/felipperegazio/i-created-a-pure-unicode-code-highlighter-p-ong","comments_count":1,"public_reactions_count":4,"collection_id":null,"published_timestamp":"2022-11-07T18:09:24Z","positive_reactions_count":4,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1246947.png","canonical_url":"https://dev.to/felipperegazio/i-created-a-pure-unicode-code-highlighter-p-ong","created_at":"2022-11-07T18:09:13Z","edited_at":"2022-11-07T18:10:59Z","crossposted_at":null,"published_at":"2022-11-07T18:09:24Z","last_comment_at":"2022-11-07T23:57:07Z","reading_time_minutes":1,"tag_list":"showdev, toyproject, javascript, webdev","tags":["showdev","toyproject","javascript","webdev"],"body_html":"

The tool is called \"Unilight\". It is a simple parser and module that modifies unicode variation of string parts.

\n\n

So you can highlight anywhere that doesn't accept rich editing, just have a pure string that accepts unicode variations. Works for almost any lang, take a look, this is a simple and pure STRING haha:

\n\n

.......
\n// 𝕋𝕙𝕚𝕤 𝕚𝕤 𝕒𝕟 𝕖𝕩𝕒𝕞𝕡𝕝𝕖

\n\n

𝗳𝘂𝗻𝗰𝘁𝗶𝗼𝗻 example() {
\n 𝗰𝗼𝗻𝘀𝘁 a = \"fizz\";
\n 𝗰𝗼𝗻𝘀𝘁 b = \"buzz\";
\n 𝗿𝗲𝘁𝘂𝗿𝗻 a+b;
\n}
\n.......

\n\n

You can highlight your code here:
\nhttps://felippe-regazio.github.io/unilight/

\n\n

Source code and documentation here:
\nhttps://github.com/felippe-regazio/unilight

\n\n

Warning:

\n\n

This is a toy project made for aesthetic reasons only (just wanted to code). I made it because I wanted to study a few things about parsers and do something with it. However, there are important weaknesses of this technique:

\n\n
    \n
  1. Systems that do not support unicode will not display the text

  2. \n
  3. Since unicode variations are hidden characters, the string will always be longer than it looks

  4. \n
  5. Interpreters will not run the code because they don't know these chars, it will have to be sanitized

  6. \n
\n\n

The tool is more useful for aesthetic reasons in fact, you can highlight simple snippets on a tweet, for example. I think the most interesting thing here is: The source code and the relationships between it (if you're a beginner/intermediate it might be a good thing to see given its simplicity), and the parser (if you're more advanced in js, good luck with that).

\n\n","body_markdown":"---\ntitle: I Created a pure Unicode Code Highlighter :P\npublished: true\ndescription: It is a simple parser and module that modifies unicode variation of string parts to accomplish code highlighting\ntags: #showdev #toyproject #javascript #webdev\n# cover_image: https://direct_url_to_image.jpg\n# Use a ratio of 100:42 for best results.\n# published_at: 2022-11-07 17:59 +0000\n---\n\nThe tool is called \"Unilight\". It is a simple parser and module that modifies unicode variation of string parts.\n\nSo you can highlight anywhere that doesn't accept rich editing, just have a pure string that accepts unicode variations. Works for almost any lang, take a look, this is a simple and pure STRING haha:\n\n.......\n// 𝕋𝕙𝕚𝕤 𝕚𝕤 𝕒𝕟 𝕖𝕩𝕒𝕞𝕡𝕝𝕖\n\n𝗳𝘂𝗻𝗰𝘁𝗶𝗼𝗻 example() {\n 𝗰𝗼𝗻𝘀𝘁 a = \"fizz\";\n 𝗰𝗼𝗻𝘀𝘁 b = \"buzz\";\n 𝗿𝗲𝘁𝘂𝗿𝗻 a+b;\n}\n.......\n\nYou can highlight your code here: \nhttps://felippe-regazio.github.io/unilight/\n\nSource code and documentation here:\nhttps://github.com/felippe-regazio/unilight\n\nWarning:\n\nThis is a toy project made for aesthetic reasons only (just wanted to code). I made it because I wanted to study a few things about parsers and do something with it. However, there are important weaknesses of this technique:\n\n1. Systems that do not support unicode will not display the text\n\n2. Since unicode variations are hidden characters, the string will always be longer than it looks\n\n3. Interpreters will not run the code because they don't know these chars, it will have to be sanitized\n\nThe tool is more useful for aesthetic reasons in fact, you can highlight simple snippets on a tweet, for example. I think the most interesting thing here is: The source code and the relationships between it (if you're a beginner/intermediate it might be a good thing to see given its simplicity), and the parser (if you're more advanced in js, good luck with that).\n","user":{"name":"Felippe Regazio","username":"felipperegazio","twitter_username":"FelippeRegazio","github_username":"felippe-regazio","user_id":59890,"website_url":"https://www.cvkeep.com/cv/felipperegazio","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--1dlAClOG--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/59890/666ef0f9-9a40-4b1b-8c87-f0d3180dbf4d.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--O6CPDy3U--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/59890/666ef0f9-9a40-4b1b-8c87-f0d3180dbf4d.jpg"},"flare_tag":{"name":"showdev","bg_color_hex":"#091b47","text_color_hex":"#b2ffe1"}} -------------------------------------------------------------------------------- /public/savedData/article-1237647.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1237647,"title":"Provision Amazon EKS cluster with EKSCTL","description":"In this article, we are going to spain up an eks with Ngnix deploy. install eksctl Log on to AWS...","readable_publish_date":"Oct 31","slug":"provision-amazon-eks-cluster-with-eksctl-c3g","path":"/catherine2z/provision-amazon-eks-cluster-with-eksctl-c3g","url":"https://dev.to/catherine2z/provision-amazon-eks-cluster-with-eksctl-c3g","comments_count":0,"public_reactions_count":3,"collection_id":null,"published_timestamp":"2022-10-31T21:57:36Z","positive_reactions_count":3,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1237647.png","canonical_url":"https://dev.to/catherine2z/provision-amazon-eks-cluster-with-eksctl-c3g","created_at":"2022-10-31T21:57:36Z","edited_at":null,"crossposted_at":null,"published_at":"2022-10-31T21:57:36Z","last_comment_at":"2022-10-31T21:57:36Z","reading_time_minutes":2,"tag_list":"awscommunitybuilder, aws, eks","tags":["awscommunitybuilder","aws","eks"],"body_html":"

In this article, we are going to spain up an eks with Ngnix deploy.
\ninstall eksctl

\n\n

Log on to AWS Console. Click on IAM, then Roles, and click Create Role.

\n\n

install AWS cli
\ncurl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"
\nunzip awscliv2.zip
\nsudo ./aws/install

\n\n

Confirm your version
\naws --version

\n\n

Install kubectl on Linux
\nKubernetes 1.23
\ncurl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.23.7/2022-06-29/bin/linux/amd64/kubectl
\nGive execute command to the binary
\nchmod +x ./kubectl
\nCopy the binary to a folder in your PATH
\nmkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin
\nConfirm your version
\nkubectl version --short --client

\n\n

Install eskctl
\ncurl --silent --location \"https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz\" | tar xz -C /tmp

\n\n

Move the extracted binary to /usr/local/bin.

\n\n

sudo mv /tmp/eksctl /usr/local/bin
\nConfirm your installation version
\neksctl version

\n\n

Set up your permission
\nWe will be setting up our I AM permission

\n\n

Create your Amazon EKS cluster and nodes
\neksctl create cluster --version=1.23 --name=K8cluster --nodes=3 --managed --region=us-east-1 --node-type t3.medium --asg-access

\n\n

N.B Creating of cluster take up to 15mins, once this is successfully created you will see the below
\n[✓] EKS cluster \"my-cluster\" in \"region-code\" region is ready

\n\n","body_markdown":"In this article, we are going to spain up an eks with Ngnix deploy.\ninstall [eksctl](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html)\n\nLog on to AWS Console. Click on IAM, then Roles, and click Create Role.\n\ninstall AWS [cli ](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)\ncurl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"\nunzip awscliv2.zip\nsudo ./aws/install\n\nConfirm your version \naws --version\n\nInstall [kubectl](https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html) on Linux \nKubernetes 1.23\ncurl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.23.7/2022-06-29/bin/linux/amd64/kubectl\nGive execute command to the binary\nchmod +x ./kubectl\nCopy the binary to a folder in your PATH\nmkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin\nConfirm your version \nkubectl version --short --client\n\nInstall [eskctl](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html)\ncurl --silent --location \"https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz\" | tar xz -C /tmp\n\nMove the extracted binary to /usr/local/bin.\n\nsudo mv /tmp/eksctl /usr/local/bin\nConfirm your installation version\neksctl version\n\nSet up your permission\nWe will be setting up our I AM permission\n\nCreate your Amazon EKS cluster and nodes\neksctl create cluster --version=1.23 --name=K8cluster --nodes=3 --managed --region=us-east-1 --node-type t3.medium --asg-access\n\nN.B Creating of cluster take up to 15mins, once this is successfully created you will see the below\n[✓] EKS cluster \"my-cluster\" in \"region-code\" region is ready\n\n\n\n\n\n\n\n","user":{"name":"Catherine John","username":"catherine2z","twitter_username":"Catherine2z","github_username":null,"user_id":958512,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--VR7Mp5lG--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/958512/8b488514-f1dd-4420-aa79-77f2d0dad862.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--jUh0JbDx--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/958512/8b488514-f1dd-4420-aa79-77f2d0dad862.jpg"}} -------------------------------------------------------------------------------- /public/savedData/article-1241941.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1241941,"title":"UI testing safe checks on Appium","description":"The User Interface (UI) of an app is the visual representation of your app and what the users largely...","readable_publish_date":"Nov 3","slug":"ui-testing-safe-checks-on-appium-54ke","path":"/benthemobileguy/ui-testing-safe-checks-on-appium-54ke","url":"https://dev.to/benthemobileguy/ui-testing-safe-checks-on-appium-54ke","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-03T16:46:33Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1241941.png","canonical_url":"https://dev.to/benthemobileguy/ui-testing-safe-checks-on-appium-54ke","created_at":"2022-11-03T16:46:33Z","edited_at":"2022-11-03T17:00:12Z","crossposted_at":null,"published_at":"2022-11-03T16:46:33Z","last_comment_at":"2022-11-03T16:46:33Z","reading_time_minutes":1,"tag_list":"appium, reactnative, javascript","tags":["appium","reactnative","javascript"],"body_html":"

The User Interface (UI) of an app is the visual representation of your app and what the users largely interact with. A glitchy UI interpretation of your app can negatively impact your brand, this makes UI testing a necessity before an application can be released to production.

\n\n

This article is aimed at exploring some automated UI testing safe checks we can use to ensure our UI implementation is predictable and more solid.

\n\n

Note: All approaches will be focused with Appium (an open-source tool for automating native, mobile web, and hybrid applications on iOS mobile, Android mobile, and Windows desktop platforms ).

\n\n

Check visibility of elements

\n\n

Lets say you want to check for a visibility of a splash screen, you can check if the screen is displayed using isDisplayed() method.

\n\n

List<MobileElement> element =
\ndriver.findElementsByAccessibilityId(\"automation id here\");
\nif(element.size()>0){
\n//screen is displayed
\n} else{
\n//screen is not displayed }
\nKeyboard visible

\n\n

In Appium, we can use the isKeyboardShown keyword to ascertain whether or not the soft keyboard is shown.
\n
\ndriver = new AndroidDriver(appiumURL, capabilities);
\nboolean isDisplayed = driver.isKeyboardShown();

\n\n

Element over element

\n\n

Also, in case we want to check if an element is inside another element, we can use an approach like this.

\n\n

private WebElement element=
\ndriver.findElementByAccessibilityId(\"First element\")
\nfindElementByAccessibilityId(\"Second element\");

\n\n

Further Reading

\n\n

Is Keyboard Shown — Appium

\n\n

Testing your React Native app with Appium — LogRocket Blog

\n\n

Appium mobile testing — test React Native apps | Codemagic Blog

\n\n","body_markdown":"The User Interface (UI) of an app is the visual representation of your app and what the users largely interact with. A glitchy UI interpretation of your app can negatively impact your brand, this makes UI testing a necessity before an application can be released to production.\n\nThis article is aimed at exploring some automated UI testing safe checks we can use to ensure our UI implementation is predictable and more solid.\n\nNote: All approaches will be focused with Appium (an open-source tool for automating native, mobile web, and hybrid applications on iOS mobile, Android mobile, and Windows desktop platforms ).\n\n**Check visibility of elements**\n\nLets say you want to check for a visibility of a splash screen, you can check if the screen is displayed using isDisplayed() method.\n\n\n`List element = \ndriver.findElementsByAccessibilityId(\"automation id here\"); \nif(element.size()>0){\n//screen is displayed \n} else{ \n//screen is not displayed }\nKeyboard visible`\n\nIn Appium, we can use the isKeyboardShown keyword to ascertain whether or not the soft keyboard is shown.\n`\ndriver = new AndroidDriver(appiumURL, capabilities); \nboolean isDisplayed = driver.isKeyboardShown();`\n\n**Element over element**\n\nAlso, in case we want to check if an element is inside another element, we can use an approach like this.\n\n`private WebElement element= \ndriver.findElementByAccessibilityId(\"First element\")\nfindElementByAccessibilityId(\"Second element\");`\n\n**Further Reading**\n\n[Is Keyboard Shown — Appium](https://appium.io/docs/en/commands/device/keys/is-keyboard-shown/)\n\n[Testing your React Native app with Appium — LogRocket Blog](https://blog.logrocket.com/testing-your-react-native-app-with-appium/)\n\n[Appium mobile testing — test React Native apps | Codemagic Blog](https://blog.codemagic.io/mobile-testing-appium-react-native-apps/)","user":{"name":"Ben Chukwuma","username":"benthemobileguy","twitter_username":null,"github_username":"benthemobileguy","user_id":964348,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--KPJKvb1A--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/964348/1c26c80e-9d2c-4a2f-82ad-f769ded0c5ad.jpeg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--g4_jCxeI--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/964348/1c26c80e-9d2c-4a2f-82ad-f769ded0c5ad.jpeg"}} -------------------------------------------------------------------------------- /src/components/ArticleDetail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { styled } from '@mui/material/styles'; 3 | import IconButton, { IconButtonProps } from '@mui/material/IconButton'; 4 | import { red } from '@mui/material/colors'; 5 | import ShareIcon from '@mui/icons-material/Share'; 6 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 7 | import { useSelector } from 'react-redux'; 8 | import { useState } from 'react'; 9 | import { 10 | Avatar, Box, Card, CardActions, CardContent, 11 | CardHeader, CardMedia, Chip, Collapse, Fab, Typography, 12 | } from '@mui/material'; 13 | import MoreIcon from '@mui/icons-material/More'; 14 | import { useRouter } from 'next/router'; 15 | import Link from 'next/link'; 16 | import { Article, UserSliceType } from '../types/article-types'; 17 | import { 18 | getArticleImgUrl, getArticleTags, getFormattedDate, getTagLink, 19 | } from '../helpers/article-helper'; 20 | import { ReduxState } from '../redux/store'; 21 | import FavoriteItemHeartIcon from './FavoriteItemHeartIcon'; 22 | import SearchBar from './SearchBar'; 23 | 24 | interface ExpandMoreProps extends IconButtonProps { 25 | expand: boolean; 26 | } 27 | 28 | const ExpandMore = styled((props: ExpandMoreProps) => { 29 | const { expand, ...other } = props; 30 | // eslint-disable-next-line react/jsx-props-no-spreading 31 | return ; 32 | })(({ theme, expand }) => ({ 33 | transform : !expand ? 'rotate(0deg)' : 'rotate(180deg)', 34 | marginLeft: 'auto', 35 | transition: theme.transitions.create('transform', { 36 | duration: theme.transitions.duration.shortest, 37 | }), 38 | })); 39 | 40 | interface Props { 41 | article: Article; 42 | } 43 | 44 | const ArticleDetail = ({ article }:Props) => { 45 | const router = useRouter(); 46 | 47 | const reduxUserData:UserSliceType = useSelector((reduxState: ReduxState) => reduxState.user); 48 | 49 | const [expanded, setExpanded] = useState(true); 50 | 51 | const isFavorite = reduxUserData.favoriteItems.some((item) => item.id === article.id); 52 | 53 | const allTags = getArticleTags(article); 54 | 55 | const handleExpandClick = () => { 56 | setExpanded(!expanded); 57 | }; 58 | 59 | return ( 60 | 61 | 68 | {article.user.name} 69 | 70 | )} 71 | action={( 72 | router.back()}> 73 | 74 | 75 | )} 76 | title={article.title} 77 | subheader={`${article.user.name} - ${getFormattedDate(article.published_at, 'MM/dd/yyyy EEEE HH:mm')}`} 78 | /> 79 | 85 | 86 | 87 | {article.description} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {article.title} 108 | 109 | 110 |
114 | 115 | 116 | {allTags.length > 0 && ( 117 | 118 | 124 | )} 125 | label={article.user.name} 126 | variant="outlined" 127 | sx={{ margin: '0 1rem 1rem 0', background: '#f5f5f5' }} 128 | /> 129 | 130 | {allTags.map((tag) => ( 131 | 132 | 133 | 134 | ))} 135 | 136 | 137 | 138 | )} 139 | 140 | 141 | 142 | 143 | theme.spacing(20), 148 | right : (theme) => theme.spacing(3), 149 | }} 150 | color={isFavorite ? 'inherit' : 'default'} 151 | > 152 | 161 | 162 | 163 | 164 | ); 165 | }; 166 | 167 | export default ArticleDetail; 168 | -------------------------------------------------------------------------------- /public/savedData/article-1246326.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246326,"title":"Very Large Delay Value In setTimeout","description":"Hello Dev, In this article, I will explain you 1 very important limitation of setTimeout in...","readable_publish_date":"Nov 7","slug":"very-large-delay-value-in-settimeout-51m8","path":"/capscode/very-large-delay-value-in-settimeout-51m8","url":"https://dev.to/capscode/very-large-delay-value-in-settimeout-51m8","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T11:00:14Z","positive_reactions_count":0,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--S8Rb0H-A--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u90stihw0gwbv2h2koge.jpg","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--dOALdslb--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u90stihw0gwbv2h2koge.jpg","canonical_url":"https://www.capscode.in/blog/large-delay-value-in-setTimeout","created_at":"2022-11-07T11:00:14Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T11:00:14Z","last_comment_at":"2022-11-07T11:00:14Z","reading_time_minutes":1,"tag_list":"webdev, javascript, react, node","tags":["webdev","javascript","react","node"],"body_html":"

Hello Dev,

\n\n

In this article, I will explain you 1 very important limitation of setTimeout in JavaScript.

\n\n

Browsers including Internet Explorer, Chrome, Safari, and Firefox store the delay as a 32-bit signed integer internally. This causes an integer overflow when using delays larger than 2,147,483,647 ms (about 24.8 days), resulting in the timeout being executed immediately.
\n

\n\n
\n
function func(){\n    setTimeout(()=>{\n        console.log('Print hello after 2,147,483,648 milliseconds')\n    }, 2147483648)\n}\n\nfunc() \n\n
\n
\n
\n Enter fullscreen mode\n \n\n\n Exit fullscreen mode\n \n\n\n
\n
\n
\n\n\n\n

Expected output/ behavior for the above code should be the callback function of setTimeout will execute after 2,147,483,648 milliseconds.

\n\n

But the callback function will get executed immediately because the maximum delay value in setTimeout is 2,147,483,647 milliseconds and we have provided 2,147,483,648 milliseconds as delay.

\n\n

Thank you for reading this so far. This is a brief introduction on How Large Delay Value In setTimeout Acts in JavaScript.

\n\n

Hope it's a nice and informative read for you.
\nIf you find this article useful, like and share this article. Someone could find it useful too.

\n\n

If you find anything technically inaccurate, please feel free to reach out to us.

\n\n

VISIT https://www.capscode.in/blog TO LEARN MORE.

\n\n

See you in my next Blog article, Take care!!

\n\n

Thanks,
\nCapsCode

\n\n","body_markdown":"Hello Dev,\n\nIn this article, I will explain you 1 very important limitation of setTimeout in JavaScript.\n\nBrowsers including Internet Explorer, Chrome, Safari, and Firefox store the delay as a 32-bit signed integer internally. This causes an integer overflow when using delays larger than **2,147,483,647 ms (about 24.8 days)**, resulting in the timeout being executed immediately.\n\n\n```js\nfunction func(){\n setTimeout(()=>{\n console.log('Print hello after 2,147,483,648 milliseconds')\n }, 2147483648)\n}\n\nfunc() \n\n```\n\nExpected output/ behavior for the above code should be the callback function of setTimeout will execute after **2,147,483,648 milliseconds**.\n\nBut the callback function will get executed immediately because the maximum delay value in `setTimeout` is **2,147,483,647 milliseconds** and we have provided **2,147,483,648 milliseconds** as delay.\n\n\nThank you for reading this so far. This is a brief introduction on **How Large Delay Value In `setTimeout` Acts in JavaScript**.\n\nHope it's a nice and informative read for you.\nIf you find this article useful, like and share this article. Someone could find it useful too.\n\nIf you find anything technically inaccurate, please feel free to reach out to us.\n\nVISIT https://www.capscode.in/blog TO LEARN MORE.\n\nSee you in my next Blog article, Take care!!\n\nThanks,\nCapsCode","user":{"name":"capscode","username":"capscode","twitter_username":"capscodeindia","github_username":"capscode","user_id":518225,"website_url":"https://www.capscode.in","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--uB7PYKc_--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/518225/2b5ce408-e9e0-4232-94c9-95e55d3e3c15.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--wdsiuQMO--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/518225/2b5ce408-e9e0-4232-94c9-95e55d3e3c15.png"}} -------------------------------------------------------------------------------- /public/savedData/article-1241348.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1241348,"title":"Need help ! Issue with Auto Scaling for Application Load Balancer with 2 different instances for target groups","description":"What is your question/issue (provide as much detail as possible)? Hi All, I currently have an...","readable_publish_date":"Nov 3","slug":"need-help-issue-with-auto-scaling-for-application-load-balancer-with-2-different-instances-for-target-groups-gp0","path":"/deborahcat/need-help-issue-with-auto-scaling-for-application-load-balancer-with-2-different-instances-for-target-groups-gp0","url":"https://dev.to/deborahcat/need-help-issue-with-auto-scaling-for-application-load-balancer-with-2-different-instances-for-target-groups-gp0","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-03T08:03:06Z","positive_reactions_count":0,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1241348.png","canonical_url":"https://dev.to/deborahcat/need-help-issue-with-auto-scaling-for-application-load-balancer-with-2-different-instances-for-target-groups-gp0","created_at":"2022-11-03T08:01:36Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-03T08:03:06Z","last_comment_at":"2022-11-03T08:03:06Z","reading_time_minutes":2,"tag_list":"help, aws, ec2","tags":["help","aws","ec2"],"body_html":"

What is your question/issue (provide as much detail as possible)?

\n\n

Hi All, I currently have an Application Load Balancer that has two rules :

\n\n

If the request is coming from a specific path Example: /test/, use a target group that points to an instance ( Let's say instance b ). For any other request use this target group ( another instance a ) In the autoscaling settings I have added these two target groups so that if autoscaling occurs and terminates the instances, they will be re-registered. This works correctly, however, I noticed that when termination happens the target group registers instance a where only instance b needs to be registered.

\n\n

I do have 2 ASGs, one for the instance a and one for another instance b. However, I mostly use the main site Load balancer as the rules are located there ( if the path of /test/* it uses the instance b ( target group ). The reason this was set like this is that the instance b is accessed by the main site domain. ( For example www.example.com/test ) The desired count is set to 1.
\nSince I'm using the ALB of the main site only when the ASG terminates the instance, a new target is registered for the main site in the target group of the instance b. My issue is that this target group should only register one instance and not 2 ( instance a ( main site ) and instance b )

\n\n

Would really appreciate some guidance, haven't received much from Re:post :(

\n\n

What technologies are you using?

\n\n

AWS, AutoScaling, EC2, ElasticBeanstalk

\n\n

What were you expecting to happen?

\n\n

When Autoscaling terminates the instance, the target group that has instance b registers, initiates another instance and places the same instance b and not another instance a.

\n\n

What is actually happening?

\n\n

When Autoscaling terminates the instance, the target group is adding another instance a and therefore the site breaks as the incorrect server is being shown.

\n\n

What have you already tried/thought about?

\n\n

Maybe having one autoscaling group for two different instances, however didn't manage. But there is still the issue of cannot control which instance is added to the target group when its terminated.

\n\n","body_markdown":"\n\nWhat is your question/issue (provide as much detail as possible)?\n\nHi All, I currently have an Application Load Balancer that has two rules :\n\nIf the request is coming from a specific path Example: /test/, use a target group that points to an instance ( Let's say instance b ). For any other request use this target group ( another instance a ) In the autoscaling settings I have added these two target groups so that if autoscaling occurs and terminates the instances, they will be re-registered. This works correctly, however, I noticed that when termination happens the target group registers instance a where only instance b needs to be registered.\n\nI do have 2 ASGs, one for the instance a and one for another instance b. However, I mostly use the main site Load balancer as the rules are located there ( if the path of /test/* it uses the instance b ( target group ). The reason this was set like this is that the instance b is accessed by the main site domain. ( For example www.example.com/test ) The desired count is set to 1.\nSince I'm using the ALB of the main site only when the ASG terminates the instance, a new target is registered for the main site in the target group of the instance b. My issue is that this target group should only register one instance and not 2 ( instance a ( main site ) and instance b )\n\nWould really appreciate some guidance, haven't received much from Re:post :(\n\nWhat technologies are you using?\n\nAWS, AutoScaling, EC2, ElasticBeanstalk\n\nWhat were you expecting to happen?\n\nWhen Autoscaling terminates the instance, the target group that has instance b registers, initiates another instance and places the same instance b and not another instance a. \n\nWhat is actually happening?\n\nWhen Autoscaling terminates the instance, the target group is adding another instance a and therefore the site breaks as the incorrect server is being shown.\n\nWhat have you already tried/thought about?\n\nMaybe having one autoscaling group for two different instances, however didn't manage. But there is still the issue of cannot control which instance is added to the target group when its terminated.\n","user":{"name":"deborah-Cat","username":"deborahcat","twitter_username":null,"github_username":"deborah-Cat","user_id":938078,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--gz4QkxLK--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/938078/0b988fcb-72f8-4a0c-a1ce-b9e16426cbfb.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--iduZIaiB--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/938078/0b988fcb-72f8-4a0c-a1ce-b9e16426cbfb.png"},"flare_tag":{"name":"help","bg_color_hex":"#ff3232","text_color_hex":"#ffffff"}} -------------------------------------------------------------------------------- /public/savedData/article-1221876.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1221876,"title":"Working with Containers? Check AWS Cloud Map","description":"If you're working with containers, most of the time you need a service discovery service, to help you...","readable_publish_date":"Nov 7","slug":"working-with-containers-check-aws-cloud-map-1l40","path":"/aws-builders/working-with-containers-check-aws-cloud-map-1l40","url":"https://dev.to/aws-builders/working-with-containers-check-aws-cloud-map-1l40","comments_count":0,"public_reactions_count":0,"collection_id":null,"published_timestamp":"2022-11-07T09:58:04Z","positive_reactions_count":0,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--Z5Bf7Nwe--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o9b4z7fxwci4if9etw2i.jpg","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--Btg4NJNc--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o9b4z7fxwci4if9etw2i.jpg","canonical_url":"https://dev.to/aws-builders/working-with-containers-check-aws-cloud-map-1l40","created_at":"2022-10-17T08:18:18Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-07T09:58:04Z","last_comment_at":"2022-11-07T09:58:04Z","reading_time_minutes":2,"tag_list":"aws, containerapps, microservices, devops","tags":["aws","containerapps","microservices","devops"],"body_html":"

If you're working with containers, most of the time you need a service discovery service, to help you find your services. AWS Cloud Map does help you with that.

\n\n

AWS Cloud Map is a cloud resource discovery service. With Cloud Map, you can define custom names for your application resources, and it maintains the updated location of these dynamically changing resources. This increases your application's availability because your web service always discovers the most up-to-date locations of its resources.

\n\n

In many architectures you may have several instances or machines running in the background, and sometimes you might turn instances down and deploy new ones (using different deployment strategies), hence it will generate a new IP address for your instances which will be hard to track.

\n\n

With Cloud Map you can even register many other AWS services (such as Amazon EC2 instances, Amazon DynamoDB tables, Amazon S3 buckets, Amazon Simple Queue Service (Amazon SQS) queues, or APIs deployed on top of Amazon API Gateway)

\n\n

\n \n \n Features:\n

\n\n
    \n
  • Discover resources via API calls or DNS queries
  • \n
  • Simplified service naming
  • \n
  • Assign custom attributes
  • \n
  • Access control
  • \n
  • Automatic health check
  • \n
  • Deep integration with AWS container services
  • \n
  • Rapid change propagation
  • \n
  • Fully managed
  • \n
\n\n

\n \n \n Benefits\n

\n\n
    \n
  • Register application resources with custom names
  • \n
  • Constantly check the health of resources, ensure location is up to-date
  • \n
  • Provides a single registry for all your application services
  • \n
  • Reliably obtain up-to-date health statuses of your application resources
  • \n
  • Enables microservices to easily locate one another
  • \n
\n\n

The main aim of this article is to light up services from AWS, I wanted to keep it simple without any code or deep technical discussion, hence making it easy to be read.

\n\n

Hope it was a good one and I highly encourage to start using the service specially if your working on enterprise level applications

\n\n","body_markdown":"If you're working with containers, most of the time you need a service discovery service, to help you find your services. AWS Cloud Map does help you with that.\n\nAWS Cloud Map is a cloud resource discovery service. With Cloud Map, you can define custom names for your application resources, and it maintains the updated location of these dynamically changing resources. This increases your application's availability because your web service always discovers the most up-to-date locations of its resources.\n\nIn many architectures you may have several instances or machines running in the background, and sometimes you might turn instances down and deploy new ones (using different deployment strategies), hence it will generate a new IP address for your instances which will be hard to track.\n\nWith Cloud Map you can even register many other AWS services (such as Amazon EC2 instances, Amazon DynamoDB tables, Amazon S3 buckets, Amazon Simple Queue Service (Amazon SQS) queues, or APIs deployed on top of Amazon API Gateway)\n\n## Features:\n- Discover resources via API calls or DNS queries\n- Simplified service naming\n- Assign custom attributes\n- Access control\n- Automatic health check\n- Deep integration with AWS container services\n- Rapid change propagation\n- Fully managed\n\n## Benefits\n- Register application resources with custom names\n- Constantly check the health of resources, ensure location is up to-date\n- Provides a single registry for all your application services\n- Reliably obtain up-to-date health statuses of your application resources\n- Enables microservices to easily locate one another\n\nThe main aim of this article is to light up services from AWS, I wanted to keep it simple without any code or deep technical discussion, hence making it easy to be read.\n\nHope it was a good one and I highly encourage to start using the service specially if your working on enterprise level applications","user":{"name":"awedis","username":"awedis","twitter_username":null,"github_username":"awedis","user_id":599185,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--a_yG-_V2--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/599185/f51cc8e5-2967-43f6-a6d1-480e953681b9.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--QgbSZMb_--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/599185/f51cc8e5-2967-43f6-a6d1-480e953681b9.jpg"},"organization":{"name":"AWS Community Builders ","username":"aws-builders","slug":"aws-builders","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--zmOZQNzv--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2794/88da75b6-aadd-4ea1-8083-ae2dfca8be94.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--vWmcJ-ty--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2794/88da75b6-aadd-4ea1-8083-ae2dfca8be94.png"}} -------------------------------------------------------------------------------- /public/savedData/article-1210441.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1210441,"title":"How To find easy to rank keywords on Google - Quick SEO Hack","description":"There are many ways to find low competition keywords that rank easily on Google, Bing, and other...","readable_publish_date":"Oct 4","slug":"how-to-find-easy-to-rank-keywords-on-google-quick-seo-hack-311l","path":"/its__keziah/how-to-find-easy-to-rank-keywords-on-google-quick-seo-hack-311l","url":"https://dev.to/its__keziah/how-to-find-easy-to-rank-keywords-on-google-quick-seo-hack-311l","comments_count":0,"public_reactions_count":2,"collection_id":null,"published_timestamp":"2022-10-04T06:53:12Z","positive_reactions_count":2,"cover_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--ylO09sAj--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cm00afao5eorm1pmrqu0.png","social_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--i_FskCVn--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cm00afao5eorm1pmrqu0.png","canonical_url":"https://dev.to/its__keziah/how-to-find-easy-to-rank-keywords-on-google-quick-seo-hack-311l","created_at":"2022-10-04T06:53:13Z","edited_at":null,"crossposted_at":null,"published_at":"2022-10-04T06:53:12Z","last_comment_at":"2022-10-04T06:53:12Z","reading_time_minutes":2,"tag_list":"rankongoogle, seo, python, laravel","tags":["rankongoogle","seo","python","laravel"],"body_html":"

There are many ways to find low competition keywords that rank easily on Google, Bing, and other Search Engine. Now I am discussing some techniques to find low competition and profitable keywords that will rank easily.

\n\n

1. KGR Keyword Research Method:

\n\n

KGR keyword research method is an effective strategy to find long tail and easy ranking keywords. It was invented by Doug Cunnington.

\n\n

What is the KGR?

\n\n

Keyword Golden Research (KGR) is a strategy for discovering long tail keywords which rank on top easily.

\n\n

How is KGR calculated?

\n\n

KGR= (Allintitle results) / (Monthly search volume)

\n\n

Where monthly search volume is 250 or less.

\n\n

If KGR Ratio is less than 0.25, it works well.

\n\n

See the screenshot bellow

\n\n

My keyword is ‘’ Magellan outdoors folding camp cot’’

\n\n

Monthly search volume of these keywords is 70.

\n\n

(I use SEMrush for search volume)

\n\n

Now calculate the KGR (04 / 70) = 0.06

\n\n

The aim is to find a KGR score is 0.25 or lower than 0.25. We get a keyword whose KGR value is less than 0.25. So it is a great KGR keyword.

\n\n

Importance of KGR method:

\n\n

Good content with KGR keywords will rank easily without backlinks.

\n\n

It is a modern SEO strategy that's really working well.

\n\n

It is most effective for a new website or blog.

\n\n

If you want to rank in a short time, it is a great way for you.

\n\n

KGR or Golden Keyword Ratio is called easy ranking Keywords

\n\n

Is it really work now on 2022?

\n\n

KGR keywords are easy ranking keywords. But all keywords will not work. If your website or blog is brand new, KGR method is best for you. If you write 10 good content with focus KGR keywords, your maximum content will rank.

\n\n

How much time will it take to rank after using KGR keywords?

\n\n

KGR keyword research is a modern-day SEO strategy that works for every niche. It will take 1-3 months to see the result. if you generate high-quality content by focusing KGR keywords you have a great chance to rank on top within a short time.

\n\n

Why go with such low search volume keywords?

\n\n

Generally, KGR keywords are low search volume keywords, but these kinds of keywords rank easily without much effort. If you rank for a single KGR keyword you will rank on other similar keywords which bring a good amount of traffic to your website.

\n\n","body_markdown":"There are many ways to find low competition keywords that rank easily on Google, Bing, and other Search Engine. Now I am discussing some techniques to find low competition and profitable keywords that will rank easily.\n\n\n**1. KGR Keyword Research Method:**\n\nKGR keyword research method is an effective strategy to find long tail and easy ranking keywords. It was invented by Doug Cunnington.\n\n**What is the KGR?**\n\nKeyword Golden Research (KGR) is a strategy for discovering long tail keywords which rank on top easily.\n\n**How is KGR calculated?**\n\nKGR= (Allintitle results) / (Monthly search volume)\n\nWhere monthly search volume is 250 or less.\n\nIf KGR Ratio is less than 0.25, it works well.\n\nSee the screenshot bellow\n\n\n\nMy keyword is ‘’ Magellan outdoors folding camp cot’’\n\nMonthly search volume of these keywords is 70.\n\n(I use SEMrush for search volume)\n\nNow calculate the KGR (04 / 70) = 0.06\n\nThe aim is to find a KGR score is 0.25 or lower than 0.25. We get a keyword whose KGR value is less than 0.25. So it is a great KGR keyword.\n\n**Importance of KGR method:**\n\nGood content with KGR keywords will rank easily without backlinks.\n\nIt is a modern SEO strategy that's really working well.\n\nIt is most effective for a new website or blog.\n\nIf you want to rank in a short time, it is a great way for you.\n\nKGR or Golden Keyword Ratio is called easy ranking Keywords\n\n**Is it really work now on 2022?**\n\nKGR keywords are easy ranking keywords. But all keywords will not work. If your website or blog is brand new, KGR method is best for you. If you write 10 good content with focus KGR keywords, your maximum content will rank.\n\n**How much time will it take to rank after using KGR keywords?**\n\nKGR keyword research is a modern-day SEO strategy that works for every niche. It will take 1-3 months to see the result. if you generate high-quality content by focusing KGR keywords you have a great chance to rank on top within a short time.\n\n**Why go with such low search volume keywords?**\n\nGenerally, KGR keywords are low search volume keywords, but these kinds of keywords rank easily without much effort. If you rank for a single KGR keyword you will rank on other similar keywords which bring a good amount of traffic to your website.","user":{"name":"keziah Awino","username":"its__keziah","twitter_username":"Its__Keziah","github_username":null,"user_id":933226,"website_url":"https://fecytech.com","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--Zpn09K7C--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/933226/d828b466-1b9f-4dd1-9522-c97d3ce8a1a1.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--9E982S5M--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/933226/d828b466-1b9f-4dd1-9522-c97d3ce8a1a1.jpg"}} -------------------------------------------------------------------------------- /public/savedData/article-1244300.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1244300,"title":"Understanding AWS Market Place?","description":"In General Marketplace is a place or platform where you can find Goods or product for buy. Also in...","readable_publish_date":"Nov 5","slug":"understanding-aws-market-place-ji3","path":"/aws-builders/understanding-aws-market-place-ji3","url":"https://dev.to/aws-builders/understanding-aws-market-place-ji3","comments_count":0,"public_reactions_count":2,"collection_id":null,"published_timestamp":"2022-11-05T08:47:34Z","positive_reactions_count":2,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1244300.png","canonical_url":"https://dev.to/aws-builders/understanding-aws-market-place-ji3","created_at":"2022-11-05T08:43:55Z","edited_at":null,"crossposted_at":null,"published_at":"2022-11-05T08:47:34Z","last_comment_at":"2022-11-05T08:47:34Z","reading_time_minutes":1,"tag_list":"aws, awsmarketplace","tags":["aws","awsmarketplace"],"body_html":"

In General Marketplace is a place or platform where you can find Goods or product for buy. Also in market place where vendors can come together to sell their products or services to a curated customer base.

\n\n

\n \n \n What is AWS Market Place?\n

\n\n

AWS market place is virtual place where different vendors and developer sell their software or services which we can use when we build and architect our applications or solutions on AWS.

\n\n

\"AWS

\n\n

There are lots of Categories you found when you land on AWS Market Place Site.

\n\n

For Example,
\n\"AWS

\n\n

As you can see below image you can purchase ready-made WordPress Solutions in AWS Market Place. With that you can easily deploy a WordPress site with zero knowledge of Infrastructure.

\n\n

\"AWS

\n\n

You can also find lots of Solutions on there.

\n\n

\"Categories\"

\n\n

Visit and Explore AWS Market Place: https://aws.amazon.com/marketplace

\n\n

Thanks

\n\n","body_markdown":"---\ntitle: Understanding AWS Market Place?\npublished: true\ndescription: \ntags: aws, awsmarketplace\n# cover_image: https://direct_url_to_image.jpg\n# Use a ratio of 100:42 for best results.\n# published_at: 2022-11-05 08:30 +0000\n---\n\nIn General Marketplace is a place or platform where you can find Goods or product for buy. Also in market place where vendors can come together to sell their products or services to a curated customer base.\n\n## What is AWS Market Place?\nAWS market place is virtual place where different vendors and developer sell their software or services which we can use when we build and architect our applications or solutions on AWS. \n\n\n![AWS Market Place](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/71npjqxwwb2e3tflwnpk.png)\n\n\nThere are lots of Categories you found when you land on AWS Market Place Site. \n\nFor Example,\n![AWS Market Place Categories](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e15pku9d1tq6oess94kq.png)\n\nAs you can see below image you can purchase ready-made WordPress Solutions in AWS Market Place. With that you can easily deploy a WordPress site with zero knowledge of Infrastructure.\n\n![AWS WordPress](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9m9itj6a4sh6uunekbtx.png)\n\nYou can also find lots of Solutions on there.\n\n![Categories](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rit5evcfqw61i05zehdh.png)\n\nVisit and Explore AWS Market Place: https://aws.amazon.com/marketplace\n\nThanks\n\n\n\n\n\n\n\n\n\n","user":{"name":"Mim Ahmed","username":"mimahmed","twitter_username":"joyahmed03","github_username":null,"user_id":61446,"website_url":"https://www.youtube.com/channel/UClFSt_03Dc7rmrE8HGiHKew","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--4hEW6fsd--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/61446/7f1f1538-fe96-4ea8-b6e1-586d8597e88e.jpg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--fbxQ-MDO--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/61446/7f1f1538-fe96-4ea8-b6e1-586d8597e88e.jpg"},"organization":{"name":"AWS Community Builders ","username":"aws-builders","slug":"aws-builders","profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--zmOZQNzv--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2794/88da75b6-aadd-4ea1-8083-ae2dfca8be94.png","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--vWmcJ-ty--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2794/88da75b6-aadd-4ea1-8083-ae2dfca8be94.png"}} -------------------------------------------------------------------------------- /public/savedData/article-1246730.json: -------------------------------------------------------------------------------- 1 | {"type_of":"article","id":1246730,"title":"JavaScript concepts you need to know before getting into React...","description":"1.Arrow Functions: An arrow function expression is a compact alternative to a traditional...","readable_publish_date":"Nov 7","slug":"javascript-concepts-you-need-to-know-before-getting-into-react-ia2","path":"/gaurbprajapati/javascript-concepts-you-need-to-know-before-getting-into-react-ia2","url":"https://dev.to/gaurbprajapati/javascript-concepts-you-need-to-know-before-getting-into-react-ia2","comments_count":0,"public_reactions_count":2,"collection_id":null,"published_timestamp":"2022-11-07T17:18:37Z","positive_reactions_count":2,"cover_image":null,"social_image":"https://dev.to/social_previews/article/1246730.png","canonical_url":"https://dev.to/gaurbprajapati/javascript-concepts-you-need-to-know-before-getting-into-react-ia2","created_at":"2022-11-07T17:18:37Z","edited_at":"2022-11-07T17:22:25Z","crossposted_at":null,"published_at":"2022-11-07T17:18:37Z","last_comment_at":"2022-11-07T17:18:37Z","reading_time_minutes":1,"tag_list":"javascript, react, webdev, programming","tags":["javascript","react","webdev","programming"],"body_html":"

1.Arrow Functions:
\n An arrow function expression is a compact alternative to a
\n traditional function expression.
\n https://javascript.info/arrow-functions-basics

\n\n

2.Let/var/const variables:
\n Understand the difference between them, uses and their
\n respective scopes.
\n https://youtu.be/BNC6slYCj50

\n\n

https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/

\n\n

3.Destructuring assignment:
\n JavaScript expression that makes it possible to unpack values
\n from arrays, or properties from objects, into a bunch of
\n variables.
\n https://youtu.be/giNjEgYTd9E
\n https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

\n\n

4.Template Literals:
\n With ES6, a newer form of string called template literal was
\n given, which consists of two backticks .
\nhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

\n\n

https://youtu.be/K4Kh5gw4PRE

\n\n

5.Using Fetch:
\n The Fetch API provides a JavaScript interface for accessing and
\n manipulating parts of the HTTP pipeline, such as requests and
\n responses.
\nhttps://youtu.be/drK6mdA9d_M

\n\n

https://developer.mozilla.org/enUS/docs/Web/API/Fetch_API/Using_Fetch

\n\n

6.Import / Export:
\n ES6 gave people ability to share code between their own
\n JavaScript files as well as third-party libraries using ES
\n modules.
\nhttps://youtu.be/s9kNndJLOjg

\n\n

https://javascript.info/import-export

\n\n

7.Async Js, Promise, Callback:
\n Such topics have been given in this thread, checkout.
\n https://youtu.be/ZYb_ZU8LNxs

\n\n

8.Array functions- Map, Reduce, and Filter:

\n\n

https://youtu.be/zdp0zrpKzIE

\n\n

https://www.geeksforgeeks.org/how-to-map-reduce-and-filter-a-set-elementusingjavascript/#:~:text=The%20map()%2C%20reduce(),instead%20of%20using%20the%20loops.

\n\n

If You Want to learn Javascript in detail you can refer
\n\"Namaste Javascript\" YouTube playlist 👇

\n\n

https://youtube.com/playlist?list=PLlasXeu85E9cQ32gLCvAvr9vNaUccPVNP

\n\n","body_markdown":"**1.Arrow Functions:**\n An arrow function expression is a compact alternative to a \n traditional function expression.\n https://javascript.info/arrow-functions-basics\n\n**2.Let/var/const variables:**\n Understand the difference between them, uses and their \n respective scopes.\n https://youtu.be/BNC6slYCj50\n\n https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/\n\n**3.Destructuring assignment:**\n JavaScript expression that makes it possible to unpack values \n from arrays, or properties from objects, into a bunch of \n variables.\n https://youtu.be/giNjEgYTd9E\n https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment\n\n**4.Template Literals:**\n With ES6, a newer form of string called template literal was \n given, which consists of two backticks ` `.\nhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals\n\nhttps://youtu.be/K4Kh5gw4PRE\n\n**5.Using Fetch:**\n The Fetch API provides a JavaScript interface for accessing and \n manipulating parts of the HTTP pipeline, such as requests and \n responses.\nhttps://youtu.be/drK6mdA9d_M\n\nhttps://developer.mozilla.org/enUS/docs/Web/API/Fetch_API/Using_Fetch\n\n**6.Import / Export:**\n ES6 gave people ability to share code between their own \n JavaScript files as well as third-party libraries using ES \n modules.\nhttps://youtu.be/s9kNndJLOjg\n\nhttps://javascript.info/import-export\n\n**7.Async Js, Promise, Callback:**\n Such topics have been given in this thread, checkout.\n https://youtu.be/ZYb_ZU8LNxs\n\n**8.Array functions- Map, Reduce, and Filter:**\n\nhttps://youtu.be/zdp0zrpKzIE\n\nhttps://www.geeksforgeeks.org/how-to-map-reduce-and-filter-a-set-elementusingjavascript/#:~:text=The%20map()%2C%20reduce(),instead%20of%20using%20the%20loops.\n\n\nIf You Want to learn Javascript in detail you can refer \n\"Namaste Javascript\" YouTube playlist 👇\n\nhttps://youtube.com/playlist?list=PLlasXeu85E9cQ32gLCvAvr9vNaUccPVNP\n","user":{"name":"gaurbprajapati","username":"gaurbprajapati","twitter_username":null,"github_username":"gaurbprajapati","user_id":855855,"website_url":null,"profile_image":"https://res.cloudinary.com/practicaldev/image/fetch/s--A_m_F_6d--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/855855/21312a6f-b3ce-4ff7-ad01-08d79546a6d7.jpeg","profile_image_90":"https://res.cloudinary.com/practicaldev/image/fetch/s--sLZglYEs--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/855855/21312a6f-b3ce-4ff7-ad01-08d79546a6d7.jpeg"}} -------------------------------------------------------------------------------- /src/layouts/MainHeader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Box from '@mui/material/Box'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Typography from '@mui/material/Typography'; 7 | import Menu from '@mui/material/Menu'; 8 | import MenuIcon from '@mui/icons-material/Menu'; 9 | import Container from '@mui/material/Container'; 10 | import Avatar from '@mui/material/Avatar'; 11 | import Button from '@mui/material/Button'; 12 | import Tooltip from '@mui/material/Tooltip'; 13 | import MenuItem from '@mui/material/MenuItem'; 14 | import AdbIcon from '@mui/icons-material/Adb'; 15 | import Link from 'next/link'; 16 | import Person from '@mui/icons-material/Person'; 17 | import { useSelector } from 'react-redux'; 18 | import { Badge } from '@mui/material'; 19 | import { TOP_MENU_TAGS, USER_MENU_LINKS } from '../constants/article-const'; 20 | import { getTagLink } from '../helpers/article-helper'; 21 | import { ReduxState } from '../redux/store'; 22 | import { UserSliceType } from '../types/article-types'; 23 | 24 | const MainHeader = () => { 25 | const [anchorElNav, setAnchorElNav] = React.useState(null); 26 | const [anchorElUser, setAnchorElUser] = React.useState(null); 27 | 28 | const reduxUserData:UserSliceType = useSelector((reduxState: ReduxState) => reduxState.user); 29 | 30 | const badgeContent = reduxUserData.favoriteItems.length || reduxUserData.recentItems.length; 31 | const badgeColor = reduxUserData.favoriteItems.length ? 'error' : 'secondary'; 32 | 33 | const handleOpenNavMenu = (event: React.MouseEvent) => { 34 | setAnchorElNav(event.currentTarget); 35 | }; 36 | const handleOpenUserMenu = (event: React.MouseEvent) => { 37 | setAnchorElUser(event.currentTarget); 38 | }; 39 | 40 | const handleCloseNavMenu = () => { 41 | setAnchorElNav(null); 42 | }; 43 | 44 | const handleCloseUserMenu = () => { 45 | setAnchorElUser(null); 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 68 | React 69 | 70 | 71 | 72 | 80 | 81 | 82 | 100 | {TOP_MENU_TAGS.map((tag) => ( 101 | 102 | 103 | 104 | 105 | {tag} 106 | 107 | 108 | 109 | 110 | ))} 111 | 112 | 113 | 114 | 130 | React 131 | 132 | 133 | {TOP_MENU_TAGS.map((tag) => ( 134 | 135 | 143 | 144 | ))} 145 | 146 | 147 | 148 | 149 | 150 | 151 | {reduxUserData?.profile?.avatarUrl ? ( 152 | 153 | ) : ( 154 | 155 | 156 | 157 | ) } 158 | 159 | 160 | 161 | 177 | {USER_MENU_LINKS.map((menuLink) => ( 178 | 179 | 180 | {menuLink.title} 181 | 182 | 183 | ))} 184 | 185 | 186 | 187 | 188 | 189 | ); 190 | }; 191 | 192 | export default MainHeader; 193 | --------------------------------------------------------------------------------