├── .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 | 
30 |
31 | 
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 |
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 |
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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------