├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── api.js
├── components
├── NavBar
│ ├── Brand.js
│ ├── Link.js
│ ├── Menu.js
│ ├── MenuButton.js
│ └── index.js
└── Story
│ ├── Footer.js
│ ├── Header.js
│ ├── Styles.js
│ └── index.js
├── containers
└── StoryList.js
├── index.css
├── index.js
├── logo.svg
├── registerServiceWorker.js
└── utils
├── helper.js
└── media.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Andreas Reiterer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project is a ReactJS client for HackerNews stories. Since the [API](https://github.com/HackerNews/API) only provides read access
2 | it display the newest stories
3 |
4 | ## Roadmap
5 | * [x] List of stories (static)
6 | * [x] Get the content from HackerNews API
7 | * [ ] Paging
8 | * [ ] Show discussions/comments
9 |
10 | ## Try out
11 | If you want to try it out, just clone the repository and use
12 |
13 | ```
14 | npm -i
15 | npm start
16 | ```
17 |
18 | ## Libraries used
19 | * [styled-components](https://www.styled-components.com)
20 | * [re-base](https://github.com/tylermcginnis/re-base)
21 | * [firebase](https://github.com/firebase/)
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hackernewsclone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "firebase": "^4.8.1",
7 | "prop-types": "^15.6.0",
8 | "re-base": "^3.2.1",
9 | "react": "^16.2.0",
10 | "react-dom": "^16.2.0",
11 | "react-spinkit": "^3.0.0",
12 | "styled-components": "^2.3.3"
13 | },
14 | "devDependencies": {
15 | "react-scripts": "1.0.10"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test --env=jsdom",
21 | "eject": "react-scripts eject"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/areiterer/hackernews-client/9be9cd70682aecb1c58a14f62fbeafe27c3a8fd3/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | HackerNews Clone
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-intro {
18 | font-size: large;
19 | }
20 |
21 | @keyframes App-logo-spin {
22 | from { transform: rotate(0deg); }
23 | to { transform: rotate(360deg); }
24 | }
25 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { injectGlobal } from "styled-components";
3 |
4 | import Api from "./api";
5 | import StoryList from "./containers/StoryList";
6 | import NavBar from "./components/NavBar";
7 |
8 | // eslint-disable-next-line no-unused-expressions
9 | injectGlobal`
10 | @font-face {
11 | font-family: 'Verdana, Geneva, sans-serif'
12 | }
13 | body {
14 | margin: 0;
15 | }
16 | `;
17 |
18 | function fetchSingleStory(id, index) {
19 | const rank = index + 1;
20 | return new Promise(resolve => {
21 | Api.fetch(`/item/${id}`, {
22 | then(data) {
23 | let item = data;
24 | // add the rank since it does not exist yet
25 | item.rank = rank;
26 | resolve(item);
27 | }
28 | });
29 | });
30 | }
31 |
32 | class App extends Component {
33 | state = {
34 | newStories: []
35 | };
36 |
37 | fetchNewStories(storyIds) {
38 | let actions = storyIds.slice(0, 30).map(fetchSingleStory);
39 | let results = Promise.all(actions);
40 | results.then(data =>
41 | this.setState(
42 | Object.assign({}, this.state, {
43 | newStories: data
44 | })
45 | )
46 | );
47 | }
48 |
49 | componentDidMount() {
50 | Api.fetch(`/newstories`, {
51 | context: this,
52 | then(storyIds) {
53 | this.fetchNewStories(storyIds);
54 | }
55 | });
56 | }
57 |
58 | render() {
59 | return (
60 |
61 |
62 |
63 |
64 | );
65 | }
66 | }
67 |
68 | export default App;
69 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/api.js:
--------------------------------------------------------------------------------
1 | import rebase from "re-base";
2 | import firebase from "@firebase/app";
3 | import "@firebase/database";
4 |
5 | const HN_DATABASE_URL = "https://hacker-news.firebaseio.com";
6 | const HN_VERSION = "v0";
7 |
8 | firebase.initializeApp({ databaseURL: HN_DATABASE_URL });
9 | let db = firebase.database();
10 | let base = rebase.createClass(db);
11 |
12 | // Api is a wrapper around base, to include the version child path to the binding automatically.
13 | const Api = {
14 | /**
15 | * One way data binding from Firebase to a component's state.
16 | * @param {string} endpoint
17 | * @param {object} options
18 | * @return {object} An object which you can pass to 'removeBinding' if you want to remove the
19 | * listener while the component is still mounted.
20 | */
21 | bindToState(endpoint, options) {
22 | return base.bindToState(`/${HN_VERSION}${endpoint}`, options);
23 | },
24 |
25 | /**
26 | * Listens to a Firebase endpoint without binding changes to a state property. Instead a callback
27 | * will be invoked.
28 | *
29 | * @param {string} endpoint
30 | * @param {object} options
31 | * @returns
32 | */
33 | listenTo(endpoint, options) {
34 | return base.listenTo(`/${HN_VERSION}${endpoint}`, options);
35 | },
36 |
37 | /**
38 | * Retrieves data from Firebase once without setting up binding
39 | * @param {string} endpoint
40 | * @param {object} options
41 | * @return {Promise} A Firebase Promise which resolves when the write is complete and rejects
42 | * if there is an error.
43 | */
44 | fetch(endpoint, options) {
45 | return base.fetch(`/${HN_VERSION}${endpoint}`, options);
46 | }
47 | };
48 |
49 | export default Api;
50 |
--------------------------------------------------------------------------------
/src/components/NavBar/Brand.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | import { StyledLink } from "./Link";
4 |
5 | const Brand = styled(StyledLink)`
6 | float:left;
7 | font-size: 16px;
8 | font-weight: bold;
9 |
10 | padding: 11px 14px;
11 |
12 | margin-right: 20px;
13 |
14 | &:hover {
15 | color: #f2f2f2;
16 | }
17 | `;
18 |
19 | export default Brand;
20 |
--------------------------------------------------------------------------------
/src/components/NavBar/Link.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 | import { desktop } from "../../utils/media";
3 |
4 | export const StyledLink = styled.a`
5 | display: inline-block;
6 | color: #f2f2f2;
7 | text-align: center;
8 |
9 | padding: 12px 14px;
10 |
11 | text-decoration: none;
12 | font-size: 14px;
13 |
14 | ${desktop(css`
15 | &:hover {
16 | color: #fff;
17 | }`)};
18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/NavBar/Menu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled, { css } from "styled-components";
3 | import { desktop, maxTablet } from "../../utils/media";
4 |
5 | import { StyledLink } from "./Link";
6 |
7 | const MenuWrapper = styled.div`
8 | overflow: hidden;
9 | display: none;
10 |
11 | ${desktop(css`
12 | display:block;
13 | `)};
14 |
15 | ${props =>
16 | maxTablet(
17 | props.isHidden ||
18 | css`
19 | display:block;
20 | width: 100%;
21 | margin-top: 40px;
22 | `
23 | )};
24 | `;
25 | const MenuItem = styled(StyledLink)`
26 | float: left;
27 |
28 | ${props =>
29 | maxTablet(
30 | props.isHidden ||
31 | css`
32 | float: none;
33 | display:block;
34 | text-align: left;
35 |
36 | font-size:12px;
37 | letter-spacing: 1px;
38 | color: #333;
39 | background-color: #f6f6ef;
40 | border-bottom: 1px solid rgba(255,255,255,0.75);
41 |
42 | &:active {
43 | background-color: #e6e6e6;
44 | font-weight: bold;
45 | }
46 | `
47 | )};
48 | `;
49 |
50 | function Menu(props) {
51 | const children = props.children;
52 |
53 | return (
54 |
55 | {React.Children.map(children, child =>
56 | React.cloneElement(child, { isHidden: props.isHidden })
57 | )}
58 |
59 | );
60 | }
61 |
62 | export default Menu;
63 | export { MenuItem };
64 |
--------------------------------------------------------------------------------
/src/components/NavBar/MenuButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled, { css } from "styled-components";
3 | import { desktop } from "../../utils/media";
4 |
5 | import { StyledLink } from "./Link";
6 |
7 | const MenuButton = styled(StyledLink).attrs({
8 | height: null,
9 | width: null
10 | })`
11 | height: 16px;
12 | position: absolute;
13 | right: 0;
14 | top: 0;
15 | display: block;
16 | line-height: 14px;
17 | cursor: pointer;
18 |
19 | &:hover {
20 | background: none;
21 | color: #f2f2f2;
22 | }
23 |
24 | ${desktop(css`
25 | display: none;
26 | `)};
27 | `;
28 |
29 | export default props => ☰;
30 |
--------------------------------------------------------------------------------
/src/components/NavBar/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 |
4 | import Menu, { MenuItem } from "./Menu";
5 | import MenuButton from "./MenuButton";
6 | import Brand from "./Brand";
7 |
8 | const Wrapper = styled.div`
9 | background-color: #333;
10 | overflow: hidden;
11 | box-shadow: 5px 1px 5px #888888;
12 | `;
13 |
14 | class NavBar extends Component {
15 | state = {
16 | isMenuHidden: true
17 | };
18 |
19 | onToggleMenu = () => {
20 | this.setState((prevState, props) => {
21 | return { isMenuHidden: !prevState.isMenuHidden };
22 | });
23 | };
24 |
25 | render() {
26 | return (
27 |
28 | HNews
29 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default NavBar;
39 |
--------------------------------------------------------------------------------
/src/components/Story/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { FooterWrapper, FooterLink } from "./Styles";
5 | import * as utils from "../../utils/helper";
6 |
7 | export default function Footer(props) {
8 | const userUrl = utils.getUserUrl(props.username);
9 | const itemUrl = utils.getItemUrl(props.itemId);
10 |
11 | return (
12 |
13 | {props.score} point by
14 | {props.username}
15 | |
16 | {new Date(props.timestamp * 1000).toDateString()}
17 | |
18 | view on HackerNews
19 |
20 | );
21 | }
22 |
23 | Footer.propTypes = {
24 | username: PropTypes.string.isRequired,
25 | itemId: PropTypes.number.isRequired,
26 | score: PropTypes.number.isRequired,
27 | timestamp: PropTypes.number.isRequired
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/Story/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import * as utils from "../../utils/helper";
4 |
5 | import {
6 | HeaderWrapper,
7 | RankContainer,
8 | Title,
9 | SourceContainer,
10 | SourceLink
11 | } from "./Styles";
12 |
13 | export default function Header(props) {
14 | let url = props.url;
15 | let sourceUrl;
16 |
17 | if (!url) {
18 | url = utils.getItemUrl(props.itemId);
19 | }
20 |
21 | sourceUrl = utils.getSourceUrl(url);
22 |
23 | return (
24 |
25 |
26 | {props.rank}.
27 |
28 |
29 | {props.title}
30 |
31 |
32 | ({sourceUrl})
33 |
34 |
35 | );
36 | }
37 |
38 | Header.propTypes = {
39 | itemId: PropTypes.number.isRequired,
40 | rank: PropTypes.number.isRequired,
41 | title: PropTypes.string.isRequired,
42 | url: PropTypes.string
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/Story/Styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Wrapper = styled.div`
4 | background-color: #f6f6ef;
5 | padding: 8px;
6 | margin: 8px;
7 |
8 | box-shadow: 1px 1px 5px #888888;
9 | `;
10 |
11 | export const HeaderWrapper = styled.div`
12 | display: flex;
13 | flex-direction: row;
14 | font-size: 10pt;
15 | `;
16 |
17 | export const RankContainer = styled.div`
18 | margin: 0px 5px;
19 | color: #828282;
20 | `;
21 |
22 | export const Title = styled.a.attrs({
23 | target: "_blank",
24 | rel: "noopener"
25 | })`
26 | color: #000000;
27 | text-decoration: none;
28 | margin: 0px 2px;
29 |
30 | &:visited {
31 | color: #828282;
32 | }
33 | `;
34 |
35 | export const SourceContainer = styled.span`
36 | font-size: 10px;
37 | margin-top: 1px;
38 | color: #828282;
39 | `;
40 |
41 | export const SourceLink = styled.a.attrs({
42 | target: "_blank",
43 | rel: "noopener"
44 | })`
45 | color: #828282;
46 | text-decoration: none;
47 | margin: 0px 1px;
48 | `;
49 |
50 | export const FooterWrapper = styled.div`
51 | padding: 4px 0px 0px 24px;
52 |
53 | font-size: 7pt;
54 | color: #828282;
55 |
56 | margin: 0;
57 | `;
58 |
59 | export const FooterLink = styled.a.attrs({
60 | target: "_blank",
61 | rel: "noopener"
62 | })`
63 | color: #828282;
64 | text-decoration: none;
65 | margin: 0px 3px;
66 | `;
67 |
--------------------------------------------------------------------------------
/src/components/Story/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { Wrapper } from "./Styles";
5 | import Header from "./Header";
6 | import Footer from "./Footer";
7 |
8 | export default class Story extends Component {
9 | render() {
10 | const item = this.props.item;
11 |
12 | return (
13 |
14 |
20 |
26 |
27 | );
28 | }
29 | }
30 |
31 | Story.propTypes = {
32 | item: PropTypes.object.isRequired
33 | };
34 |
--------------------------------------------------------------------------------
/src/containers/StoryList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 | import Spinner from "react-spinkit";
5 |
6 | import Story from "../components/Story";
7 |
8 | const Wrapper = styled.div`margin: 20px;`;
9 | const LoadingWrapper = styled.div`
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | padding: 50px;
14 | `;
15 |
16 | export default class StoryList extends Component {
17 | render() {
18 | const items = this.props.items;
19 |
20 | if (items.length > 0) {
21 | return this.renderList(items);
22 | } else
23 | return (
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | renderList(items) {
31 | return (
32 |
33 | {items.map((item, i) => )}
34 |
35 | );
36 | }
37 | }
38 |
39 | StoryList.propTypes = {
40 | items: PropTypes.array.isRequired
41 | };
42 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/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 registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
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);
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/facebookincubator/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 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/utils/helper.js:
--------------------------------------------------------------------------------
1 | export const baseSiteUrl = "https://news.ycombinator.com";
2 |
3 | export function getUserUrl(username) {
4 | if (!username) return "#";
5 |
6 | return `${baseSiteUrl}/user?id=${username}`;
7 | }
8 |
9 | export function getItemUrl(itemId) {
10 | if (!itemId) return "#";
11 |
12 | return `${baseSiteUrl}/item?id=${itemId}`;
13 | }
14 |
15 | export function getSourceUrl(url) {
16 | const regex = /^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n]+)/;
17 | let match = regex.exec(url);
18 |
19 | if (!match) return "";
20 |
21 | let sourceUrl = match[1] ? match[1] : "#";
22 |
23 | return sourceUrl;
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/media.js:
--------------------------------------------------------------------------------
1 | import { css } from "styled-components";
2 |
3 | export const phone = inner => css`
4 | @media only screen and (min-width : ${480 / 16}em) {
5 | ${inner}
6 | }
7 | `;
8 |
9 | export const tablet = inner => css`
10 | @media only screen and (min-width : ${768 / 16}em) {
11 | ${inner}
12 | }
13 | `;
14 |
15 | export const desktop = inner => css`
16 | @media only screen and (min-width : ${992 / 16}em) {
17 | ${inner}
18 | }
19 | `;
20 |
21 | export const maxPhone = inner => css`
22 | @media only screen and (max-width : ${768 / 16}em) {
23 | ${inner}
24 | }
25 | `;
26 |
27 | export const maxTablet = inner => css`
28 | @media only screen and (max-width : ${992 / 16}em) {
29 | ${inner}
30 | }
31 | `;
32 |
--------------------------------------------------------------------------------