├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── preview_images ├── tech_news_en_desktop.gif └── tech_news_en_mobile.gif ├── public ├── favicon.ico ├── img │ └── preview.PNG ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── app-header.js ├── bottom-navigation.js ├── favorites │ └── chips-component.js ├── featured-post-component.js ├── home │ ├── posts-component.js │ ├── sections-component.js │ └── single-post-component.js ├── post │ ├── dialog-fullscreen-component.js │ ├── post-component.css │ └── post-component.js ├── settings │ └── preferences-component.js ├── sidebar-menu-component.js ├── skeletons-component.js ├── snackbar-no-internet-component.js ├── snackbar-notify-component.js └── theme.js ├── constants └── constants.js ├── customHooks └── custom-hooks.js ├── index.css ├── index.js ├── logo.svg ├── pages ├── favorites-page.js ├── home-page.js ├── post-page.js ├── saved-page.js ├── search-page.js └── settings-page.js ├── redux ├── actions │ └── actions.js ├── reducers │ └── reducers.js └── store │ └── store.js ├── reportWebVitals.js ├── service-worker.js ├── serviceWorkerRegistration.js ├── services ├── configService.js ├── siteService.js └── storageService.js ├── setupTests.js └── utils └── functions.js /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_TEST_VAR=testValueFromEnv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .eslintcache 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PWA Blog template using ReactJs and Material UI 2 | 3 | This is a template PWA - Progresive Web Application that uses ReactJs and Material UI.
4 | App works offline by saving responses in localStorage.
5 | Currently I've done the development in a subfolder ('/pwa/'). To run in the root folder just remove the ("homepage": "/pwa/",) in the package.json file. (Also remove the "set HOST=intranet&& " from scripts->start property in package.json) 6 | 7 | Store is now managed by React-Redux. 8 | Store is managed using React's Context API.
9 | Switch to "react-context" branch to see the React-Contex version
10 | 11 | (Posts are being retrieved from a wordpress site using the WordPress REST API) 12 | 13 | Steps to install and start playing with the project: 14 | 15 | 1. git clone https://github.com/edisonneza/react-blog.git 16 | 2. npm i 17 | 3. npm run start 18 | 19 | To generate build files (by removing the source map files) 20 | 21 | * npm run winBuild 22 |
23 | or (if LINUX) 24 | 25 | * npm run build 26 | 27 | See GIFs below on desktop and mobile devices: 28 | 29 | ![desktop version](preview_images/tech_news_en_desktop.gif) 30 | 31 | ![mobile version](preview_images/tech_news_en_mobile.gif) 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwa", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/pwa/", 6 | "dependencies": { 7 | "@material-ui/core": "^4.11.2", 8 | "@material-ui/icons": "^4.11.2", 9 | "@material-ui/lab": "^4.0.0-alpha.57", 10 | "@testing-library/jest-dom": "^5.11.8", 11 | "@testing-library/react": "^11.2.3", 12 | "@testing-library/user-event": "^12.6.0", 13 | "moment": "^2.29.1", 14 | "react": "^17.0.1", 15 | "react-dom": "^17.0.1", 16 | "react-redux": "^7.2.2", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "4.0.1", 19 | "redux": "^4.0.5", 20 | "web-vitals": "^0.2.4", 21 | "workbox-background-sync": "^5.1.4", 22 | "workbox-broadcast-update": "^5.1.4", 23 | "workbox-cacheable-response": "^5.1.4", 24 | "workbox-core": "^5.1.4", 25 | "workbox-expiration": "^5.1.4", 26 | "workbox-google-analytics": "^5.1.4", 27 | "workbox-navigation-preload": "^5.1.4", 28 | "workbox-precaching": "^5.1.4", 29 | "workbox-range-requests": "^5.1.4", 30 | "workbox-routing": "^5.1.4", 31 | "workbox-strategies": "^5.1.4", 32 | "workbox-streams": "^5.1.4" 33 | }, 34 | "scripts": { 35 | "start": "set HOST=intranet&& react-scripts start", 36 | "build": "GENERATE_SOURCEMAP=false react-scripts build", 37 | "winBuild": "set \"GENERATE_SOURCEMAP=false\" && react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /preview_images/tech_news_en_desktop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonneza/react-blog/ee47ed830f6b7a4ea1f3fccf2a7c7f37b5ddea7b/preview_images/tech_news_en_desktop.gif -------------------------------------------------------------------------------- /preview_images/tech_news_en_mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonneza/react-blog/ee47ed830f6b7a4ea1f3fccf2a7c7f37b5ddea7b/preview_images/tech_news_en_mobile.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonneza/react-blog/ee47ed830f6b7a4ea1f3fccf2a7c7f37b5ddea7b/public/favicon.ico -------------------------------------------------------------------------------- /public/img/preview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonneza/react-blog/ee47ed830f6b7a4ea1f3fccf2a7c7f37b5ddea7b/public/img/preview.PNG -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 41 | Tech News 42 | 43 | 44 | 45 | 46 |
47 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonneza/react-blog/ee47ed830f6b7a4ea1f3fccf2a7c7f37b5ddea7b/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonneza/react-blog/ee47ed830f6b7a4ea1f3fccf2a7c7f37b5ddea7b/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Tech News", 3 | "name": "Lajme rreth teknologjisë", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": "/pwa/", 22 | "display": "standalone", 23 | "theme_color": "#3367D6", 24 | "background_color": "#ffffff", 25 | "scope": "/pwa/", 26 | "shortcuts": [ 27 | { 28 | "name": "Kërko lajme", 29 | "short_name": "Kërko", 30 | "description": "Hyni ne pamjen e kerkimit te informacioneve", 31 | "url": "/pwa/search", 32 | "icons": [{ "src": "/pwa/logo192.png", "sizes": "192x192" }] 33 | }, 34 | { 35 | "name": "Shiko postimet e ruajtura", 36 | "short_name": "Të ruajtura", 37 | "description": "Shiko postimet qe keni ruajtur gjate leximit", 38 | "url": "/pwa/saved", 39 | "icons": [{ "src": "/pwa/logo192.png", "sizes": "192x192" }] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @media (prefers-reduced-motion: no-preference) { 2 | 3 | } 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 4 | 5 | import HomePage from "./pages/home-page"; 6 | import SettingsPage from "./pages/settings-page"; 7 | import LabelBottomNavigation from "./components/bottom-navigation"; 8 | import AppHeader from "./components/app-header"; 9 | import SearchPage from "./pages/search-page"; 10 | import SavedPage from "./pages/saved-page"; 11 | import PostPage from "./pages/post-page"; 12 | import FavoritesPage from "./pages/favorites-page"; 13 | import { Container, Box } from "@material-ui/core"; 14 | import CssBaseline from "@material-ui/core/CssBaseline"; 15 | import { isMobile } from "./utils/functions"; 16 | import { Provider } from "react-redux"; 17 | import store from "./redux/store/store"; 18 | import Theme from "./components/theme"; 19 | 20 | function App() { 21 | return ( 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 |
57 | {isMobile() && } 58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/app-header.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import AppBar from "@material-ui/core/AppBar"; 4 | import Toolbar from "@material-ui/core/Toolbar"; 5 | import Typography from "@material-ui/core/Typography"; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import MenuIcon from '@material-ui/icons/Menu'; 8 | import SideBarMenu from './sidebar-menu-component'; 9 | import { isMobile } from '../utils/functions'; 10 | import { useSelector } from "react-redux"; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | root: { 14 | flexGrow: 1, 15 | // backgroundColor: theme.palette.background.paper 16 | }, 17 | toolbar: { 18 | minHeight: "44px", 19 | }, 20 | title: { 21 | flexGrow: 1, 22 | textAlign: "center", 23 | }, 24 | // appBar: { 25 | // backgroundColor: theme.palette.grey[800] 26 | // } 27 | })); 28 | 29 | export default function AppHeader() { 30 | const classes = useStyles(); 31 | const title = useSelector(state => state.title); 32 | const mobile = isMobile(); 33 | 34 | const [open, setOpen] = useState(false); 35 | 36 | return ( 37 |
38 | 39 | 40 | {!mobile && ( setOpen(true)} 46 | > 47 | 48 | ) } 49 | 50 | {/* Lajmet e përmbledhura teknologjike */} 51 | {title} 52 | 53 | 54 | 55 | 56 | {!mobile && setOpen(!open)}/>} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/bottom-navigation.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import BottomNavigation from "@material-ui/core/BottomNavigation"; 4 | import BottomNavigationAction from "@material-ui/core/BottomNavigationAction"; 5 | import SearchIcon from "@material-ui/icons/Search"; 6 | import HomeIcon from "@material-ui/icons/Home"; 7 | import FavoriteIcon from "@material-ui/icons/Favorite"; 8 | import BookmarksIcon from "@material-ui/icons/Bookmarks"; 9 | import SettingsIcon from "@material-ui/icons/Settings"; 10 | import { useHistory } from "react-router-dom"; 11 | import Constants from "../constants/constants"; 12 | import { useDispatch } from "react-redux"; 13 | import { setTitle } from "../redux/actions/actions"; 14 | 15 | const useStyles = makeStyles({ 16 | root: { 17 | width: "100%", 18 | position: "fixed", 19 | left: "0px", 20 | right: "0px", 21 | bottom: 0, 22 | }, 23 | }); 24 | 25 | export default function LabelBottomNavigation() { 26 | const classes = useStyles(); 27 | let history = useHistory(); 28 | const [value, setValue] = React.useState(history.location.pathname); 29 | 30 | const dispatch = useDispatch(); 31 | const handleTitle = (title) => dispatch(setTitle(title)); 32 | 33 | const setTitleByRoute = (value) => { 34 | switch (value) { 35 | case "/": 36 | handleTitle(Constants.appName); 37 | break; 38 | case "/search": 39 | handleTitle("Kërkoni"); 40 | break; 41 | case "/favorites": 42 | handleTitle("Preferencat"); 43 | break; 44 | case "/saved": 45 | handleTitle("Postimet e ruajtura"); 46 | break; 47 | case "/settings": 48 | handleTitle("Cilësimet"); 49 | break; 50 | default: 51 | handleTitle(Constants.appName); 52 | break; 53 | } 54 | }; 55 | 56 | useEffect(() => { 57 | setTitleByRoute(value); 58 | }, [value]); 59 | 60 | const handleChange = (event, newValue) => { 61 | setValue(newValue); 62 | setTitleByRoute(newValue); 63 | history.push(newValue); 64 | }; 65 | 66 | return ( 67 | 72 | } /> 73 | } 77 | /> 78 | } 82 | /> 83 | } 87 | /> 88 | } 92 | /> 93 | 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/favorites/chips-component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import Chip from "@material-ui/core/Chip"; 4 | import DoneIcon from "@material-ui/icons/Done"; 5 | import SiteService from "../../services/siteService"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | root: { 9 | display: "flex", 10 | justifyContent: "center", 11 | flexWrap: "wrap", 12 | "& > *": { 13 | margin: theme.spacing(0.5), 14 | }, 15 | }, 16 | })); 17 | 18 | const siteService = new SiteService(); 19 | 20 | export default function ChipsComponent() { 21 | const classes = useStyles(); 22 | const [tags, setTags] = React.useState([]); 23 | 24 | const handleClick = (value) => { 25 | siteService.saveTags(value).then(data => setTags(data)); 26 | }; 27 | 28 | React.useEffect(() => { 29 | siteService.getTags().then(data => setTags(data)); 30 | }, []); 31 | 32 | return ( 33 |
34 | {tags.map((item, index) => { 35 | return ( 36 | handleClick(item.value)} 40 | onDelete={() => handleClick(item.value)} 41 | deleteIcon={!item.active ? : null} 42 | variant="outlined" 43 | color={item.active ? "primary" : "default"} 44 | /> 45 | ); 46 | })} 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/featured-post-component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Paper from "@material-ui/core/Paper"; 5 | import Typography from "@material-ui/core/Typography"; 6 | import Grid from "@material-ui/core/Grid"; 7 | import Button from "@material-ui/core/Button"; 8 | import { useDispatch } from "react-redux"; 9 | import { setPost } from "../redux/actions/actions"; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | mainFeaturedPost: { 13 | position: "relative", 14 | backgroundColor: theme.palette.grey[800], 15 | color: theme.palette.common.white, 16 | marginBottom: theme.spacing(4), 17 | backgroundImage: "url(https://source.unsplash.com/random)", 18 | backgroundSize: "cover", 19 | backgroundRepeat: "no-repeat", 20 | backgroundPosition: "center", 21 | }, 22 | overlay: { 23 | position: "absolute", 24 | top: 0, 25 | bottom: 0, 26 | right: 0, 27 | left: 0, 28 | backgroundColor: "rgba(0,0,0,.3)", 29 | }, 30 | mainFeaturedPostContent: { 31 | position: "relative", 32 | padding: theme.spacing(3), 33 | [theme.breakpoints.up("md")]: { 34 | padding: theme.spacing(6), 35 | paddingRight: 0, 36 | }, 37 | }, 38 | })); 39 | 40 | export default function FeaturedPost(props) { 41 | const dispatch = useDispatch(); 42 | 43 | const handlePost = (post) => dispatch(setPost(post)); 44 | 45 | const classes = useStyles(); 46 | const { post } = props; 47 | 48 | return ( 49 | 53 | {/* Increase the priority of the hero background image */} 54 | { 55 | {post.imageText} 60 | } 61 |
62 | 63 | 64 |
65 | 71 | {post.title} 72 | 73 | 82 | {/* {post.description.split(' ').splice(0, 10).join(' ')}... */} 83 | 84 | 92 |
93 |
94 |
95 | 96 | ); 97 | } 98 | 99 | FeaturedPost.propTypes = { 100 | post: PropTypes.object, 101 | }; 102 | -------------------------------------------------------------------------------- /src/components/home/posts-component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Grid from "@material-ui/core/Grid"; 4 | import SinglePost from './single-post-component'; 5 | 6 | export default function Posts(props) { 7 | const { posts, showDelete, handleDelete } = props; 8 | 9 | return ( 10 | 11 | {posts.map((post) => ( 12 | 13 | ))} 14 | 15 | ); 16 | } 17 | 18 | Posts.propTypes = { 19 | posts: PropTypes.array, 20 | showDelete: PropTypes.bool, 21 | handleDelete: PropTypes.func 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/home/sections-component.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Toolbar from "@material-ui/core/Toolbar"; 5 | import Tab from "@material-ui/core/Tab"; 6 | import Tabs from "@material-ui/core/Tabs"; 7 | import { useDispatch, useSelector } from "react-redux"; 8 | import { setTabSelected } from "../../redux/actions/actions"; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | toolbar: { 12 | borderBottom: `1px solid ${theme.palette.divider}`, 13 | }, 14 | toolbarTitle: { 15 | flex: 1, 16 | }, 17 | toolbarSecondary: { 18 | justifyContent: "space-between", 19 | overflowX: "auto", 20 | }, 21 | toolbarLink: { 22 | padding: theme.spacing(1), 23 | flexShrink: 0, 24 | // borderRadius: '50%', 25 | // width: 100, 26 | // height: 100, 27 | // padding: 10, 28 | // marginRight: 5, 29 | // border: '1px solid red' 30 | }, 31 | })); 32 | 33 | function a11yProps(index) { 34 | return { 35 | id: `scrollable-auto-tab-${index}`, 36 | "aria-controls": `scrollable-auto-tabpanel-${index}`, 37 | }; 38 | } 39 | 40 | export default function SectionsHeader(props) { 41 | const classes = useStyles(); 42 | const tabSelected = useSelector(state => state.tabSelected); 43 | const dispatch = useDispatch(); 44 | 45 | const { sections } = props; 46 | const [value, setValue] = useState({ 47 | id: tabSelected.index, 48 | value: sections[tabSelected.index].title, 49 | }); 50 | 51 | const handleTabChange = (event, val) => { 52 | dispatch(setTabSelected({index: val, value: event.target.innerText})); 53 | setValue({ id: val, value: event.target.innerText }); 54 | }; 55 | 56 | return ( 57 | 58 | {/* 59 | 60 | 68 | {title} 69 | 70 | 71 | 72 | 73 | 76 | */} 77 | 82 | 91 | {sections.map((section, index) => ( 92 | 93 | ))} 94 | 95 | 96 | 97 | ); 98 | } 99 | 100 | SectionsHeader.propTypes = { 101 | sections: PropTypes.array, 102 | title: PropTypes.string, 103 | }; 104 | -------------------------------------------------------------------------------- /src/components/home/single-post-component.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import Grid from "@material-ui/core/Grid"; 5 | import Card from "@material-ui/core/Card"; 6 | import CardActionArea from "@material-ui/core/CardActionArea"; 7 | import CardContent from "@material-ui/core/CardContent"; 8 | import CardMedia from "@material-ui/core/CardMedia"; 9 | import CardActions from "@material-ui/core/CardActions"; 10 | import IconButton from "@material-ui/core/IconButton"; 11 | import ShareIcon from "@material-ui/icons/Share"; 12 | import FavoriteIcon from "@material-ui/icons/Favorite"; 13 | import { DateFromNow, ShareAPI } from "../../utils/functions"; 14 | import { Delete } from "@material-ui/icons"; 15 | import { GetValue, SaveValue } from "../../services/storageService"; 16 | import { Button, Dialog, DialogActions, DialogTitle } from "@material-ui/core"; 17 | import { SavePost } from "../../services/storageService"; 18 | import { makeStyles } from "@material-ui/core/styles"; 19 | import SnackbarNotify from "../snackbar-notify-component"; 20 | import { useDispatch } from "react-redux"; 21 | import { setPost } from "../../redux/actions/actions"; 22 | 23 | const useStyles = makeStyles({ 24 | cardContent: { 25 | padding: "10px 8px 0 10px", 26 | }, 27 | }); 28 | 29 | export default function SinglePost(props) { 30 | const classes = useStyles(); 31 | const { post, showDelete, handleDelete } = props; 32 | const [openDialog, setOpenDialog] = useState(false); 33 | const [openSnackbarNotify, setOpenSnackbarNotify] = useState(false); 34 | const dispatch = useDispatch(); 35 | const handlePost = (post) => dispatch(setPost(post)); 36 | 37 | const handleDeletePost = () => { 38 | const posts = GetValue("savedPost"); 39 | if (posts) { 40 | const otherPosts = posts.filter( 41 | (item) => item.originalLink !== post.originalLink 42 | ); 43 | SaveValue("savedPost", otherPosts); 44 | handleDelete(post); //to refresh the post list in parent component 45 | } 46 | }; 47 | 48 | const handleSavePost = () => { 49 | SavePost(post); 50 | setOpenSnackbarNotify(true); 51 | }; 52 | 53 | const handleShare = () => { 54 | const title = post.title; 55 | const text = `Une po lexoj nga webi Tech News. Lexo postimin ne linkun origjinal: ${post.title}`; 56 | const url = post.originalLink; 57 | 58 | ShareAPI(title, text, url); 59 | }; 60 | 61 | return ( 62 | <> 63 | {openSnackbarNotify && ( 64 | 65 | )} 66 | 67 | 68 | 69 | {/* */} 70 | {/* 71 |
72 | 73 | 74 | {post.title} 75 | 76 | 77 | {post.date} 78 | 79 | 80 | {post.description} 81 | 82 | 83 | Continue reading... 84 | 85 | 86 |
87 | 88 | 89 | 90 |
*/} 91 | 98 | handlePost(post)} 100 | className={classes.cardContent} 101 | > 102 | 103 | {post.title} 104 | 105 | 114 | 115 |
116 | 117 | 118 | 119 | {DateFromNow(post.date)} 120 | 121 | {/* 127 | 128 | */} 129 | 130 | {/* */} 138 | 139 | 140 | 141 | 149 | 150 | 151 | 158 | 159 | 160 | 161 | {showDelete && ( 162 | setOpenDialog(true)} 168 | style={{ marginLeft: 10 }} 169 | > 170 | 171 | 172 | )} 173 | 174 | 175 | 176 |
177 |
178 | 179 | 184 | 185 | {"Jeni të sigurtë për fshirjen e këtij postimi?"} 186 | 187 | 188 | 191 | 194 | 195 | 196 | 197 | ); 198 | } 199 | 200 | SinglePost.propTypes = { 201 | post: PropTypes.object, 202 | showDelete: PropTypes.bool, 203 | handleDelete: PropTypes.func, 204 | }; 205 | -------------------------------------------------------------------------------- /src/components/post/dialog-fullscreen-component.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import IconButton from "@material-ui/core/IconButton"; 4 | import Dialog from "@material-ui/core/Dialog"; 5 | import Divider from "@material-ui/core/Divider"; 6 | import CloseIcon from "@material-ui/icons/Close"; 7 | import { Slide } from "@material-ui/core"; 8 | import FeaturedPost from "./post-component"; 9 | import { Container, Fab } from "@material-ui/core"; 10 | import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder"; 11 | import ShareIcon from "@material-ui/icons/Share"; 12 | import { SavePost } from "../../services/storageService"; 13 | import { ShareAPI } from "../../utils/functions"; 14 | import SnackbarNotify from '../snackbar-notify-component'; 15 | // import { useHistory } from 'react-router-dom'; 16 | 17 | const useStyles = makeStyles((theme) => ({ 18 | appBar: { 19 | position: "relative", 20 | }, 21 | title: { 22 | marginLeft: theme.spacing(2), 23 | flex: 1, 24 | }, 25 | fab: { 26 | position: "fixed", 27 | bottom: theme.spacing(2), 28 | right: theme.spacing(2), 29 | }, 30 | button: { 31 | margin: theme.spacing(1), 32 | }, 33 | })); 34 | 35 | const Transition = React.forwardRef(function Transition(props, ref) { 36 | return ; 37 | }); 38 | 39 | export default function FullScreenPostDialog(props) { 40 | const classes = useStyles(); 41 | const [openSnackbarNotify, setOpenSnackbarNotify] = useState(false); 42 | 43 | const handleClose = () => { 44 | props.handlePost(null); 45 | }; 46 | 47 | const handleSavePost = () => { 48 | SavePost(props.post); 49 | setOpenSnackbarNotify(true); 50 | setTimeout(() => { 51 | setOpenSnackbarNotify(false); 52 | }, 2000); 53 | } 54 | 55 | const handleShare = () => { 56 | const title = props.post.title; 57 | const text = `Une po lexoj nga webi Tech News. Lexo postimin ne linkun origjinal: ${props.post.title}` 58 | const url = props.post.originalLink; 59 | 60 | ShareAPI(title, text, url); 61 | } 62 | 63 | let open = !!props.post; 64 | 65 | return ( 66 |
67 | {openSnackbarNotify && ( 68 | 69 | )} 70 | {/* */} 73 | 79 | {/* 80 | 81 | 87 | 88 | 89 | 90 | Sound 91 | 92 | 95 | 96 | */} 97 |
98 |
99 | 100 | {props.post && } 101 | 102 | 103 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | 124 |

125 | 132 | 133 | 134 |
135 |
136 |
137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/components/post/post-component.css: -------------------------------------------------------------------------------- 1 | .description .wp-block-image { 2 | text-align: center; 3 | } 4 | 5 | .description img{ 6 | max-width: 100%; 7 | } -------------------------------------------------------------------------------- /src/components/post/post-component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Paper from "@material-ui/core/Paper"; 5 | import Typography from "@material-ui/core/Typography"; 6 | import Grid from "@material-ui/core/Grid"; 7 | import Button from "@material-ui/core/Button"; 8 | import Divider from "@material-ui/core/Divider"; 9 | import "./post-component.css"; 10 | import { ToDateTime } from '../../utils/functions'; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | mainFeaturedPost: { 14 | position: "relative", 15 | backgroundColor: theme.palette.grey[800], 16 | color: theme.palette.common.white, 17 | marginBottom: theme.spacing(4), 18 | // backgroundImage: "url(https://source.unsplash.com/random)", 19 | backgroundSize: "cover", 20 | backgroundRepeat: "no-repeat", 21 | backgroundPosition: "center", 22 | minHeight: 320 23 | }, 24 | overlay: { 25 | position: "absolute", 26 | top: 0, 27 | bottom: 0, 28 | right: 0, 29 | left: 0, 30 | backgroundColor: "rgba(0,0,0,.3)", 31 | }, 32 | mainFeaturedPostContent: { 33 | margin: 40, 34 | position: "relative", 35 | padding: theme.spacing(3), 36 | [theme.breakpoints.up("md")]: { 37 | padding: theme.spacing(10), 38 | paddingRight: 0, 39 | }, 40 | }, 41 | buttonsDiv: { 42 | margin: 5, 43 | }, 44 | buttons: { 45 | marginRight: 15, 46 | }, 47 | })); 48 | 49 | export default function FeaturedPost(props) { 50 | const classes = useStyles(); 51 | const { post } = props; 52 | 53 | return ( 54 |
55 | 59 | 60 | 61 | 62 | 63 | {post.title} 64 | 65 | 66 | 67 | 76 | {/* 82 | 83 | 84 | 90 | 91 | */} 92 | {ToDateTime(post.date)} 93 | 94 | 95 | 100 | 101 |
102 | ); 103 | } 104 | 105 | FeaturedPost.propTypes = { 106 | post: PropTypes.object, 107 | }; 108 | -------------------------------------------------------------------------------- /src/components/settings/preferences-component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FormControlLabel from "@material-ui/core/FormControlLabel"; 3 | import Switch from "@material-ui/core/Switch"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | import Grid from "@material-ui/core/Grid"; 6 | import { useSelector, useDispatch } from "react-redux"; 7 | import { setDarkTheme } from "../../redux/actions/actions"; 8 | const useStyles = makeStyles((theme) => ({ 9 | root: { 10 | display: "flex", 11 | flexWrap: "wrap", 12 | marginTop: 15, 13 | }, 14 | formControl: { 15 | minWidth: 120, 16 | }, 17 | })); 18 | 19 | export default function SettingsForm() { 20 | const classes = useStyles(); 21 | const darkTheme = useSelector((state) => state.darkTheme); 22 | const dispatch = useDispatch(); 23 | 24 | const handleChange = (event) => { 25 | if (event.target.name === "darkTheme") 26 | dispatch(setDarkTheme(event.target.checked)); 27 | // setState({ ...state, [event.target.name]: event.target.checked }); 28 | }; 29 | 30 | return ( 31 |
32 | 33 | 34 | 35 | 36 |

} 38 | label="Modaliteti errësirë (Dark Mode)" 39 | labelPlacement="start" 40 | /> 41 |
42 | 43 | 50 | } 51 | label="" 52 | labelPlacement="start" 53 | /> 54 | 55 |
{" "} 56 | {/*end container*/} 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/sidebar-menu-component.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import Drawer from "@material-ui/core/Drawer"; 4 | import CssBaseline from "@material-ui/core/CssBaseline"; 5 | import List from "@material-ui/core/List"; 6 | import Divider from "@material-ui/core/Divider"; 7 | import IconButton from "@material-ui/core/IconButton"; 8 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; 9 | import ListItem from "@material-ui/core/ListItem"; 10 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 11 | import ListItemText from "@material-ui/core/ListItemText"; 12 | import SearchIcon from "@material-ui/icons/Search"; 13 | import HomeIcon from "@material-ui/icons/Home"; 14 | import FavoriteIcon from "@material-ui/icons/Favorite"; 15 | import BookmarksIcon from "@material-ui/icons/Bookmarks"; 16 | import SettingsIcon from "@material-ui/icons/Settings"; 17 | import { useHistory } from "react-router-dom"; 18 | import Constants from "../constants/constants"; 19 | import { useDispatch } from "react-redux"; 20 | import { setTitle } from "../redux/actions/actions"; 21 | 22 | const drawerWidth = 240; 23 | 24 | const useStyles = makeStyles((theme) => ({ 25 | root: { 26 | display: "flex", 27 | }, 28 | appBar: { 29 | transition: theme.transitions.create(["margin", "width"], { 30 | easing: theme.transitions.easing.sharp, 31 | duration: theme.transitions.duration.leavingScreen, 32 | }), 33 | }, 34 | appBarShift: { 35 | width: `calc(100% - ${drawerWidth}px)`, 36 | marginLeft: drawerWidth, 37 | transition: theme.transitions.create(["margin", "width"], { 38 | easing: theme.transitions.easing.easeOut, 39 | duration: theme.transitions.duration.enteringScreen, 40 | }), 41 | }, 42 | menuButton: { 43 | marginRight: theme.spacing(2), 44 | }, 45 | hide: { 46 | display: "none", 47 | }, 48 | drawer: { 49 | width: drawerWidth, 50 | flexShrink: 0, 51 | }, 52 | drawerPaper: { 53 | width: drawerWidth, 54 | }, 55 | drawerHeader: { 56 | display: "flex", 57 | alignItems: "center", 58 | padding: theme.spacing(0, 1), 59 | // necessary for content to be below app bar 60 | ...theme.mixins.toolbar, 61 | justifyContent: "space-between", 62 | minHeight: "44px !important", 63 | }, 64 | })); 65 | 66 | export default function SideBarMenu({ open, handleOpen }) { 67 | const classes = useStyles(); 68 | // const theme = useTheme(); 69 | const history = useHistory(); 70 | const [value, setValue] = React.useState(history.location.pathname); 71 | const dispatch = useDispatch(); 72 | 73 | const handleTitle = (title) => dispatch(setTitle(title)); 74 | 75 | const setTitleByRoute = (value) => { 76 | switch (value) { 77 | case "/": 78 | handleTitle(Constants.appName); 79 | break; 80 | case "/search": 81 | handleTitle("Kërkoni"); 82 | break; 83 | case "/favorites": 84 | handleTitle("Preferencat"); 85 | break; 86 | case "/saved": 87 | handleTitle("Postimet e ruajtura"); 88 | break; 89 | case "/settings": 90 | handleTitle("Cilësimet"); 91 | break; 92 | default: 93 | handleTitle(Constants.appName); 94 | break; 95 | } 96 | }; 97 | 98 | const handleChange = (newValue) => { 99 | history.push(newValue); 100 | setValue(newValue); 101 | // setTitleByRoute(newValue); 102 | }; 103 | 104 | const isSelected = (route) => { 105 | return history.location.pathname === route; 106 | }; 107 | 108 | useEffect(() => { 109 | setTitleByRoute(value); 110 | }, [value]); 111 | 112 | return ( 113 |
114 | 115 | 124 |
125 | {Constants.appName} 126 | 127 | 128 | 129 |
130 | 131 | 132 | handleChange("/")} 137 | > 138 | 139 | 140 | 141 | 142 | 143 | handleChange("/search")} 148 | > 149 | 150 | 151 | 152 | 153 | 154 | handleChange("/favorites")} 159 | > 160 | 161 | 162 | 163 | 164 | 165 | handleChange("/saved")} 170 | > 171 | 172 | 173 | 174 | 175 | 176 | handleChange("/settings")} 181 | > 182 | 183 | 184 | 185 | 186 | 187 | 188 |
189 |
190 | ); 191 | } 192 | -------------------------------------------------------------------------------- /src/components/skeletons-component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Skeleton from "@material-ui/lab/Skeleton"; 5 | import Grid from "@material-ui/core/Grid"; 6 | 7 | const useStyles = makeStyles({ 8 | root: {}, 9 | media: { 10 | height: 190, 11 | }, 12 | }); 13 | 14 | export default function Skeletons({showFeaturedSkeleton}) { 15 | const classes = useStyles(); 16 | 17 | 18 | return ( 19 | <> 20 | {showFeaturedSkeleton && 21 | <> 22 | 23 | 24 | 25 | 26 | } 27 |

28 | 29 | {Array.from(new Array(3)).map((item, index) => ( 30 | 31 | 36 | 41 | 46 | 51 | 56 | 57 | ))} 58 | 59 | 60 | ); 61 | } 62 | 63 | Skeletons.protoTypes = { 64 | showFeaturedSkeleton: PropTypes.bool 65 | } 66 | -------------------------------------------------------------------------------- /src/components/snackbar-no-internet-component.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Snackbar, IconButton} from "@material-ui/core"; 3 | import CloseIcon from "@material-ui/icons/Close"; 4 | 5 | export default function SnackbarNoInternet() { 6 | const [open, setOpen ] = useState(!navigator.onLine); 7 | 8 | setTimeout(() => { 9 | setOpen(false); 10 | }, 10 * 1000); 11 | 12 | return ( setOpen(!open)} 21 | > 22 | 23 | 24 | } 25 | />) 26 | } -------------------------------------------------------------------------------- /src/components/snackbar-notify-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Snackbar from '@material-ui/core/Snackbar'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import CloseIcon from '@material-ui/icons/Close'; 5 | 6 | export default function SnackbarNotify({message}) { 7 | const [open, setOpen] = React.useState(!!message.length); 8 | 9 | const handleClose = (event, reason) => { 10 | if (reason === 'clickaway') { 11 | return; 12 | } 13 | 14 | setOpen(false); 15 | }; 16 | 17 | return ( 18 |
19 | 30 | 31 | 32 | 33 | 34 | } 35 | /> 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/theme.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | ThemeProvider, 4 | unstable_createMuiStrictModeTheme as createMuiTheme, 5 | } from "@material-ui/core/styles"; 6 | import FullScreenPostDialog from "./post/dialog-fullscreen-component"; 7 | import { 8 | orange, 9 | lightBlue, 10 | deepPurple, 11 | deepOrange, 12 | } from "@material-ui/core/colors"; 13 | import { useDispatch, shallowEqual, useSelector } from "react-redux"; 14 | import { setPost } from "../redux/actions/actions"; 15 | 16 | export default function Theme({ children }) { 17 | const dispatch = useDispatch(); 18 | const post = useSelector((state) => state.post, shallowEqual); 19 | const darkTheme = useSelector((state) => state.darkTheme); 20 | 21 | const palletType = darkTheme ? "dark" : "light"; 22 | const mainPrimaryColor = darkTheme ? orange[500] : lightBlue[500]; 23 | const mainSecondaryColor = darkTheme ? deepOrange[900] : deepPurple[500]; 24 | 25 | const Theme = { 26 | palette: { 27 | type: palletType, 28 | primary: { 29 | main: mainPrimaryColor, 30 | }, 31 | secondary: { 32 | main: mainSecondaryColor, 33 | }, 34 | }, 35 | }; 36 | const theme = createMuiTheme(Theme); 37 | 38 | const handlePost = (post) => dispatch(setPost(post)); 39 | 40 | return ( 41 | 42 | 43 | {children} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/constants/constants.js: -------------------------------------------------------------------------------- 1 | const Constants = { 2 | appName: 'Tech News', 3 | appVersion: 'v.1.0', 4 | localStoragePrefix: 'tech_new_app_' 5 | } 6 | 7 | export default Constants; -------------------------------------------------------------------------------- /src/customHooks/custom-hooks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function usePrevious(value) { 4 | const ref = React.useRef(); 5 | React.useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | } 10 | 11 | // use async operation with automatic abortion on unmount 12 | export function useAsync(asyncFn, onSuccess) { 13 | React.useEffect(() => { 14 | let isMounted = true; 15 | asyncFn().then(data => { 16 | if (isMounted) onSuccess(data); 17 | }); 18 | return () => { 19 | isMounted = false; 20 | }; 21 | }, [asyncFn, onSuccess]); 22 | } 23 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://cra.link/PWA 18 | // serviceWorkerRegistration.unregister(); 19 | serviceWorkerRegistration.register(); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/favorites-page.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import ChipsComponent from "../components/favorites/chips-component"; 4 | 5 | const useStyles = makeStyles({ 6 | root: {}, 7 | }); 8 | 9 | export default function FavoritesPage() { 10 | const classes = useStyles(); 11 | 12 | return ( 13 |
14 |

Zgjidhni perferencat në bazë të të cilave do ju shfaqen postimet

15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/home-page.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import SectionsHeader from "../components/home/sections-component"; 4 | import FeaturedPost from "../components/featured-post-component"; 5 | import Posts from "../components/home/posts-component"; 6 | import SiteService from "../services/siteService"; 7 | import { IconButton } from "@material-ui/core"; 8 | import Skeletons from "../components/skeletons-component"; 9 | import { usePrevious } from "../customHooks/custom-hooks"; 10 | import Snackbar from "@material-ui/core/Snackbar"; 11 | import CloseIcon from "@material-ui/icons/Close"; 12 | import SnackbarNoInternet from "../components/snackbar-no-internet-component"; 13 | import { useDispatch, useSelector } from "react-redux"; 14 | import { setPosts } from "../redux/actions/actions"; 15 | 16 | const useStyles = makeStyles((theme) => ({ 17 | root: {}, 18 | close: { 19 | padding: theme.spacing(0.5), 20 | }, 21 | })); 22 | 23 | const service = new SiteService(); 24 | 25 | export default function HomePage() { 26 | const classes = useStyles(); 27 | const posts = useSelector((state) => state.posts); 28 | const tabSelected = useSelector((state) => state.tabSelected); 29 | const dispatch = useDispatch(); 30 | 31 | const [isLoading, setIsLoading] = useState(true); 32 | const [errors, setErrors] = useState(""); 33 | 34 | const tabSelectedPrev = usePrevious(tabSelected); 35 | useEffect(() => { 36 | // if (!categories) 37 | // service.getCategories().then((data) => handleCategories(data)); 38 | // if (!tags) service.getTags().then((data) => handleTags(data)); 39 | if (!posts || (tabSelectedPrev && tabSelectedPrev !== tabSelected)) { 40 | setIsLoading(true); 41 | let searchVal = tabSelected.index > 0 ? tabSelected.value : ""; 42 | service 43 | .getPosts(searchVal) 44 | .then((data) => { 45 | dispatch(setPosts(data)); 46 | setIsLoading(false); 47 | }) 48 | .catch((error) => { 49 | setErrors(error.errorMessage); 50 | }); 51 | } else setIsLoading(false); 52 | }, [tabSelected.index]); 53 | 54 | const sections = [ 55 | { title: "Të gjitha", url: "#" }, 56 | { title: "Teknologji", url: "#" }, 57 | { title: "Apple", url: "#" }, 58 | { title: "Microsoft", url: "#" }, 59 | { title: "Android", url: "#" }, 60 | { title: "Samsung", url: "#" }, 61 | { title: "Shkence", url: "#" }, 62 | { title: "Programim", url: "#" }, 63 | { title: "Design", url: "#" }, 64 | { title: "Nasa", url: "#" }, 65 | { title: "Covid", url: "#" }, 66 | ]; 67 | 68 | return ( 69 |
70 | {/*

Faqja kryesore

*/} 71 | 72 |
73 | 74 | {!isLoading && posts.length > 0 ? ( 75 | <> 76 | 77 | index !== 0)} />{" "} 78 | {/* get all but not first item (because is used in FeaturedPost) */} 79 | 80 | ) : ( 81 | <> 82 | setErrors("")} 93 | > 94 | 95 | 96 | } 97 | /> 98 | 99 | 100 | )} 101 | {/* */} 102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/post-page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import Post from "../components/post/post-component"; 4 | import CircularProgress from "@material-ui/core/CircularProgress"; 5 | import { useLocation } from 'react-router-dom'; 6 | 7 | const useStyles = makeStyles({ 8 | root: { 9 | marginTop: 15, 10 | }, 11 | }); 12 | 13 | export default function PostPage() { 14 | const classes = useStyles(); 15 | const location = useLocation(); 16 | 17 | // const [post, setPost] = useState(location.state.post); 18 | 19 | useEffect(() => { 20 | 21 | }, []); 22 | 23 | return ( 24 |
25 | {location.state.post ? :
} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/saved-page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { TextField, Grid, Divider } from "@material-ui/core"; 4 | import Posts from "../components/home/posts-component"; 5 | import { GetValue } from "../services/storageService"; 6 | 7 | const useStyles = makeStyles({ 8 | root: {}, 9 | gridContainer: { 10 | display: "flex", 11 | alignItems: "center", 12 | }, 13 | }); 14 | 15 | export default function SavedPage() { 16 | const classes = useStyles(); 17 | 18 | const [searchVal, setSearchVal] = useState(""); 19 | const [posts, setPosts] = useState(); 20 | 21 | useEffect(() => { 22 | if (searchVal.length > 2) { 23 | const posts = GetValue("savedPost"); 24 | if (posts) { 25 | const postsFound = posts.filter( 26 | (item) => 27 | item.title.toLowerCase().indexOf(searchVal.toLowerCase()) > -1 28 | ); 29 | setPosts(postsFound); 30 | } 31 | } else { 32 | setPosts(GetValue("savedPost")); 33 | } 34 | }, [searchVal]); 35 | 36 | const handleChange = (ev) => { 37 | setSearchVal(ev.target.value); 38 | }; 39 | 40 | const handleDelete = (post) => { 41 | setPosts(GetValue("savedPost")); 42 | }; 43 | 44 | return ( 45 |
46 | {/*

Kërko

*/} 47 | 48 | 49 | 50 | 65 | 66 | 67 | 68 |
69 | 70 | {posts && posts.length ? ( 71 | 72 | ) : ( 73 |

74 | Asnjë postim nuk u gjend. 75 |

76 | )} 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/pages/search-page.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { TextField, Grid, Divider, Snackbar, IconButton } from "@material-ui/core"; 4 | import CloseIcon from "@material-ui/icons/Close"; 5 | import Skeletons from "../components/skeletons-component"; 6 | import Posts from "../components/home/posts-component"; 7 | import SiteService from "../services/siteService"; 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { setSearchPosts } from "../redux/actions/actions"; 10 | 11 | const useStyles = makeStyles({ 12 | root: {}, 13 | gridContainer: { 14 | display: "flex", 15 | alignItems: "center", 16 | }, 17 | }); 18 | 19 | const service = new SiteService(); 20 | 21 | export default function SearchPage() { 22 | const classes = useStyles(); 23 | const searchPosts = useSelector(state => state.searchPosts); 24 | const dispatch = useDispatch(); 25 | 26 | const [isLoading, setIsLoading] = useState(false); 27 | const [errors, setErrors] = useState(""); 28 | const [searchVal, setSearchVal] = useState(searchPosts.searchValue); 29 | 30 | useEffect(() => { 31 | const delaySearch = setTimeout(() => { 32 | //wait 1 sec until user stop typing 33 | if (searchVal.length > 2) { 34 | setIsLoading(true); 35 | service 36 | .getPosts(searchVal, 15) 37 | .then((data) => { 38 | dispatch(setSearchPosts({ searchValue: searchVal, posts: data })); 39 | setIsLoading(false); 40 | }) 41 | .catch((error) => { 42 | setErrors(error.errorMessage); 43 | }); 44 | } else { 45 | dispatch(setSearchPosts({ searchValue: "", posts: [] })); 46 | } 47 | }, 1000); 48 | 49 | return () => clearTimeout(delaySearch); 50 | }, [searchVal]); 51 | 52 | const handleChange = (ev) => { 53 | setSearchVal(ev.target.value); 54 | }; 55 | 56 | return ( 57 |
58 | {/*

Kërko

*/} 59 | 60 | 61 | 62 | 77 | 78 | 79 | 80 |
81 | 82 | {!isLoading && searchPosts.posts ? ( 83 | <> 84 | 85 | 86 | ) : ( 87 | isLoading && 88 | )} 89 | 90 | 91 | setErrors('')} 102 | > 103 | 104 | 105 | } 106 | /> 107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /src/pages/settings-page.js: -------------------------------------------------------------------------------- 1 | import { Typography, Link } from "@material-ui/core"; 2 | import React from "react"; 3 | import SettingsForm from "../components/settings/preferences-component"; 4 | import Constants from "../constants/constants"; 5 | 6 | export default function SettingsPage() { 7 | return ( 8 | <> 9 | 10 |
11 |
12 | 13 |
14 | © Edison Neza 15 |
{Constants.appVersion} 16 |
17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/redux/actions/actions.js: -------------------------------------------------------------------------------- 1 | export const SET_TITLE = "SET_TITLE"; 2 | export const SET_DARK_THEME = "SET_DARK_THEME"; 3 | export const SET_POSTS = "SET_POSTS"; 4 | export const SET_POST = "SET_POST"; 5 | export const SET_CATEGORIES = "SET_CATEGORIES"; 6 | export const SET_TAGS = "SET_TAGS"; 7 | export const SET_TAB_SELECTED = "SET_TAB_SELECTED"; 8 | export const SET_SEARCH_POSTS = "SET_SEARCH_POSTS"; 9 | 10 | export function setTitle(title) { 11 | return { type: SET_TITLE, title: title }; 12 | } 13 | 14 | export function setDarkTheme(darkTheme) { 15 | return { type: SET_DARK_THEME, darkTheme }; 16 | } 17 | 18 | export function setPosts(posts) { 19 | return { type: SET_POSTS, posts }; 20 | } 21 | 22 | export function setPost(post) { 23 | return { type: SET_POST, post }; 24 | } 25 | 26 | export function setCategories(categories) { 27 | return { type: SET_CATEGORIES, categories }; 28 | } 29 | 30 | export function setTags(tags) { 31 | return { type: SET_TAGS, tags }; 32 | } 33 | 34 | export function setTabSelected(tabSelected) { 35 | return { type: SET_TAB_SELECTED, tabSelected }; 36 | } 37 | 38 | export function setSearchPosts(searchPosts) { 39 | return { type: SET_SEARCH_POSTS, searchPosts }; 40 | } 41 | -------------------------------------------------------------------------------- /src/redux/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_TITLE, 3 | SET_DARK_THEME, 4 | SET_POST, 5 | SET_POSTS, 6 | SET_CATEGORIES, 7 | SET_TAGS, 8 | SET_SEARCH_POSTS, 9 | SET_TAB_SELECTED, 10 | } from "../actions/actions"; 11 | import Constants from "../../constants/constants"; 12 | import { GetValue, SaveValue } from "../../services/storageService"; 13 | 14 | const initialState = { 15 | title: Constants.appName, 16 | darkTheme: GetValue("darkTheme"), 17 | posts: null, 18 | categories: null, 19 | tags: null, 20 | post: null, 21 | tabSelected: { index: 0, value: "" }, 22 | searchPosts: { 23 | searchValue: "", 24 | posts: null, 25 | }, 26 | }; 27 | 28 | function rootReducer(state = initialState, action) { 29 | switch (action.type) { 30 | case SET_TITLE: 31 | return { 32 | ...state, 33 | title: action.title, 34 | }; 35 | case SET_DARK_THEME: 36 | SaveValue("darkTheme", action.darkTheme); 37 | return { 38 | ...state, 39 | darkTheme: action.darkTheme, 40 | }; 41 | case SET_POST: 42 | return { 43 | ...state, 44 | post: action.post, 45 | }; 46 | case SET_POSTS: 47 | return { 48 | ...state, 49 | posts: action.posts, 50 | }; 51 | case SET_CATEGORIES: 52 | return { 53 | ...state, 54 | categories: action.categories, 55 | }; 56 | 57 | case SET_TAGS: 58 | return { 59 | ...state, 60 | tags: action.tags, 61 | }; 62 | 63 | case SET_SEARCH_POSTS: 64 | return { 65 | ...state, 66 | searchPosts: { 67 | searchValue: action.searchPosts.searchValue, 68 | posts: action.searchPosts.posts, 69 | }, 70 | }; 71 | 72 | case SET_TAB_SELECTED: 73 | return { 74 | ...state, 75 | tabSelected: { 76 | index: action.tabSelected.index, 77 | value: action.tabSelected.value, 78 | }, 79 | }; 80 | default: 81 | return state; 82 | } 83 | } 84 | 85 | export default rootReducer; 86 | -------------------------------------------------------------------------------- /src/redux/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import rootReducer from '../reducers/reducers'; 3 | 4 | export default createStore ( 5 | rootReducer, 6 | undefined, 7 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 8 | ) -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | // This service worker can be customized! 4 | // See https://developers.google.com/web/tools/workbox/modules 5 | // for the list of available Workbox modules, or add any other 6 | // code you'd like. 7 | // You can also remove this file if you'd prefer not to use a 8 | // service worker, and the Workbox build step will be skipped. 9 | 10 | import { clientsClaim } from 'workbox-core'; 11 | import { ExpirationPlugin } from 'workbox-expiration'; 12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; 13 | import { registerRoute } from 'workbox-routing'; 14 | import { StaleWhileRevalidate } from 'workbox-strategies'; 15 | 16 | clientsClaim(); 17 | 18 | // Precache all of the assets generated by your build process. 19 | // Their URLs are injected into the manifest variable below. 20 | // This variable must be present somewhere in your service worker file, 21 | // even if you decide not to use precaching. See https://cra.link/PWA 22 | precacheAndRoute(self.__WB_MANIFEST); 23 | 24 | // Set up App Shell-style routing, so that all navigation requests 25 | // are fulfilled with your index.html shell. Learn more at 26 | // https://developers.google.com/web/fundamentals/architecture/app-shell 27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); 28 | registerRoute( 29 | // Return false to exempt requests from being fulfilled by index.html. 30 | ({ request, url }) => { 31 | // If this isn't a navigation, skip. 32 | if (request.mode !== 'navigate') { 33 | return false; 34 | } // If this is a URL that starts with /_, skip. 35 | 36 | if (url.pathname.startsWith('/_')) { 37 | return false; 38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip. 39 | 40 | if (url.pathname.match(fileExtensionRegexp)) { 41 | return false; 42 | } // Return true to signal that we want to use the handler. 43 | 44 | return true; 45 | }, 46 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') 47 | ); 48 | 49 | // An example runtime caching route for requests that aren't handled by the 50 | // precache, in this case same-origin .png requests like those from in public/ 51 | registerRoute( 52 | // Add in any other file extensions or routing criteria as needed. 53 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. 54 | new StaleWhileRevalidate({ 55 | cacheName: 'images', 56 | plugins: [ 57 | // Ensure that once this runtime cache reaches a maximum size the 58 | // least-recently used images are removed. 59 | new ExpirationPlugin({ maxEntries: 50 }), 60 | ], 61 | }) 62 | ); 63 | 64 | // This allows the web app to trigger skipWaiting via 65 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 66 | self.addEventListener('message', (event) => { 67 | if (event.data && event.data.type === 'SKIP_WAITING') { 68 | self.skipWaiting(); 69 | } 70 | }); 71 | 72 | // Any other custom service worker logic can go here. 73 | -------------------------------------------------------------------------------- /src/serviceWorkerRegistration.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | export function register(config) { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Let's check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl, config); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://cra.link/PWA' 45 | ); 46 | }); 47 | } else { 48 | // Is not localhost. Just register service worker 49 | registerValidSW(swUrl, config); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl, config) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then((registration) => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker == null) { 62 | return; 63 | } 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === 'installed') { 66 | if (navigator.serviceWorker.controller) { 67 | // At this point, the updated precached content has been fetched, 68 | // but the previous service worker will still serve the older 69 | // content until all client tabs are closed. 70 | console.log( 71 | 'New content is available and will be used when all ' + 72 | 'tabs for this page are closed. See https://cra.link/PWA.' 73 | ); 74 | 75 | // Execute callback 76 | if (config && config.onUpdate) { 77 | config.onUpdate(registration); 78 | } 79 | } else { 80 | // At this point, everything has been precached. 81 | // It's the perfect time to display a 82 | // "Content is cached for offline use." message. 83 | console.log('Content is cached for offline use.'); 84 | 85 | // Execute callback 86 | if (config && config.onSuccess) { 87 | config.onSuccess(registration); 88 | } 89 | } 90 | } 91 | }; 92 | }; 93 | }) 94 | .catch((error) => { 95 | console.error('Error during service worker registration:', error); 96 | }); 97 | } 98 | 99 | function checkValidServiceWorker(swUrl, config) { 100 | // Check if the service worker can be found. If it can't reload the page. 101 | fetch(swUrl, { 102 | headers: { 'Service-Worker': 'script' }, 103 | }) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log('No internet connection found. App is running in offline mode.'); 124 | }); 125 | } 126 | 127 | export function unregister() { 128 | if ('serviceWorker' in navigator) { 129 | navigator.serviceWorker.ready 130 | .then((registration) => { 131 | registration.unregister(); 132 | }) 133 | .catch((error) => { 134 | console.error(error.message); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/services/configService.js: -------------------------------------------------------------------------------- 1 | export default class Configurations { 2 | constructor() { 3 | this.configUrl = 4 | "https://raw.githubusercontent.com/edisonneza/edisonneza.github.io/configs/publicConfigs/config.json"; 5 | } 6 | 7 | getAll() { 8 | return fetch(this.configUrl) 9 | .then((resp) => resp.json()) 10 | .then((data) => data) 11 | .catch((err) => err); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/services/siteService.js: -------------------------------------------------------------------------------- 1 | import { SaveValue, GetValue } from "./storageService"; 2 | 3 | export default class SiteService { 4 | constructor(baseUrl) { 5 | this.baseUrl = baseUrl; 6 | if (!baseUrl) this.baseUrl = "https://shop.shpresa.al/wp-json/wp/v2"; 7 | } 8 | 9 | getPosts(searchQuery, perPage=10) { 10 | if (!navigator.onLine) { 11 | return new Promise((resolve, reject) => { 12 | if (GetValue("posts")) resolve(GetValue("posts")); 13 | else 14 | reject({ 15 | errorMessage: 16 | "Momentalisht nuk keni lidhje interneti dhe nuk keni shikuar asnje post deri tani. Provoni perseri pasi te jeni ne linje.", 17 | }); 18 | }); 19 | } else { 20 | return fetch( 21 | `${this.baseUrl}/posts?_embed=wp:featuredmedia&per_page=${perPage}&search=${searchQuery}` 22 | ) 23 | .then((resp) => resp.json()) 24 | .then((data) => { 25 | const posts = data.map((data) => { 26 | // console.log(data) 27 | return { 28 | title: data.title.rendered, 29 | date: data.date, 30 | shortDesc: data.excerpt.rendered, 31 | description: data.content.rendered, 32 | image: data._embedded["wp:featuredmedia"]["0"].source_url, // "https://source.unsplash.com/random", 33 | imageText: "Image Text", 34 | link: "/post", 35 | originalLink: data.link, 36 | }; 37 | }); 38 | SaveValue("posts", posts); 39 | return posts; 40 | }) 41 | .catch((err) => err); 42 | } 43 | } 44 | 45 | getCategories() { 46 | if (!navigator.onLine) 47 | return new Promise((resolve, reject) => resolve(GetValue("categories"))); 48 | else { 49 | return fetch(this.baseUrl + "/categories") 50 | .then((resp) => resp.json()) 51 | .then((data) => { 52 | SaveValue("categories", data); 53 | return data; 54 | }) 55 | .catch((err) => err); 56 | } 57 | } 58 | 59 | getTags() { 60 | if (!navigator.onLine) 61 | return new Promise((resolve, reject) => resolve(GetValue("tags"))); 62 | else { 63 | // return fetch(this.baseUrl + "/tags") 64 | // .then((resp) => resp.json()) 65 | // .then((data) => { 66 | // SaveValue("tags", data); 67 | // return data; 68 | // }) 69 | // .catch((err) => err); 70 | return new Promise((resolve, reject) => { 71 | const localStorageTags = GetValue('tags'); 72 | if(!localStorageTags || !localStorageTags.length){ 73 | const initialTags = [ 74 | { value: "Apple", active: false }, 75 | { value: "Technology", active: false }, 76 | { value: "Microsoft", active: false }, 77 | { value: "Android", active: false }, 78 | { value: "iOS", active: false }, 79 | { value: "Shkence", active: false }, 80 | { value: "Samsung", active: false }, 81 | { value: "iPhone", active: false }, 82 | { value: "OnePlus", active: false }, 83 | { value: "Nokia", active: false }, 84 | { value: "Programming", active: false }, 85 | { value: "Website", active: false }, 86 | { value: "Web App", active: false }, 87 | { value: ".NET 5", active: false }, 88 | { value: "ASP.NET", active: false }, 89 | { value: "C#", active: false }, 90 | { value: "Java", active: false }, 91 | { value: "Javascript", active: false }, 92 | { value: "Typescript", active: false }, 93 | { value: "PHP", active: false }, 94 | { value: "React", active: false }, 95 | { value: "Angular", active: false }, 96 | { value: "Covid", active: false }, 97 | ]; 98 | SaveValue('tags', initialTags); 99 | } 100 | 101 | return resolve(GetValue('tags')); 102 | }); 103 | } 104 | } 105 | 106 | saveTags(value) { //to save all 107 | return new Promise((resolve, reject) => { 108 | const tags = GetValue('tags'); 109 | const newTags = tags.map((item) => { 110 | return item.value !== value ? item : { value, active: !item.active }; 111 | }); 112 | SaveValue('tags', newTags); 113 | resolve(GetValue('tags')); 114 | }); 115 | } 116 | 117 | getPostByHref(href) { 118 | return fetch(href) 119 | .then((resp) => resp.json()) 120 | .then((data) => { 121 | const post = { 122 | title: data.title.rendered, 123 | date: data.date, 124 | description: data.content.rendered, 125 | image: data._embedded["wp:featuredmedia"]["0"].source_url, // "https://source.unsplash.com/random", 126 | imageText: "Image Text", 127 | link: "/post", 128 | }; 129 | return post; 130 | }) 131 | .catch((err) => err); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/services/storageService.js: -------------------------------------------------------------------------------- 1 | import Constants from '../constants/constants'; 2 | 3 | export function SaveValue(name, values) { 4 | localStorage.setItem(Constants.localStoragePrefix + name, JSON.stringify(values)); 5 | } 6 | 7 | export function GetValue(name) { 8 | return JSON.parse(localStorage.getItem(Constants.localStoragePrefix + name)); 9 | } 10 | 11 | export function SavePost(post){ 12 | let savedPost = GetValue('savedPost'); 13 | if(savedPost){ 14 | const postExist = savedPost.filter(item => item.originalLink === post.originalLink).length > 0; 15 | if(!postExist) 16 | SaveValue('savedPost', [...savedPost, {...post}]); 17 | }else{ 18 | SaveValue('savedPost', [{...post}]) 19 | } 20 | } 21 | 22 | // export function GetValues() { 23 | // let items = []; 24 | // for (var key in localStorage) { 25 | // if (key.indexOf("StorageName") === 0) { 26 | // const item = JSON.parse(localStorage[key]); 27 | // const arr = { key: key, ...item }; 28 | // items.push(JSON.stringify(arr)); 29 | // } 30 | // } 31 | 32 | // return items; 33 | // } 34 | 35 | export function DeleteValue(name) { 36 | localStorage.removeItem(Constants.localStoragePrefix + name); 37 | } 38 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/functions.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import 'moment/locale/sq'; 3 | 4 | 5 | export function ToDateTime(value){ 6 | return moment(value).locale('sq').format('DD/MM/YYYY hh:mm:ss'); 7 | } 8 | 9 | export function DateFromNow(value){ 10 | return moment(value).locale('sq').fromNow(); 11 | } 12 | 13 | export async function ShareAPI(title, text, url){ 14 | if (navigator.share === undefined) { 15 | console.log('Error: Unsupported feature: navigator.share'); 16 | return; 17 | } 18 | 19 | // const text = `Une po lexoj nga faqja Tech News. Lexo postimin nga linkun origjinal: ${props.post.title}` 20 | 21 | try { 22 | await navigator.share({title, text, url}); 23 | console.log('Successfully sent share'); 24 | } catch (error) { 25 | console.log('Error sharing: ' + error); 26 | } 27 | } 28 | 29 | export function isMobile(){ 30 | return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 31 | } --------------------------------------------------------------------------------