", "notifications"]
42 | }
43 |
--------------------------------------------------------------------------------
/src/options/components/App/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | max-width: calc(var(--lh) * 22);
6 | margin: 0 auto;
7 | padding: var(--lh);
8 | min-height: 100vh;
9 | }
10 |
11 | .app__header {
12 | animation-name: fadeIn;
13 | animation-duration: var(--animation-duration);
14 | animation-fill-mode: backwards;
15 | }
16 |
17 | .app__main > * {
18 | animation-name: fadeIn;
19 | animation-duration: var(--animation-duration);
20 | animation-fill-mode: backwards;
21 | }
22 |
23 | .app__footer {
24 | margin-top: auto;
25 | animation-name: fadeIn;
26 | animation-duration: var(--animation-duration);
27 | animation-fill-mode: backwards;
28 | }
29 |
--------------------------------------------------------------------------------
/src/options/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { userFetchFromChromeStorageAction } from "redux-options/actions/user";
6 | import {
7 | checkConnection,
8 | wentOffline,
9 | wentOnline
10 | } from "redux-options/actions/online";
11 | import Footer from "./components/Footer";
12 | import Online from "./components/Online";
13 | import Offline from "./components/Offline";
14 | import Logo from "./components/Logo";
15 | import "./App.css";
16 |
17 | export class App extends Component {
18 | componentDidMount() {
19 | window.addEventListener("online", this.handleOnlineEvent);
20 | window.addEventListener("offline", this.handleOfflineEvent);
21 |
22 | this.props.checkConnection(navigator.onLine);
23 | this.props.userFetchFromChromeStorage();
24 | }
25 |
26 | componentWillUnmount() {
27 | window.removeEventListener("online", this.handleOnlineEvent);
28 | window.removeEventListener("offline", this.handleOfflineEvent);
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
37 |
38 |
39 | {this.props.online ? : }
40 |
41 |
42 |
45 |
46 | );
47 | }
48 |
49 | handleOnlineEvent = () => {
50 | this.props.wentOnline();
51 | };
52 |
53 | handleOfflineEvent = () => {
54 | this.props.wentOffline();
55 | };
56 | }
57 |
58 | App.propTypes = {
59 | online: PropTypes.bool.isRequired,
60 | userFetchFromChromeStorage: PropTypes.func.isRequired,
61 | checkConnection: PropTypes.func.isRequired,
62 | wentOnline: PropTypes.func.isRequired,
63 | wentOffline: PropTypes.func.isRequired
64 | };
65 |
66 | const mapStateToProps = state => ({
67 | online: state.online
68 | });
69 |
70 | const mapDispatchToProps = dispatch => {
71 | return {
72 | userFetchFromChromeStorage: userInfo =>
73 | dispatch(userFetchFromChromeStorageAction(userInfo)),
74 | checkConnection: online => dispatch(checkConnection(online)),
75 | wentOffline: () => dispatch(wentOffline()),
76 | wentOnline: () => dispatch(wentOnline())
77 | };
78 | };
79 |
80 | export default connect(
81 | mapStateToProps,
82 | mapDispatchToProps
83 | )(App);
84 |
--------------------------------------------------------------------------------
/src/options/components/App/__tests__/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { App } from "../App";
5 | import Online from "../components/Online";
6 | import Offline from "../components/Offline";
7 |
8 | describe("", () => {
9 | const defaultProps = {
10 | online: true,
11 | userFetchFromChromeStorage: jest.fn(),
12 | checkConnection: jest.fn(),
13 | wentOnline: jest.fn(),
14 | wentOffline: jest.fn()
15 | };
16 |
17 | it("should render correctly", () => {
18 | const tree = shallow();
19 | expect(tree).toMatchSnapshot();
20 | });
21 |
22 | it("should render Online view when online", () => {
23 | const tree = shallow();
24 | expect(tree.contains()).toBeTruthy();
25 | });
26 |
27 | it("should render Offline view when offline", () => {
28 | const tree = shallow();
29 | expect(tree.contains()).toBeTruthy();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/options/components/App/__tests__/__snapshots__/App.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
39 | Symbol(enzyme.__renderer__): Object {
40 | "batchedUpdates": [Function],
41 | "getNode": [Function],
42 | "render": [Function],
43 | "simulateError": [Function],
44 | "simulateEvent": [Function],
45 | "unmount": [Function],
46 | },
47 | Symbol(enzyme.__node__): Object {
48 | "instance": null,
49 | "key": undefined,
50 | "nodeType": "host",
51 | "props": Object {
52 | "children": Array [
53 | ,
58 |
61 |
62 | ,
63 | ,
68 | ],
69 | "className": "app",
70 | },
71 | "ref": null,
72 | "rendered": Array [
73 | Object {
74 | "instance": null,
75 | "key": undefined,
76 | "nodeType": "host",
77 | "props": Object {
78 | "children": ,
79 | "className": "app__header",
80 | },
81 | "ref": null,
82 | "rendered": Object {
83 | "instance": null,
84 | "key": undefined,
85 | "nodeType": "class",
86 | "props": Object {},
87 | "ref": null,
88 | "rendered": null,
89 | "type": [Function],
90 | },
91 | "type": "header",
92 | },
93 | Object {
94 | "instance": null,
95 | "key": undefined,
96 | "nodeType": "host",
97 | "props": Object {
98 | "children": ,
99 | "className": "app__main",
100 | },
101 | "ref": null,
102 | "rendered": Object {
103 | "instance": null,
104 | "key": undefined,
105 | "nodeType": "class",
106 | "props": Object {},
107 | "ref": null,
108 | "rendered": null,
109 | "type": [Function],
110 | },
111 | "type": "main",
112 | },
113 | Object {
114 | "instance": null,
115 | "key": undefined,
116 | "nodeType": "host",
117 | "props": Object {
118 | "children": ,
119 | "className": "app__footer",
120 | },
121 | "ref": null,
122 | "rendered": Object {
123 | "instance": null,
124 | "key": undefined,
125 | "nodeType": "function",
126 | "props": Object {},
127 | "ref": null,
128 | "rendered": null,
129 | "type": [Function],
130 | },
131 | "type": "footer",
132 | },
133 | ],
134 | "type": "div",
135 | },
136 | Symbol(enzyme.__nodes__): Array [
137 | Object {
138 | "instance": null,
139 | "key": undefined,
140 | "nodeType": "host",
141 | "props": Object {
142 | "children": Array [
143 | ,
148 |
151 |
152 | ,
153 | ,
158 | ],
159 | "className": "app",
160 | },
161 | "ref": null,
162 | "rendered": Array [
163 | Object {
164 | "instance": null,
165 | "key": undefined,
166 | "nodeType": "host",
167 | "props": Object {
168 | "children": ,
169 | "className": "app__header",
170 | },
171 | "ref": null,
172 | "rendered": Object {
173 | "instance": null,
174 | "key": undefined,
175 | "nodeType": "class",
176 | "props": Object {},
177 | "ref": null,
178 | "rendered": null,
179 | "type": [Function],
180 | },
181 | "type": "header",
182 | },
183 | Object {
184 | "instance": null,
185 | "key": undefined,
186 | "nodeType": "host",
187 | "props": Object {
188 | "children": ,
189 | "className": "app__main",
190 | },
191 | "ref": null,
192 | "rendered": Object {
193 | "instance": null,
194 | "key": undefined,
195 | "nodeType": "class",
196 | "props": Object {},
197 | "ref": null,
198 | "rendered": null,
199 | "type": [Function],
200 | },
201 | "type": "main",
202 | },
203 | Object {
204 | "instance": null,
205 | "key": undefined,
206 | "nodeType": "host",
207 | "props": Object {
208 | "children": ,
209 | "className": "app__footer",
210 | },
211 | "ref": null,
212 | "rendered": Object {
213 | "instance": null,
214 | "key": undefined,
215 | "nodeType": "function",
216 | "props": Object {},
217 | "ref": null,
218 | "rendered": null,
219 | "type": [Function],
220 | },
221 | "type": "footer",
222 | },
223 | ],
224 | "type": "div",
225 | },
226 | ],
227 | Symbol(enzyme.__options__): Object {
228 | "adapter": ReactSixteenAdapter {
229 | "options": Object {
230 | "enableComponentDidUpdateOnSetState": true,
231 | "lifecycles": Object {
232 | "componentDidUpdate": Object {
233 | "onSetState": true,
234 | },
235 | "getDerivedStateFromProps": true,
236 | "getSnapshotBeforeUpdate": true,
237 | "setState": Object {
238 | "skipsComponentDidUpdateOnNullish": true,
239 | },
240 | },
241 | },
242 | },
243 | },
244 | }
245 | `;
246 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Footer/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | font-size: .75rem;
3 | }
4 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Footer.css";
5 |
6 | const Footer = () => (
7 |
13 | );
14 |
15 | export default Footer;
16 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Footer/__tests__/Footer.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Footer from "../Footer";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Footer/__tests__/__snapshots__/Footer.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "host",
19 | "props": Object {
20 | "className": "footer",
21 | "dangerouslySetInnerHTML": Object {
22 | "__html": "optionsFooter",
23 | },
24 | },
25 | "ref": null,
26 | "rendered": null,
27 | "type": "p",
28 | },
29 | Symbol(enzyme.__nodes__): Array [
30 | Object {
31 | "instance": null,
32 | "key": undefined,
33 | "nodeType": "host",
34 | "props": Object {
35 | "className": "footer",
36 | "dangerouslySetInnerHTML": Object {
37 | "__html": "optionsFooter",
38 | },
39 | },
40 | "ref": null,
41 | "rendered": null,
42 | "type": "p",
43 | },
44 | ],
45 | Symbol(enzyme.__options__): Object {
46 | "adapter": ReactSixteenAdapter {
47 | "options": Object {
48 | "enableComponentDidUpdateOnSetState": true,
49 | "lifecycles": Object {
50 | "componentDidUpdate": Object {
51 | "onSetState": true,
52 | },
53 | "getDerivedStateFromProps": true,
54 | "getSnapshotBeforeUpdate": true,
55 | "setState": Object {
56 | "skipsComponentDidUpdateOnNullish": true,
57 | },
58 | },
59 | },
60 | },
61 | },
62 | }
63 | `;
64 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Footer";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Logo/Logo.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | display: flex;
3 | }
4 |
5 | .logo__title {
6 | margin-right: calc(var(--lh) / 4);
7 | }
8 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Logo/Logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { Title, Link } from "theme";
6 | import "./Logo.css";
7 |
8 | export const Logo = props => {
9 | const { username, online } = props;
10 | const shouldDisplayUsername = username && online;
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | {shouldDisplayUsername && (
18 | <>
19 | (
20 |
26 | )
27 | >
28 | )}
29 |
30 | );
31 | };
32 |
33 | Logo.propTypes = {
34 | username: PropTypes.string.isRequired,
35 | online: PropTypes.bool.isRequired
36 | };
37 |
38 | const mapStateToProps = state => ({
39 | username: state.user.username,
40 | online: state.online
41 | });
42 |
43 | export default connect(mapStateToProps)(Logo);
44 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Logo/__tests__/Logo.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Logo } from "../Logo";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | username: "pibuddy",
9 | online: true
10 | };
11 |
12 | it("should render correctly", () => {
13 | const tree = shallow();
14 | expect(tree).toMatchSnapshot();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Logo/__tests__/__snapshots__/Logo.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
10 | Symbol(enzyme.__renderer__): Object {
11 | "batchedUpdates": [Function],
12 | "getNode": [Function],
13 | "render": [Function],
14 | "simulateError": [Function],
15 | "simulateEvent": [Function],
16 | "unmount": [Function],
17 | },
18 | Symbol(enzyme.__node__): Object {
19 | "instance": null,
20 | "key": undefined,
21 | "nodeType": "host",
22 | "props": Object {
23 | "children": Array [
24 |
27 |
30 | ,
31 |
32 | (
33 |
39 | )
40 | ,
41 | ],
42 | "className": "logo",
43 | },
44 | "ref": null,
45 | "rendered": Array [
46 | Object {
47 | "instance": null,
48 | "key": undefined,
49 | "nodeType": "host",
50 | "props": Object {
51 | "children": ,
54 | "className": "logo__title",
55 | },
56 | "ref": null,
57 | "rendered": Object {
58 | "instance": null,
59 | "key": undefined,
60 | "nodeType": "function",
61 | "props": Object {
62 | "t": "extensionName",
63 | },
64 | "ref": null,
65 | "rendered": null,
66 | "type": [Function],
67 | },
68 | "type": "div",
69 | },
70 | Object {
71 | "instance": null,
72 | "key": undefined,
73 | "nodeType": "function",
74 | "props": Object {
75 | "children": Array [
76 | "(",
77 | ,
83 | ")",
84 | ],
85 | },
86 | "ref": null,
87 | "rendered": Array [
88 | "(",
89 | Object {
90 | "instance": null,
91 | "key": undefined,
92 | "nodeType": "function",
93 | "props": Object {
94 | "blank": true,
95 | "t": "pibuddy",
96 | "title": "popupOpenYourPinboardProfile",
97 | "url": "https://pinboard.in/u:pibuddy",
98 | },
99 | "ref": null,
100 | "rendered": null,
101 | "type": [Function],
102 | },
103 | ")",
104 | ],
105 | "type": Symbol(react.fragment),
106 | },
107 | ],
108 | "type": "div",
109 | },
110 | Symbol(enzyme.__nodes__): Array [
111 | Object {
112 | "instance": null,
113 | "key": undefined,
114 | "nodeType": "host",
115 | "props": Object {
116 | "children": Array [
117 |
120 |
123 | ,
124 |
125 | (
126 |
132 | )
133 | ,
134 | ],
135 | "className": "logo",
136 | },
137 | "ref": null,
138 | "rendered": Array [
139 | Object {
140 | "instance": null,
141 | "key": undefined,
142 | "nodeType": "host",
143 | "props": Object {
144 | "children": ,
147 | "className": "logo__title",
148 | },
149 | "ref": null,
150 | "rendered": Object {
151 | "instance": null,
152 | "key": undefined,
153 | "nodeType": "function",
154 | "props": Object {
155 | "t": "extensionName",
156 | },
157 | "ref": null,
158 | "rendered": null,
159 | "type": [Function],
160 | },
161 | "type": "div",
162 | },
163 | Object {
164 | "instance": null,
165 | "key": undefined,
166 | "nodeType": "function",
167 | "props": Object {
168 | "children": Array [
169 | "(",
170 | ,
176 | ")",
177 | ],
178 | },
179 | "ref": null,
180 | "rendered": Array [
181 | "(",
182 | Object {
183 | "instance": null,
184 | "key": undefined,
185 | "nodeType": "function",
186 | "props": Object {
187 | "blank": true,
188 | "t": "pibuddy",
189 | "title": "popupOpenYourPinboardProfile",
190 | "url": "https://pinboard.in/u:pibuddy",
191 | },
192 | "ref": null,
193 | "rendered": null,
194 | "type": [Function],
195 | },
196 | ")",
197 | ],
198 | "type": Symbol(react.fragment),
199 | },
200 | ],
201 | "type": "div",
202 | },
203 | ],
204 | Symbol(enzyme.__options__): Object {
205 | "adapter": ReactSixteenAdapter {
206 | "options": Object {
207 | "enableComponentDidUpdateOnSetState": true,
208 | "lifecycles": Object {
209 | "componentDidUpdate": Object {
210 | "onSetState": true,
211 | },
212 | "getDerivedStateFromProps": true,
213 | "getSnapshotBeforeUpdate": true,
214 | "setState": Object {
215 | "skipsComponentDidUpdateOnNullish": true,
216 | },
217 | },
218 | },
219 | },
220 | },
221 | }
222 | `;
223 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Logo/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Logo";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Offline/Offline.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Paragraph, Quote } from "theme";
3 |
4 | const Offline = () => (
5 | <>
6 |
10 |
11 |
12 | >
13 | );
14 |
15 | export default Offline;
16 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Offline/__tests__/Offline.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Offline from "../Offline";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Offline/__tests__/__snapshots__/Offline.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "function",
19 | "props": Object {
20 | "children": Array [
21 | ,
25 |
,
28 | ,
32 | ],
33 | },
34 | "ref": null,
35 | "rendered": Array [
36 | Object {
37 | "instance": null,
38 | "key": undefined,
39 | "nodeType": "function",
40 | "props": Object {
41 | "innerHTML": true,
42 | "t": "optionsOfflineMessageOne",
43 | },
44 | "ref": null,
45 | "rendered": null,
46 | "type": [Function],
47 | },
48 | Object {
49 | "instance": null,
50 | "key": undefined,
51 | "nodeType": "function",
52 | "props": Object {
53 | "t": "optionsOfflineMessageQuote",
54 | },
55 | "ref": null,
56 | "rendered": null,
57 | "type": [Function],
58 | },
59 | Object {
60 | "instance": null,
61 | "key": undefined,
62 | "nodeType": "function",
63 | "props": Object {
64 | "innerHTML": false,
65 | "t": "optionsOfflineMessageTwo",
66 | },
67 | "ref": null,
68 | "rendered": null,
69 | "type": [Function],
70 | },
71 | ],
72 | "type": Symbol(react.fragment),
73 | },
74 | Symbol(enzyme.__nodes__): Array [
75 | Object {
76 | "instance": null,
77 | "key": undefined,
78 | "nodeType": "function",
79 | "props": Object {
80 | "children": Array [
81 | ,
85 |
,
88 | ,
92 | ],
93 | },
94 | "ref": null,
95 | "rendered": Array [
96 | Object {
97 | "instance": null,
98 | "key": undefined,
99 | "nodeType": "function",
100 | "props": Object {
101 | "innerHTML": true,
102 | "t": "optionsOfflineMessageOne",
103 | },
104 | "ref": null,
105 | "rendered": null,
106 | "type": [Function],
107 | },
108 | Object {
109 | "instance": null,
110 | "key": undefined,
111 | "nodeType": "function",
112 | "props": Object {
113 | "t": "optionsOfflineMessageQuote",
114 | },
115 | "ref": null,
116 | "rendered": null,
117 | "type": [Function],
118 | },
119 | Object {
120 | "instance": null,
121 | "key": undefined,
122 | "nodeType": "function",
123 | "props": Object {
124 | "innerHTML": false,
125 | "t": "optionsOfflineMessageTwo",
126 | },
127 | "ref": null,
128 | "rendered": null,
129 | "type": [Function],
130 | },
131 | ],
132 | "type": Symbol(react.fragment),
133 | },
134 | ],
135 | Symbol(enzyme.__options__): Object {
136 | "adapter": ReactSixteenAdapter {
137 | "options": Object {
138 | "enableComponentDidUpdateOnSetState": true,
139 | "lifecycles": Object {
140 | "componentDidUpdate": Object {
141 | "onSetState": true,
142 | },
143 | "getDerivedStateFromProps": true,
144 | "getSnapshotBeforeUpdate": true,
145 | "setState": Object {
146 | "skipsComponentDidUpdateOnNullish": true,
147 | },
148 | },
149 | },
150 | },
151 | },
152 | }
153 | `;
154 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Offline/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Offline";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/Online.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import Login from "./components/Login";
6 | import Options from "./components/Options";
7 |
8 | export const Online = ({ username }) => {
9 | return username ? : ;
10 | };
11 |
12 | Online.propTypes = {
13 | username: PropTypes.string.isRequired
14 | };
15 |
16 | const mapStateToProps = state => ({
17 | username: state.user.username
18 | });
19 |
20 | export default connect(mapStateToProps)(Online);
21 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/__tests__/Online.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Online } from "../Online";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | username: "pibuddy"
9 | };
10 |
11 | it("should render correctly", () => {
12 | const tree = shallow();
13 | expect(tree).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/__tests__/__snapshots__/Online.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
9 | Symbol(enzyme.__renderer__): Object {
10 | "batchedUpdates": [Function],
11 | "getNode": [Function],
12 | "render": [Function],
13 | "simulateError": [Function],
14 | "simulateEvent": [Function],
15 | "unmount": [Function],
16 | },
17 | Symbol(enzyme.__node__): Object {
18 | "instance": null,
19 | "key": undefined,
20 | "nodeType": "class",
21 | "props": Object {},
22 | "ref": null,
23 | "rendered": null,
24 | "type": [Function],
25 | },
26 | Symbol(enzyme.__nodes__): Array [
27 | Object {
28 | "instance": null,
29 | "key": undefined,
30 | "nodeType": "class",
31 | "props": Object {},
32 | "ref": null,
33 | "rendered": null,
34 | "type": [Function],
35 | },
36 | ],
37 | Symbol(enzyme.__options__): Object {
38 | "adapter": ReactSixteenAdapter {
39 | "options": Object {
40 | "enableComponentDidUpdateOnSetState": true,
41 | "lifecycles": Object {
42 | "componentDidUpdate": Object {
43 | "onSetState": true,
44 | },
45 | "getDerivedStateFromProps": true,
46 | "getSnapshotBeforeUpdate": true,
47 | "setState": Object {
48 | "skipsComponentDidUpdateOnNullish": true,
49 | },
50 | },
51 | },
52 | },
53 | },
54 | }
55 | `;
56 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { userLogInAction } from "redux-options/actions/user";
6 | import { Paragraph, Error, Input, Button } from "theme";
7 | import Loading from "./components/Loading";
8 |
9 | export class Login extends Component {
10 | state = {
11 | token: ""
12 | };
13 |
14 | static validateInput(input) {
15 | const regex = /^([^:]{1,})(:)(\S{1,})/;
16 | return input.match(regex);
17 | }
18 |
19 | render() {
20 | return (
21 |
54 | );
55 | }
56 |
57 | onFormSubmit = e => {
58 | e.preventDefault();
59 | this.props.userLogIn(this.state.token);
60 | };
61 |
62 | onChangeUserInput = e => {
63 | const { value } = e.target;
64 | this.setState(() => {
65 | return {
66 | token: value
67 | };
68 | });
69 | };
70 | }
71 |
72 | Login.propTypes = {
73 | loading: PropTypes.bool.isRequired,
74 | error: PropTypes.bool.isRequired,
75 | userLogIn: PropTypes.func.isRequired
76 | };
77 |
78 | const mapStateToProps = state => ({
79 | loading: state.user.loading,
80 | error: state.user.error
81 | });
82 |
83 | const mapDispatchToProps = dispatch => ({
84 | userLogIn: token => dispatch(userLogInAction(token))
85 | });
86 |
87 | export default connect(
88 | mapStateToProps,
89 | mapDispatchToProps
90 | )(Login);
91 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Login/__tests__/Login.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Login } from "../Login";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | loading: false,
9 | error: false,
10 | userLogIn: jest.fn()
11 | };
12 |
13 | it("should render correctly", () => {
14 | const tree = shallow();
15 | expect(tree).toMatchSnapshot();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Login/components/Loading/Loading.css:
--------------------------------------------------------------------------------
1 | @keyframes loading {
2 | to {
3 | transform: rotate(360deg);
4 | }
5 | }
6 |
7 | .loading {
8 | height: calc(var(--lh) * 6);
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 |
14 | .loading__icon {
15 | border-radius: 50%;
16 | width: calc(var(--lh) - 2px);
17 | height: calc(var(--lh) - 2px);
18 | border: 2px solid var(--color-gray-light);
19 | border-top-color: var(--color-primary);
20 | animation-name: loading;
21 | animation-duration: calc(var(--animation-duration) * 4);
22 | animation-iteration-count: infinite;
23 | animation-timing-function: cubic-bezier(.3, .1, .3, .85);
24 | }
25 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Login/components/Loading/Loading.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Paragraph } from "theme";
3 | import "./Loading.css";
4 |
5 | const Loading = props => (
6 |
9 | );
10 |
11 | export default Loading;
12 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Login/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Loading";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Login/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Login";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Options/Options.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 |
5 | import { userLogOutAction } from "redux-options/actions/user";
6 | import {
7 | optionsUpdateAction,
8 | fetchOptionsAction
9 | } from "redux-options/actions/options";
10 | import { Paragraph, Button, Checkbox, Select } from "theme";
11 |
12 | const defaultViewOptions = [
13 | {
14 | value: "all",
15 | text: chrome.i18n.getMessage("optionsDefaultViewAllText")
16 | },
17 | {
18 | value: "add",
19 | text: chrome.i18n.getMessage("optionsDefaultViewAddText")
20 | }
21 | ];
22 |
23 | export class Options extends Component {
24 | componentDidMount() {
25 | this.props.fetchOptions();
26 | }
27 |
28 | render() {
29 | const { username } = this.props;
30 | const {
31 | defaultView,
32 | privateCheckboxByDefault,
33 | toReadChecboxByDefault,
34 | enableSystemNotifications
35 | } = this.props.options;
36 | return (
37 | <>
38 |
42 |
46 |
47 |
48 |
49 |
56 |
57 |
63 |
64 |
70 |
71 |
77 |
78 |
79 |
80 |
81 | -
82 |
83 | {navigator.platform === "MacIntel" ? "⌥ + p" : "Alt + p"}
84 |
{" "}
85 | - {chrome.i18n.getMessage("optionsShortcutDescriptionToggle")}
86 |
87 | -
88 |
89 | {navigator.platform === "MacIntel" ? "⌥ + 1" : "Alt + 1"}
90 |
{" "}
91 | - {chrome.i18n.getMessage("optionsShortcutDescriptionGoToAll")}
92 |
93 | -
94 |
95 | {navigator.platform === "MacIntel" ? "⌥ + 2" : "Alt + 2"}
96 |
{" "}
97 | - {chrome.i18n.getMessage("optionsShortcutDescriptionGoToAdd")}
98 |
99 | -
100 |
101 | {navigator.platform === "MacIntel" ? "⌘ + Enter" : "Ctrl + Enter"}
102 |
{" "}
103 | - {chrome.i18n.getMessage("optionsShortcutDescriptionAddBookmark")}
104 |
105 | -
106 |
107 | {navigator.platform === "MacIntel" ? "⌘ + Enter" : "Ctrl + Enter"}
108 |
{" "}
109 | -{" "}
110 | {chrome.i18n.getMessage(
111 | "optionsShortcutDescriptionOpenInBackground"
112 | )}
113 |
114 | -
115 |
116 | {navigator.platform === "MacIntel"
117 | ? "⌘ + Backspace"
118 | : "Ctrl + Backspace"}
119 |
{" "}
120 | -{" "}
121 | {chrome.i18n.getMessage("optionsShortcutDescriptionDeleteBookmark")}
122 |
123 |
124 | >
125 | );
126 | }
127 |
128 | onCickLogOutButton = () => {
129 | this.props.userLogOut();
130 | };
131 |
132 | handleSelectChange = e => {
133 | const { id, selectedIndex } = e.target;
134 | this.props.optionsUpdate({
135 | [id]: defaultViewOptions[selectedIndex].value
136 | });
137 | };
138 |
139 | handleCheckboxChange = e => {
140 | const { id, checked } = e.target;
141 | this.props.optionsUpdate({
142 | [id]: checked
143 | });
144 | };
145 | }
146 |
147 | Options.propTypes = {
148 | username: PropTypes.string.isRequired,
149 | options: PropTypes.shape({
150 | defaultView: PropTypes.string.isRequired,
151 | privateCheckboxByDefault: PropTypes.bool.isRequired,
152 | toReadChecboxByDefault: PropTypes.bool.isRequired,
153 | enableSystemNotifications: PropTypes.bool.isRequired
154 | }).isRequired,
155 | userLogOut: PropTypes.func.isRequired,
156 | fetchOptions: PropTypes.func.isRequired,
157 | optionsUpdate: PropTypes.func.isRequired
158 | };
159 |
160 | const mapStateToProps = (state, ownProps) => {
161 | return {
162 | username: state.user.username,
163 | options: state.options
164 | };
165 | };
166 |
167 | const mapDispatchToProps = dispatch => {
168 | return {
169 | userLogOut: () => dispatch(userLogOutAction()),
170 | fetchOptions: () => dispatch(fetchOptionsAction()),
171 | optionsUpdate: option => dispatch(optionsUpdateAction(option))
172 | };
173 | };
174 |
175 | export default connect(
176 | mapStateToProps,
177 | mapDispatchToProps
178 | )(Options);
179 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Options/__tests__/Options.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Options } from "../Options";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | username: "pinbuddy",
9 | options: {
10 | defaultView: "all",
11 | privateCheckboxByDefault: false,
12 | toReadChecboxByDefault: false,
13 | enableSystemNotifications: false
14 | },
15 | userLogOut: jest.fn(),
16 | fetchOptions: jest.fn(),
17 | optionsUpdate: jest.fn()
18 | };
19 |
20 | it("should render correctly", () => {
21 | const tree = shallow();
22 | expect(tree).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/components/Options/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Options";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/components/Online/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Online";
2 |
--------------------------------------------------------------------------------
/src/options/components/App/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./App";
2 |
--------------------------------------------------------------------------------
/src/options/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | PinBuddy — Options
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/options/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 | import { createStore, applyMiddleware } from "redux";
4 | import { Provider } from "react-redux";
5 | import thunk from "redux-thunk";
6 |
7 | import App from "./components/App";
8 | import reducers from "redux-options/reducers";
9 | import "theme/index.css";
10 |
11 | const store = createStore(reducers, applyMiddleware(thunk));
12 |
13 | render(
14 |
15 |
16 | ,
17 | document.getElementById("root")
18 | );
19 |
--------------------------------------------------------------------------------
/src/options/redux/actions/online.js:
--------------------------------------------------------------------------------
1 | export const checkConnection = online => {
2 | return {
3 | type: "CHECK_ONLINE",
4 | online
5 | };
6 | };
7 |
8 | export const wentOnline = () => {
9 | return {
10 | type: "WENT_ONLINE"
11 | };
12 | };
13 |
14 | export const wentOffline = () => {
15 | return {
16 | type: "WENT_OFFLINE"
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/options/redux/actions/options.js:
--------------------------------------------------------------------------------
1 | export const optionsUpdateAction = option => {
2 | return dispatch => {
3 | chrome.storage.sync.set(option, () => {
4 | dispatch({
5 | type: "UPDATE_OPTION",
6 | option
7 | });
8 | });
9 | };
10 | };
11 |
12 | export const fetchOptionsAction = () => {
13 | return dispatch => {
14 | chrome.storage.sync.get(
15 | [
16 | "defaultView",
17 | "privateCheckboxByDefault",
18 | "toReadChecboxByDefault",
19 | "enableSystemNotifications"
20 | ],
21 | option => {
22 | const settingsExist =
23 | Object.prototype.hasOwnProperty.call(option, "defaultView") &&
24 | Object.prototype.hasOwnProperty.call(
25 | option,
26 | "privateCheckboxByDefault"
27 | ) &&
28 | Object.prototype.hasOwnProperty.call(
29 | option,
30 | "toReadChecboxByDefault"
31 | ) &&
32 | Object.prototype.hasOwnProperty.call(
33 | option,
34 | "enableSystemNotifications"
35 | );
36 |
37 | if (settingsExist) {
38 | dispatch({
39 | type: "UPDATE_OPTION",
40 | option
41 | });
42 | } else {
43 | chrome.storage.sync.set({
44 | defaultView: "all",
45 | privateCheckboxByDefault: false,
46 | toReadChecboxByDefault: false,
47 | enableSystemNotifications: false
48 | });
49 | }
50 | }
51 | );
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/src/options/redux/actions/user.js:
--------------------------------------------------------------------------------
1 | const API = "https://api.pinboard.in/v1/";
2 |
3 | export const userLoadingShowAction = () => ({
4 | type: "LOADING_SHOW"
5 | });
6 |
7 | export const userLoadingHideAction = () => ({
8 | type: "LOADING_HIDE"
9 | });
10 |
11 | export const userErrorShowAction = () => ({
12 | type: "ERROR_SHOW"
13 | });
14 |
15 | export const userErrorHideAction = () => ({
16 | type: "ERROR_HIDE"
17 | });
18 |
19 | export const userLogInAction = userToken => {
20 | return dispatch => {
21 | dispatch(userLoadingShowAction());
22 | dispatch(userErrorHideAction());
23 |
24 | fetch(`${API}user/api_token?format=json&auth_token=${userToken}`)
25 | .then(data => data.json())
26 | .then(dataJSON => {
27 | if (dataJSON.result) {
28 | const [username, token] = userToken.split(":");
29 | chrome.storage.local.set(
30 | {
31 | username,
32 | token
33 | },
34 | () => {
35 | dispatch(userLoadingHideAction());
36 | dispatch({
37 | type: "ADD_USERNAME",
38 | username
39 | });
40 | }
41 | );
42 |
43 | const now = Date.now();
44 |
45 | fetch(`${API}posts/all?format=json&auth_token=${userToken}`)
46 | .then(dataPosts => dataPosts.json())
47 | .then(posts => {
48 | chrome.storage.local.set({
49 | posts,
50 | postsFetched: now
51 | });
52 | });
53 | } else {
54 | dispatch(userLoadingHideAction());
55 | dispatch(userErrorShowAction());
56 | }
57 | })
58 | .catch(() => {
59 | dispatch(userLoadingHideAction());
60 | dispatch(userErrorShowAction());
61 | });
62 | };
63 | };
64 |
65 | export const userFetchFromChromeStorageAction = () => {
66 | return dispatch => {
67 | chrome.storage.local.get(["username"], username => {
68 | if (username.username) {
69 | dispatch({
70 | type: "GET_USERNAME",
71 | username
72 | });
73 | }
74 | });
75 | };
76 | };
77 |
78 | export const userLogOutAction = () => {
79 | return dispatch => {
80 | chrome.storage.local.clear(() => dispatch({ type: "LOG_OUT" }));
81 | };
82 | };
83 |
--------------------------------------------------------------------------------
/src/options/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import user from "./user";
3 | import online from "./online";
4 | import options from "./options";
5 |
6 | export default combineReducers({
7 | user,
8 | online,
9 | options
10 | });
11 |
--------------------------------------------------------------------------------
/src/options/redux/reducers/online.js:
--------------------------------------------------------------------------------
1 | const reducerOffline = (state = true, action) => {
2 | if (action.type === "CHECK_ONLINE") {
3 | return action.online;
4 | } else if (action.type === "WENT_ONLINE") {
5 | return true;
6 | } else if (action.type === "WENT_OFFLINE") {
7 | return false;
8 | }
9 |
10 | return state;
11 | };
12 |
13 | export default reducerOffline;
14 |
--------------------------------------------------------------------------------
/src/options/redux/reducers/options.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | defaultView: "all",
3 | privateCheckboxByDefault: false,
4 | toReadChecboxByDefault: false,
5 | enableSystemNotifications: false
6 | };
7 |
8 | const reducerOptions = (state = initialState, action) => {
9 | if (action.type === "UPDATE_OPTION") {
10 | return {
11 | ...state,
12 | ...action.option
13 | };
14 | }
15 |
16 | return state;
17 | };
18 |
19 | export default reducerOptions;
20 |
--------------------------------------------------------------------------------
/src/options/redux/reducers/user.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | username: "",
3 | loading: false,
4 | error: false
5 | };
6 |
7 | const reducerAuth = (state = initialState, action) => {
8 | if (action.type === "ADD_USERNAME") {
9 | return {
10 | ...state,
11 | username: action.username
12 | };
13 | } else if (action.type === "GET_USERNAME") {
14 | return {
15 | ...state,
16 | ...action.username
17 | };
18 | } else if (action.type === "LOADING_SHOW") {
19 | return {
20 | ...state,
21 | loading: true
22 | };
23 | } else if (action.type === "LOADING_HIDE") {
24 | return {
25 | ...state,
26 | loading: false
27 | };
28 | } else if (action.type === "ERROR_SHOW") {
29 | return {
30 | ...state,
31 | error: true
32 | };
33 | } else if (action.type === "ERROR_HIDE") {
34 | return {
35 | ...state,
36 | error: false
37 | };
38 | } else if (action.type === "LOG_OUT") {
39 | return initialState;
40 | }
41 |
42 | return state;
43 | };
44 |
45 | export default reducerAuth;
46 |
--------------------------------------------------------------------------------
/src/popup/components/App/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | width: calc(var(--lh) * 22);
3 | height: calc(var(--lh) * 20);
4 | overflow: auto;
5 | padding: var(--lh);
6 | }
7 |
8 | .app__header {
9 | display: flex;
10 | justify-content: space-between;
11 | }
12 |
13 | .app__logo {
14 | display: flex;
15 | }
16 |
17 | .app__main > * {
18 | animation-name: fadeIn;
19 | animation-duration: var(--animation-duration);
20 | animation-fill-mode: backwards;
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/popup/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { getUserInfo } from "redux-popup/actions/user";
5 | import {
6 | checkConnection,
7 | wentOffline,
8 | wentOnline
9 | } from "redux-popup/actions/online";
10 | import Logo from "./components/Logo";
11 | import Nav from "./components/Nav";
12 | import Main from "./components/Main";
13 | import "./App.css";
14 |
15 | export class App extends Component {
16 | componentDidMount() {
17 | window.addEventListener("online", this.handleOnlineEvent);
18 | window.addEventListener("offline", this.handleOfflineEvent);
19 | this.props.checkConnection(navigator.onLine);
20 | this.props.getUserInfo();
21 | }
22 |
23 | componentWillUnmount() {
24 | window.removeEventListener("online", this.handleOnlineEvent);
25 | window.removeEventListener("offline", this.handleOfflineEvent);
26 | }
27 |
28 | render() {
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | handleOnlineEvent = () => {
47 | this.props.wentOnline();
48 | };
49 |
50 | handleOfflineEvent = () => {
51 | this.props.wentOffline();
52 | };
53 | }
54 |
55 | App.propTypes = {
56 | getUserInfo: PropTypes.func.isRequired,
57 | checkConnection: PropTypes.func.isRequired,
58 | wentOnline: PropTypes.func.isRequired,
59 | wentOffline: PropTypes.func.isRequired
60 | };
61 |
62 | const mapDispatchToProps = dispatch => {
63 | return {
64 | getUserInfo: () => dispatch(getUserInfo()),
65 | checkConnection: online => dispatch(checkConnection(online)),
66 | wentOnline: () => dispatch(wentOnline()),
67 | wentOffline: () => dispatch(wentOffline())
68 | };
69 | };
70 |
71 | export default connect(
72 | null,
73 | mapDispatchToProps
74 | )(App);
75 |
--------------------------------------------------------------------------------
/src/popup/components/App/__tests__/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { App } from "../App";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | getUserInfo: jest.fn(),
9 | checkConnection: jest.fn(),
10 | wentOnline: jest.fn(),
11 | wentOffline: jest.fn()
12 | };
13 |
14 | it("should render correctly", () => {
15 | const tree = shallow();
16 | expect(tree).toMatchSnapshot();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Logo/Logo.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | display: flex;
3 | }
4 |
5 | .logo__title {
6 | margin-right: calc(var(--lh) / 4);
7 | }
8 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Logo/Logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { Title, Link } from "theme";
6 | import "./Logo.css";
7 |
8 | export const Logo = props => {
9 | const { username, online } = props;
10 | const shouldDisplayUsername = username && online;
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | {shouldDisplayUsername && (
18 | <>
19 | (
20 |
26 | )
27 | >
28 | )}
29 |
30 | );
31 | };
32 |
33 | Logo.propTypes = {
34 | username: PropTypes.string.isRequired,
35 | online: PropTypes.bool.isRequired
36 | };
37 |
38 | const mapStateToProps = state => ({
39 | username: state.user.username,
40 | online: state.online
41 | });
42 |
43 | export default connect(mapStateToProps)(Logo);
44 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Logo/__tests__/Logo.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Logo } from "../Logo";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | username: "pinbuddy",
9 | online: true
10 | };
11 |
12 | it("should render correctly", () => {
13 | const tree = shallow();
14 | expect(tree).toMatchSnapshot();
15 | });
16 |
17 | it("should not render when username is falsy", () => {
18 | const tree = shallow();
19 | expect(tree).toMatchSnapshot();
20 | });
21 |
22 | it("should not render when online is falsy", () => {
23 | const tree = shallow();
24 | expect(tree).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Logo/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Logo";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/Main.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import Offline from "./components/Offline";
6 | import Online from "./components/Online";
7 |
8 | export const Main = props => {
9 | return props.online ? : ;
10 | };
11 |
12 | Main.propTypes = {
13 | online: PropTypes.bool.isRequired
14 | };
15 |
16 | const mapStateToProps = state => ({
17 | online: state.online
18 | });
19 |
20 | export default connect(mapStateToProps)(Main);
21 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/__tests__/Main.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Main } from "../Main";
5 | import Online from "../components/Online";
6 | import Offline from "../components/Offline";
7 |
8 | describe("", () => {
9 | const defaultProps = {
10 | online: true
11 | };
12 |
13 | it("should render correctly", () => {
14 | const tree = shallow();
15 | expect(tree).toMatchSnapshot();
16 | });
17 |
18 | it("should render Online view when online", () => {
19 | const tree = shallow();
20 | expect(tree.contains()).toBeTruthy();
21 | });
22 |
23 | it("should render Offline view when offline", () => {
24 | const tree = shallow();
25 | expect(tree.contains()).toBeTruthy();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/__tests__/__snapshots__/Main.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
9 | Symbol(enzyme.__renderer__): Object {
10 | "batchedUpdates": [Function],
11 | "getNode": [Function],
12 | "render": [Function],
13 | "simulateError": [Function],
14 | "simulateEvent": [Function],
15 | "unmount": [Function],
16 | },
17 | Symbol(enzyme.__node__): Object {
18 | "instance": null,
19 | "key": undefined,
20 | "nodeType": "class",
21 | "props": Object {},
22 | "ref": null,
23 | "rendered": null,
24 | "type": [Function],
25 | },
26 | Symbol(enzyme.__nodes__): Array [
27 | Object {
28 | "instance": null,
29 | "key": undefined,
30 | "nodeType": "class",
31 | "props": Object {},
32 | "ref": null,
33 | "rendered": null,
34 | "type": [Function],
35 | },
36 | ],
37 | Symbol(enzyme.__options__): Object {
38 | "adapter": ReactSixteenAdapter {
39 | "options": Object {
40 | "enableComponentDidUpdateOnSetState": true,
41 | "lifecycles": Object {
42 | "componentDidUpdate": Object {
43 | "onSetState": true,
44 | },
45 | "getDerivedStateFromProps": true,
46 | "getSnapshotBeforeUpdate": true,
47 | "setState": Object {
48 | "skipsComponentDidUpdateOnNullish": true,
49 | },
50 | },
51 | },
52 | },
53 | },
54 | }
55 | `;
56 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Offline/Offline.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Paragraph, Quote } from "theme";
3 |
4 | const Offline = () => (
5 | <>
6 |
7 |
8 |
9 | >
10 | );
11 |
12 | export default Offline;
13 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Offline/__tests__/Offline.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Offline from "../Offline";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Offline/__tests__/__snapshots__/Offline.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "function",
19 | "props": Object {
20 | "children": Array [
21 | ,
25 |
,
28 | ,
32 | ],
33 | },
34 | "ref": null,
35 | "rendered": Array [
36 | Object {
37 | "instance": null,
38 | "key": undefined,
39 | "nodeType": "function",
40 | "props": Object {
41 | "innerHTML": true,
42 | "t": "popupOfflineMessageOne",
43 | },
44 | "ref": null,
45 | "rendered": null,
46 | "type": [Function],
47 | },
48 | Object {
49 | "instance": null,
50 | "key": undefined,
51 | "nodeType": "function",
52 | "props": Object {
53 | "t": "popupOfflineMessageQuote",
54 | },
55 | "ref": null,
56 | "rendered": null,
57 | "type": [Function],
58 | },
59 | Object {
60 | "instance": null,
61 | "key": undefined,
62 | "nodeType": "function",
63 | "props": Object {
64 | "innerHTML": false,
65 | "t": "popupOfflineMessageTwo",
66 | },
67 | "ref": null,
68 | "rendered": null,
69 | "type": [Function],
70 | },
71 | ],
72 | "type": Symbol(react.fragment),
73 | },
74 | Symbol(enzyme.__nodes__): Array [
75 | Object {
76 | "instance": null,
77 | "key": undefined,
78 | "nodeType": "function",
79 | "props": Object {
80 | "children": Array [
81 | ,
85 |
,
88 | ,
92 | ],
93 | },
94 | "ref": null,
95 | "rendered": Array [
96 | Object {
97 | "instance": null,
98 | "key": undefined,
99 | "nodeType": "function",
100 | "props": Object {
101 | "innerHTML": true,
102 | "t": "popupOfflineMessageOne",
103 | },
104 | "ref": null,
105 | "rendered": null,
106 | "type": [Function],
107 | },
108 | Object {
109 | "instance": null,
110 | "key": undefined,
111 | "nodeType": "function",
112 | "props": Object {
113 | "t": "popupOfflineMessageQuote",
114 | },
115 | "ref": null,
116 | "rendered": null,
117 | "type": [Function],
118 | },
119 | Object {
120 | "instance": null,
121 | "key": undefined,
122 | "nodeType": "function",
123 | "props": Object {
124 | "innerHTML": false,
125 | "t": "popupOfflineMessageTwo",
126 | },
127 | "ref": null,
128 | "rendered": null,
129 | "type": [Function],
130 | },
131 | ],
132 | "type": Symbol(react.fragment),
133 | },
134 | ],
135 | Symbol(enzyme.__options__): Object {
136 | "adapter": ReactSixteenAdapter {
137 | "options": Object {
138 | "enableComponentDidUpdateOnSetState": true,
139 | "lifecycles": Object {
140 | "componentDidUpdate": Object {
141 | "onSetState": true,
142 | },
143 | "getDerivedStateFromProps": true,
144 | "getSnapshotBeforeUpdate": true,
145 | "setState": Object {
146 | "skipsComponentDidUpdateOnNullish": true,
147 | },
148 | },
149 | },
150 | },
151 | },
152 | }
153 | `;
154 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Offline/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Offline";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/Online.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import LoggedInView from "./components/LoggedInView";
6 | import LoggedOutView from "./components/LoggedOutView";
7 |
8 | export const Online = props => {
9 | const { username, token } = props;
10 | const userSignedIn = username && token;
11 |
12 | return userSignedIn ? : ;
13 | };
14 |
15 | Online.propTypes = {
16 | username: PropTypes.string.isRequired,
17 | token: PropTypes.string.isRequired
18 | };
19 |
20 | const mapStateToProps = state => ({
21 | username: state.user.username,
22 | token: state.user.token
23 | });
24 |
25 | export default connect(mapStateToProps)(Online);
26 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/__tests__/Online.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Online } from "../Online";
5 | import LoggedInView from "../components/LoggedInView";
6 | import LoggedOutView from "../components/LoggedOutView";
7 |
8 | describe("", () => {
9 | const defaultProps = {
10 | username: "pinbuddy",
11 | token: "pinbuddy"
12 | };
13 |
14 | it("should render correctly", () => {
15 | const tree = shallow();
16 | expect(tree).toMatchSnapshot();
17 | });
18 |
19 | it("should render Online view when online", () => {
20 | const tree = shallow();
21 | expect(tree.contains()).toBeTruthy();
22 | });
23 |
24 | it("should render Offline view when offline", () => {
25 | const tree = shallow();
26 | expect(tree.contains()).toBeTruthy();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/__tests__/__snapshots__/Online.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
10 | Symbol(enzyme.__renderer__): Object {
11 | "batchedUpdates": [Function],
12 | "getNode": [Function],
13 | "render": [Function],
14 | "simulateError": [Function],
15 | "simulateEvent": [Function],
16 | "unmount": [Function],
17 | },
18 | Symbol(enzyme.__node__): Object {
19 | "instance": null,
20 | "key": undefined,
21 | "nodeType": "class",
22 | "props": Object {},
23 | "ref": null,
24 | "rendered": null,
25 | "type": [Function],
26 | },
27 | Symbol(enzyme.__nodes__): Array [
28 | Object {
29 | "instance": null,
30 | "key": undefined,
31 | "nodeType": "class",
32 | "props": Object {},
33 | "ref": null,
34 | "rendered": null,
35 | "type": [Function],
36 | },
37 | ],
38 | Symbol(enzyme.__options__): Object {
39 | "adapter": ReactSixteenAdapter {
40 | "options": Object {
41 | "enableComponentDidUpdateOnSetState": true,
42 | "lifecycles": Object {
43 | "componentDidUpdate": Object {
44 | "onSetState": true,
45 | },
46 | "getDerivedStateFromProps": true,
47 | "getSnapshotBeforeUpdate": true,
48 | "setState": Object {
49 | "skipsComponentDidUpdateOnNullish": true,
50 | },
51 | },
52 | },
53 | },
54 | },
55 | }
56 | `;
57 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/LoggedInView.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import All from "./components/All";
6 | import Add from "./components/Add";
7 | import Loading from "./components/Loading";
8 | import Error from "./components/Error";
9 |
10 | export const LoggedInView = props => {
11 | const { loading, error, view } = props;
12 |
13 | if (loading) {
14 | return ;
15 | } else if (error) {
16 | return ;
17 | }
18 |
19 | return view === "all" ? : ;
20 | };
21 |
22 | LoggedInView.propTypes = {
23 | loading: PropTypes.bool.isRequired,
24 | error: PropTypes.bool.isRequired,
25 | view: PropTypes.string.isRequired
26 | };
27 |
28 | const mapStateToProps = state => ({
29 | view: state.view,
30 | loading: state.loading,
31 | error: state.error
32 | });
33 |
34 | export default connect(mapStateToProps)(LoggedInView);
35 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/__tests__/LoggedInView.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { LoggedInView } from "../LoggedInView";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | loading: false,
9 | error: false,
10 | view: "all"
11 | };
12 |
13 | it("should render correctly", () => {
14 | const tree = shallow();
15 | expect(tree).toMatchSnapshot();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/__tests__/__snapshots__/LoggedInView.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
11 | Symbol(enzyme.__renderer__): Object {
12 | "batchedUpdates": [Function],
13 | "getNode": [Function],
14 | "render": [Function],
15 | "simulateError": [Function],
16 | "simulateEvent": [Function],
17 | "unmount": [Function],
18 | },
19 | Symbol(enzyme.__node__): Object {
20 | "instance": null,
21 | "key": undefined,
22 | "nodeType": "class",
23 | "props": Object {},
24 | "ref": null,
25 | "rendered": null,
26 | "type": [Function],
27 | },
28 | Symbol(enzyme.__nodes__): Array [
29 | Object {
30 | "instance": null,
31 | "key": undefined,
32 | "nodeType": "class",
33 | "props": Object {},
34 | "ref": null,
35 | "rendered": null,
36 | "type": [Function],
37 | },
38 | ],
39 | Symbol(enzyme.__options__): Object {
40 | "adapter": ReactSixteenAdapter {
41 | "options": Object {
42 | "enableComponentDidUpdateOnSetState": true,
43 | "lifecycles": Object {
44 | "componentDidUpdate": Object {
45 | "onSetState": true,
46 | },
47 | "getDerivedStateFromProps": true,
48 | "getSnapshotBeforeUpdate": true,
49 | "setState": Object {
50 | "skipsComponentDidUpdateOnNullish": true,
51 | },
52 | },
53 | },
54 | },
55 | },
56 | }
57 | `;
58 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/Add.css:
--------------------------------------------------------------------------------
1 | .add {
2 | animation-name: fadeIn;
3 | animation-duration: var(--animation-duration);
4 | animation-fill-mode: backwards;
5 | }
6 |
7 | .add--loading {
8 | filter: blur(calc(var(--lh) / 6));
9 | opacity: .5;
10 | }
11 |
12 | .add__options {
13 | display: flex;
14 | }
15 |
16 | .add__option {
17 | margin-right: var(--lh);
18 | }
19 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/Add.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import Form from "./components/Form";
4 | import InvalidProtocol from "./components/InvalidProtocol";
5 |
6 | class Add extends Component {
7 | state = {
8 | ready: null,
9 | validProtocol: null
10 | };
11 |
12 | componentDidMount() {
13 | chrome.tabs.query(
14 | {
15 | active: true,
16 | currentWindow: true
17 | },
18 | result => {
19 | const { url } = result[0];
20 | const protocol = url.match(/^[^:]+/)[0];
21 | const validProtocolsList = [
22 | "http",
23 | "https",
24 | "javascript",
25 | "mailto",
26 | "ftp",
27 | "file"
28 | ];
29 | const validProtocol = validProtocolsList.includes(protocol);
30 |
31 | this.setState({
32 | ready: true,
33 | validProtocol
34 | });
35 | }
36 | );
37 | }
38 |
39 | render() {
40 | const { ready, validProtocol } = this.state;
41 | const shouldDisplayForm = ready && validProtocol;
42 |
43 | return shouldDisplayForm ? : ;
44 | }
45 | }
46 |
47 | export default Add;
48 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/__tests__/Add.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Add from "../Add";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/__tests__/__snapshots__/Add.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "class",
19 | "props": Object {},
20 | "ref": null,
21 | "rendered": null,
22 | "type": [Function],
23 | },
24 | Symbol(enzyme.__nodes__): Array [
25 | Object {
26 | "instance": null,
27 | "key": undefined,
28 | "nodeType": "class",
29 | "props": Object {},
30 | "ref": null,
31 | "rendered": null,
32 | "type": [Function],
33 | },
34 | ],
35 | Symbol(enzyme.__options__): Object {
36 | "adapter": ReactSixteenAdapter {
37 | "options": Object {
38 | "enableComponentDidUpdateOnSetState": true,
39 | "lifecycles": Object {
40 | "componentDidUpdate": Object {
41 | "onSetState": true,
42 | },
43 | "getDerivedStateFromProps": true,
44 | "getSnapshotBeforeUpdate": true,
45 | "setState": Object {
46 | "skipsComponentDidUpdateOnNullish": true,
47 | },
48 | },
49 | },
50 | },
51 | },
52 | }
53 | `;
54 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/Form/Form.css:
--------------------------------------------------------------------------------
1 | .form {
2 |
3 | }
4 |
5 | .form__options {
6 | display: flex;
7 | }
8 |
9 | .form__option {
10 | margin-right: var(--lh);
11 | }
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/Form/Form.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { postsAdd } from "redux-popup/actions/posts";
6 | import { Input, Button, Checkbox } from "theme";
7 | import "./Form.css";
8 |
9 | export class Form extends Component {
10 | state = {
11 | title: "",
12 | url: "",
13 | description: "",
14 | tags: "",
15 | privatePost: false,
16 | readLater: false,
17 | previouslySaved: false
18 | };
19 |
20 | componentDidMount() {
21 | chrome.tabs.query(
22 | {
23 | active: true,
24 | currentWindow: true
25 | },
26 | result => {
27 | const { title, url } = result[0];
28 |
29 | chrome.storage.local.get(["posts"], result => {
30 | const postExists = result.posts.find(post => post.href === url);
31 | if (postExists) {
32 | const {
33 | description: title,
34 | extended: description,
35 | tags,
36 | shared,
37 | toread
38 | } = postExists;
39 | this.setState({
40 | title,
41 | url,
42 | description,
43 | tags,
44 | privatePost: shared === "no",
45 | readLater: toread === "yes",
46 | previouslySaved: true
47 | });
48 | } else {
49 | this.setState({
50 | title,
51 | url
52 | });
53 |
54 | if (!url.includes("chrome.google.com")) {
55 | chrome.tabs.executeScript(
56 | { code: "window.getSelection().toString()" },
57 | selection => {
58 | if (selection) {
59 | this.setState({
60 | description: selection[0]
61 | });
62 | }
63 | }
64 | );
65 | }
66 |
67 | chrome.storage.sync.get(
68 | ["privateCheckboxByDefault", "toReadChecboxByDefault"],
69 | result => {
70 | this.setState({
71 | privatePost: result.privateCheckboxByDefault,
72 | readLater: result.toReadChecboxByDefault
73 | });
74 | }
75 | );
76 | }
77 | });
78 | }
79 | );
80 |
81 | window.addEventListener("keydown", this.handleKeydown);
82 | }
83 |
84 | componentWillUnmount() {
85 | window.removeEventListener("keydown", this.handleKeydown);
86 | }
87 |
88 | render() {
89 | const formInvalid =
90 | this.state.title.length > 255 ||
91 | this.state.description.length > 65536 ||
92 | this.state.tags.length > 255;
93 |
94 | return (
95 |
96 |
105 |
106 |
117 |
118 |
128 |
129 |
130 |
131 |
137 |
138 |
139 |
145 |
146 |
147 |
148 |
161 |
162 | );
163 | }
164 |
165 | handleInputChange = e => {
166 | const { id, value } = e.target;
167 | this.setState({
168 | [id]: value
169 | });
170 | };
171 |
172 | handleCheckboxChange = e => {
173 | const { id, checked } = e.target;
174 | this.setState({
175 | [id]: checked
176 | });
177 | };
178 |
179 | handleButtonClick = () => {
180 | this.setState({
181 | laoding: true
182 | });
183 | this.props.postsAdd(this.state);
184 | };
185 |
186 | handleKeydown = e => {
187 | // ⌘ + Enter
188 | if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
189 | this.props.postsAdd(this.state);
190 | }
191 | };
192 | }
193 |
194 | Form.propTypes = {
195 | postsAdd: PropTypes.func.isRequired
196 | };
197 |
198 | const mapDispatchToProps = dispatch => ({
199 | postsAdd: postInfo => dispatch(postsAdd(postInfo))
200 | });
201 |
202 | export default connect(
203 | null,
204 | mapDispatchToProps
205 | )(Form);
206 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/Form/__tests__/Form.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Form } from "../Form";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | postsAdd: jest.fn()
9 | };
10 |
11 | it("should render correctly", () => {
12 | const tree = shallow();
13 | expect(tree).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/Form/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Form";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/InvalidProtocol/InvalidProtocol.css:
--------------------------------------------------------------------------------
1 | .invalid-protocol {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/InvalidProtocol/InvalidProtocol.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { Paragraph, Button } from "theme";
3 | import "./InvalidProtocol.css";
4 |
5 | class InvalidProtocol extends PureComponent {
6 | render() {
7 | return (
8 |
18 | );
19 | }
20 |
21 | handeOpenOptionPageButtonClick = () => {
22 | chrome.tabs.create({
23 | url: "https://pinboard.in/api/#entity_url"
24 | });
25 | };
26 | }
27 |
28 | export default InvalidProtocol;
29 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/InvalidProtocol/__tests__/InvalidProtocol.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import InvalidProtocol from "../InvalidProtocol";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/InvalidProtocol/__tests__/__snapshots__/InvalidProtocol.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "host",
19 | "props": Object {
20 | "children": Array [
21 | ,
25 | ,
33 | ],
34 | "className": "invalid-protocol",
35 | },
36 | "ref": null,
37 | "rendered": Array [
38 | Object {
39 | "instance": null,
40 | "key": undefined,
41 | "nodeType": "function",
42 | "props": Object {
43 | "innerHTML": true,
44 | "t": "popupInvalidProtocolParagraph",
45 | },
46 | "ref": null,
47 | "rendered": null,
48 | "type": [Function],
49 | },
50 | Object {
51 | "instance": null,
52 | "key": undefined,
53 | "nodeType": "function",
54 | "props": Object {
55 | "disabled": false,
56 | "onClick": [Function],
57 | "secondary": false,
58 | "t": "popupInvalidProtocolButton",
59 | "title": null,
60 | "type": "button",
61 | },
62 | "ref": null,
63 | "rendered": null,
64 | "type": [Function],
65 | },
66 | ],
67 | "type": "div",
68 | },
69 | Symbol(enzyme.__nodes__): Array [
70 | Object {
71 | "instance": null,
72 | "key": undefined,
73 | "nodeType": "host",
74 | "props": Object {
75 | "children": Array [
76 | ,
80 | ,
88 | ],
89 | "className": "invalid-protocol",
90 | },
91 | "ref": null,
92 | "rendered": Array [
93 | Object {
94 | "instance": null,
95 | "key": undefined,
96 | "nodeType": "function",
97 | "props": Object {
98 | "innerHTML": true,
99 | "t": "popupInvalidProtocolParagraph",
100 | },
101 | "ref": null,
102 | "rendered": null,
103 | "type": [Function],
104 | },
105 | Object {
106 | "instance": null,
107 | "key": undefined,
108 | "nodeType": "function",
109 | "props": Object {
110 | "disabled": false,
111 | "onClick": [Function],
112 | "secondary": false,
113 | "t": "popupInvalidProtocolButton",
114 | "title": null,
115 | "type": "button",
116 | },
117 | "ref": null,
118 | "rendered": null,
119 | "type": [Function],
120 | },
121 | ],
122 | "type": "div",
123 | },
124 | ],
125 | Symbol(enzyme.__options__): Object {
126 | "adapter": ReactSixteenAdapter {
127 | "options": Object {
128 | "enableComponentDidUpdateOnSetState": true,
129 | "lifecycles": Object {
130 | "componentDidUpdate": Object {
131 | "onSetState": true,
132 | },
133 | "getDerivedStateFromProps": true,
134 | "getSnapshotBeforeUpdate": true,
135 | "setState": Object {
136 | "skipsComponentDidUpdateOnNullish": true,
137 | },
138 | },
139 | },
140 | },
141 | },
142 | }
143 | `;
144 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/components/InvalidProtocol/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./InvalidProtocol";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Add/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Add";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/All.css:
--------------------------------------------------------------------------------
1 | .all {
2 | animation-name: fadeIn;
3 | animation-duration: var(--animation-duration);
4 | animation-fill-mode: backwards;
5 | }
6 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/All.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { postsGet } from "redux-popup/actions/posts";
6 | import Filters from "./components/Filters";
7 | import List from "./components/List";
8 | import "./All.css";
9 |
10 | export class All extends Component {
11 | state = {
12 | keyword: "",
13 | privatePost: false,
14 | publicPost: false,
15 | untagged: false,
16 | unread: false
17 | };
18 |
19 | componentDidMount() {
20 | if (!this.props.posts.length) {
21 | this.props.postsGet();
22 | }
23 | }
24 |
25 | render() {
26 | return (
27 | !!this.props.posts.length && (
28 |
29 |
40 |
41 |
42 | )
43 | );
44 | }
45 |
46 | updatePrivatePost = () => {
47 | this.setState(state => {
48 | return {
49 | privatePost: !state.privatePost,
50 | publicPost: false
51 | };
52 | });
53 | };
54 |
55 | updatePublicPost = () => {
56 | this.setState(state => {
57 | return {
58 | publicPost: !state.publicPost,
59 | privatePost: false
60 | };
61 | });
62 | };
63 |
64 | updateUnread = () => {
65 | this.setState(state => {
66 | return {
67 | unread: !state.unread
68 | };
69 | });
70 | };
71 |
72 | updateUntagged = () => {
73 | this.setState(state => {
74 | return {
75 | untagged: !state.untagged
76 | };
77 | });
78 | };
79 |
80 | updateKeyword = e => {
81 | this.setState({
82 | keyword: e.target.value
83 | });
84 | };
85 | }
86 |
87 | All.propTypes = {
88 | posts: PropTypes.array.isRequired,
89 | postsGet: PropTypes.func.isRequired
90 | };
91 |
92 | const mapStateToProps = (state, ownProps) => {
93 | return {
94 | posts: state.posts
95 | };
96 | };
97 |
98 | const mapDispatchToProps = dispatch => {
99 | return {
100 | postsGet: () => dispatch(postsGet())
101 | };
102 | };
103 |
104 | export default connect(
105 | mapStateToProps,
106 | mapDispatchToProps
107 | )(All);
108 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/__tests__/All.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { All } from "../All";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | posts: [],
9 | postsGet: jest.fn()
10 | };
11 |
12 | it("should render correctly", () => {
13 | const tree = shallow();
14 | expect(tree).toMatchSnapshot();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/__tests__/__snapshots__/All.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
22 | Symbol(enzyme.__renderer__): Object {
23 | "batchedUpdates": [Function],
24 | "getNode": [Function],
25 | "render": [Function],
26 | "simulateError": [Function],
27 | "simulateEvent": [Function],
28 | "unmount": [Function],
29 | },
30 | Symbol(enzyme.__node__): false,
31 | Symbol(enzyme.__nodes__): Array [
32 | false,
33 | ],
34 | Symbol(enzyme.__options__): Object {
35 | "adapter": ReactSixteenAdapter {
36 | "options": Object {
37 | "enableComponentDidUpdateOnSetState": true,
38 | "lifecycles": Object {
39 | "componentDidUpdate": Object {
40 | "onSetState": true,
41 | },
42 | "getDerivedStateFromProps": true,
43 | "getSnapshotBeforeUpdate": true,
44 | "setState": Object {
45 | "skipsComponentDidUpdateOnNullish": true,
46 | },
47 | },
48 | },
49 | },
50 | },
51 | }
52 | `;
53 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/Filters/Filters.css:
--------------------------------------------------------------------------------
1 | .filters__top {
2 | display: flex;
3 | align-items: flex-end;
4 | }
5 |
6 | .filters__input {
7 | width: 100%;
8 | }
9 |
10 | .filters__button {
11 | margin-left: var(--lh);
12 | }
13 |
14 | .filters__bottom {
15 | display: none;
16 | }
17 |
18 | .filters__bottom--active {
19 | display: flex;
20 | }
21 |
22 | .filters__option {
23 | margin-right: var(--lh);
24 | }
25 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/Filters/Filters.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { Input, Button, Checkbox } from "theme";
5 | import "./Filters.css";
6 |
7 | class Filters extends Component {
8 | state = {
9 | showFilters: false
10 | };
11 |
12 | render() {
13 | const {
14 | privatePost,
15 | publicPost,
16 | unread,
17 | untagged,
18 | updateKeyword,
19 | updatePrivatePost,
20 | updatePublicPost,
21 | updateUnread,
22 | updateUntagged
23 | } = this.props;
24 |
25 | return (
26 |
27 |
28 |
29 |
38 |
39 |
40 |
44 |
45 |
46 |
53 |
54 |
60 |
61 |
62 |
68 |
69 |
70 |
76 |
77 |
78 |
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | toggleFilters = () => {
91 | this.setState(state => {
92 | return {
93 | showFilters: !state.showFilters
94 | };
95 | });
96 | };
97 | }
98 |
99 | Filters.propTypes = {
100 | privatePost: PropTypes.bool.isRequired,
101 | publicPost: PropTypes.bool.isRequired,
102 | unread: PropTypes.bool.isRequired,
103 | untagged: PropTypes.bool.isRequired,
104 | updateKeyword: PropTypes.func.isRequired,
105 | updatePrivatePost: PropTypes.func.isRequired,
106 | updatePublicPost: PropTypes.func.isRequired,
107 | updateUnread: PropTypes.func.isRequired,
108 | updateUntagged: PropTypes.func.isRequired
109 | };
110 |
111 | export default Filters;
112 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/Filters/__tests__/Filters.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Filters from "../Filters";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | privatePost: true,
9 | publicPost: true,
10 | unread: true,
11 | untagged: true,
12 | updateKeyword: jest.fn(),
13 | updatePrivatePost: jest.fn(),
14 | updatePublicPost: jest.fn(),
15 | updateUnread: jest.fn(),
16 | updateUntagged: jest.fn()
17 | };
18 |
19 | it("should render correctly", () => {
20 | const tree = shallow();
21 | expect(tree).toMatchSnapshot();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/Filters/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Filters";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/List.css:
--------------------------------------------------------------------------------
1 | .list {
2 | list-style: none;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | .list__item {
8 | margin-bottom: calc(var(--lh) / 2);
9 | }
10 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import Article from "./components/Article";
5 | import "./List.css";
6 |
7 | class List extends Component {
8 | componentDidMount() {
9 | addEventListener("keydown", this.handleKeyDownEvent);
10 | }
11 |
12 | componentWillUnmount() {
13 | removeEventListener("keydown", this.handleKeyDownEvent);
14 | }
15 |
16 | render() {
17 | return (
18 |
19 | {this.props.posts
20 | .filter(this.filterPrivate)
21 | .filter(this.filterPublic)
22 | .filter(this.filterUnread)
23 | .filter(this.filterUntagged)
24 | .filter(this.filterkeyword)
25 | .map(this.renderFilteredList)}
26 |
27 | );
28 | }
29 |
30 | filterPrivate = post => {
31 | return this.props.filters.privatePost ? post.shared === "no" : post;
32 | };
33 |
34 | filterPublic = post => {
35 | return this.props.filters.publicPost ? post.shared === "yes" : post;
36 | };
37 |
38 | filterUnread = post => {
39 | if (this.props.filters.unread) {
40 | return post.toread === "yes";
41 | }
42 | return post;
43 | };
44 |
45 | filterUntagged = post => {
46 | return this.props.filters.untagged ? post.tags === "" : post;
47 | };
48 |
49 | filterkeyword = post => {
50 | const { keyword } = this.props.filters;
51 | const postsDescription = post.description.toLowerCase();
52 | const searchterm = keyword.toLowerCase();
53 |
54 | return keyword.length ? postsDescription.includes(searchterm) : post;
55 | };
56 |
57 | renderFilteredList = post => {
58 | const privatePost = post.shared === "no";
59 | const unread = post.toread === "yes";
60 |
61 | return (
62 |
63 |
72 |
73 | );
74 | };
75 |
76 | handleKeyDownEvent = e => {
77 | const elements = [
78 | ...document.querySelectorAll(
79 | '.input__input, .article__url:not([tabindex="-1"]'
80 | )
81 | ];
82 | const currentFocus = elements.findIndex(
83 | elm => elm === document.activeElement
84 | );
85 | const isInsideElements = elements.includes(document.activeElement);
86 |
87 | if (e.keyCode === 38 && isInsideElements) {
88 | const previousElement =
89 | currentFocus === 0 ? elements.length - 1 : currentFocus - 1;
90 | elements[previousElement].focus();
91 | } else if (e.keyCode === 40 && isInsideElements) {
92 | const nextElement =
93 | currentFocus === elements.length - 1 ? 0 : currentFocus + 1;
94 | elements[nextElement].focus();
95 | }
96 | };
97 | }
98 |
99 | List.propTypes = {
100 | posts: PropTypes.array.isRequired,
101 | filters: PropTypes.shape({
102 | keyword: PropTypes.string.isRequired,
103 | unread: PropTypes.bool.isRequired,
104 | untagged: PropTypes.bool.isRequired,
105 | privatePost: PropTypes.bool.isRequired,
106 | publicPost: PropTypes.bool.isRequired
107 | }).isRequired
108 | };
109 |
110 | export default List;
111 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/__tests__/List.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import List from "../List";
5 |
6 | describe("
", () => {
7 | const defaultProps = {
8 | posts: [],
9 | filters: {
10 | keyword: "",
11 | unread: true,
12 | untagged: true,
13 | privatePost: true,
14 | publicPost: true
15 | }
16 | };
17 |
18 | it("should render correctly", () => {
19 | const tree = shallow(
);
20 | expect(tree).toMatchSnapshot();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/__tests__/__snapshots__/List.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`
should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__):
,
18 | Symbol(enzyme.__renderer__): Object {
19 | "batchedUpdates": [Function],
20 | "getNode": [Function],
21 | "render": [Function],
22 | "simulateError": [Function],
23 | "simulateEvent": [Function],
24 | "unmount": [Function],
25 | },
26 | Symbol(enzyme.__node__): Object {
27 | "instance": null,
28 | "key": undefined,
29 | "nodeType": "host",
30 | "props": Object {
31 | "children": Array [],
32 | "className": "list",
33 | },
34 | "ref": null,
35 | "rendered": Array [],
36 | "type": "ul",
37 | },
38 | Symbol(enzyme.__nodes__): Array [
39 | Object {
40 | "instance": null,
41 | "key": undefined,
42 | "nodeType": "host",
43 | "props": Object {
44 | "children": Array [],
45 | "className": "list",
46 | },
47 | "ref": null,
48 | "rendered": Array [],
49 | "type": "ul",
50 | },
51 | ],
52 | Symbol(enzyme.__options__): Object {
53 | "adapter": ReactSixteenAdapter {
54 | "options": Object {
55 | "enableComponentDidUpdateOnSetState": true,
56 | "lifecycles": Object {
57 | "componentDidUpdate": Object {
58 | "onSetState": true,
59 | },
60 | "getDerivedStateFromProps": true,
61 | "getSnapshotBeforeUpdate": true,
62 | "setState": Object {
63 | "skipsComponentDidUpdateOnNullish": true,
64 | },
65 | },
66 | },
67 | },
68 | },
69 | }
70 | `;
71 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/components/Article/Article.css:
--------------------------------------------------------------------------------
1 | @keyframes articleLoading {
2 | to {
3 | transform: rotate(360deg);
4 | }
5 | }
6 |
7 | .article {
8 | display: block;
9 | text-decoration: none;
10 | position: relative;
11 | }
12 |
13 | .article--private {
14 | background-color: var(--color-gray-light);
15 | }
16 |
17 | .article__loading {
18 | animation-name: fadeIn;
19 | animation-duration: var(--animation-duration);
20 | animation-fill-mode: backwards;
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | width: 100%;
25 | height: 100%;
26 | display: flex;
27 | justify-content: center;
28 | align-items: center;
29 | background-color: #fff;
30 | }
31 |
32 | .article__loading-icon {
33 | border-radius: 50%;
34 | width: calc(var(--lh) - 2px);
35 | height: calc(var(--lh) - 2px);
36 | border: 2px solid var(--color-gray-light);
37 | border-top-color: var(--color-primary);
38 | animation-name: articleLoading;
39 | animation-duration: calc(var(--animation-duration) * 4);
40 | animation-iteration-count: infinite;
41 | animation-timing-function: cubic-bezier(.3, .1, .3, .85);
42 | }
43 |
44 | .article__url {
45 | text-decoration: none;
46 | }
47 |
48 | .article__url:hover {
49 | text-decoration: underline;
50 | }
51 |
52 | .article__url--unread {
53 | color: var(--color-secondary);
54 | }
55 | .article__meta {
56 | font-size: .8rem;
57 | color: var(--color-gray);
58 | }
59 |
60 | .article__metatop {
61 | text-overflow: ellipsis;
62 | overflow: hidden;
63 | white-space: nowrap;
64 | }
65 |
66 | .article__metabottom {
67 | display: flex;
68 | }
69 |
70 | .article__metainfo::before {
71 | content: '|';
72 | margin: 0 calc(var(--lh) / 4);
73 | }
74 |
75 | .article__metainfo:first-child::before {
76 | display: none;
77 | }
78 |
79 | .article__metainfo--tags {
80 | text-overflow: ellipsis;
81 | overflow: hidden;
82 | white-space: nowrap;
83 | }
84 |
85 | .article__cancel,
86 | .article__delete {
87 | cursor: pointer;
88 | }
89 |
90 | .article__cancel:hover {
91 | color: var(--color-primary);
92 | }
93 |
94 | .article__separator {
95 | margin: calc(var(--lh) / 4);
96 | }
97 |
98 | .article__delete:hover {
99 | color: var(--color-error);
100 | }
101 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/components/Article/Article.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { postsDelete } from "redux-popup/actions/posts";
6 | import "./Article.css";
7 |
8 | export class Article extends Component {
9 | state = {
10 | deleteActive: false,
11 | deleteConfirmed: false,
12 | focused: false
13 | };
14 |
15 | linkRef = React.createRef();
16 |
17 | componentDidMount() {
18 | addEventListener("keydown", this.handleKeyDownEvent);
19 | }
20 |
21 | componentWillUnmount() {
22 | removeEventListener("keydown", this.handleKeyDownEvent);
23 | }
24 |
25 | render() {
26 | const { privatePost, unread, href, description, time, tags } = this.props;
27 |
28 | const timeFormated = time.substring(0, 10).replace(/-/g, ".");
29 | const tagsFormated = tags
30 | .split(" ")
31 | .map(tag => `#${tag}`)
32 | .join(" ");
33 | const { deleteActive, deleteConfirmed } = this.state;
34 |
35 | return (
36 |
37 | {deleteConfirmed && (
38 |
41 | )}
42 |
53 | {description}
54 |
55 |
56 |
{href}
57 |
58 |
{timeFormated}
59 | {tags ? (
60 |
61 | {tagsFormated}
62 |
63 | ) : (
64 | ""
65 | )}
66 |
67 | {deleteActive && (
68 | <>
69 |
73 | {chrome.i18n.getMessage("popupArticleButtonCancel")}
74 |
75 | /
76 | >
77 | )}
78 | {!deleteConfirmed && (
79 |
83 | {chrome.i18n.getMessage("popupArticleButtonDelete")}
84 |
85 | )}
86 |
87 |
88 |
89 | {/*
90 |
{timeFormated}
91 | {tags ?
{tagsFormated}
: ''}
92 |
93 | {deleteActive && (
94 | <>
95 | {chrome.i18n.getMessage('popupArticleButtonCancel')}
96 | /
97 | >
98 | )}
99 | {!deleteConfirmed && (
100 | {chrome.i18n.getMessage('popupArticleButtonDelete')}
101 | )}
102 |
103 |
*/}
104 |
105 | );
106 | }
107 |
108 | handleDeleteClick = () => {
109 | const { href, description, extended, postsDelete } = this.props;
110 | if (this.state.deleteActive) {
111 | this.setState({
112 | deleteConfirmed: true
113 | });
114 | postsDelete(href, description, extended);
115 | }
116 |
117 | this.setState(state => {
118 | return {
119 | deleteActive: !state.deleteActive
120 | };
121 | });
122 | };
123 |
124 | handleCancelClick = () => {
125 | this.setState(state => {
126 | return {
127 | deleteActive: !state.deleteActive
128 | };
129 | });
130 | };
131 |
132 | handleOnFocusLink = () => {
133 | this.setState({
134 | focused: true
135 | });
136 | };
137 |
138 | handleOnBlursLink = () => {
139 | this.setState({
140 | focused: false
141 | });
142 | };
143 |
144 | handleKeyDownEvent = e => {
145 | const { href, description, extended, postsDelete } = this.props;
146 |
147 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 8 && this.state.focused) {
148 | this.linkRef.current.blur();
149 | this.setState(
150 | state => ({
151 | deleteActive: true,
152 | deleteConfirmed: true
153 | }),
154 | () => postsDelete(href, description, extended)
155 | );
156 | } else if (
157 | e.keyCode === 13 &&
158 | (e.ctrlKey || e.metaKey) &&
159 | this.state.focused
160 | ) {
161 | e.preventDefault();
162 | chrome.tabs.create({
163 | url: this.props.href,
164 | active: false
165 | });
166 | }
167 | };
168 | }
169 |
170 | Article.propTypes = {
171 | privatePost: PropTypes.bool.isRequired,
172 | unread: PropTypes.bool.isRequired,
173 | href: PropTypes.string.isRequired,
174 | description: PropTypes.string.isRequired,
175 | extended: PropTypes.string.isRequired,
176 | time: PropTypes.string.isRequired,
177 | tags: PropTypes.string.isRequired,
178 | postsDelete: PropTypes.func.isRequired
179 | };
180 |
181 | const mapDispatchToProps = dispatch => ({
182 | postsDelete: (href, title, description) =>
183 | dispatch(postsDelete(href, title, description))
184 | });
185 |
186 | export default connect(
187 | null,
188 | mapDispatchToProps
189 | )(Article);
190 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/components/Article/__tests__/Article.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Article } from "../Article";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | privatePost: true,
9 | unread: true,
10 | href: "",
11 | description: "",
12 | extended: "",
13 | time: "",
14 | tags: "",
15 | postsDelete: jest.fn()
16 | };
17 |
18 | it("should render correctly", () => {
19 | const tree = shallow();
20 | expect(tree).toMatchSnapshot();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/components/Article/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Article";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/components/List/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./List";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/All/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./All";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Error/Error.css:
--------------------------------------------------------------------------------
1 | .error {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Error/Error.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Paragraph, Button } from "theme";
3 | import "./Error.css";
4 |
5 | class Error extends Component {
6 | render() {
7 | return (
8 |
15 | );
16 | }
17 |
18 | handeOpenOptionPageButtonClick = () => {
19 | chrome.tabs.create({
20 | url: "/options/options.html"
21 | });
22 | };
23 | }
24 |
25 | export default Error;
26 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Error/__tests__/Error.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Error from "../Error";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Error/__tests__/__snapshots__/Error.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "host",
19 | "props": Object {
20 | "children": Array [
21 | ,
25 | ,
33 | ],
34 | "className": "error",
35 | },
36 | "ref": null,
37 | "rendered": Array [
38 | Object {
39 | "instance": null,
40 | "key": undefined,
41 | "nodeType": "function",
42 | "props": Object {
43 | "innerHTML": true,
44 | "t": "popupErrorMessage",
45 | },
46 | "ref": null,
47 | "rendered": null,
48 | "type": [Function],
49 | },
50 | Object {
51 | "instance": null,
52 | "key": undefined,
53 | "nodeType": "function",
54 | "props": Object {
55 | "disabled": false,
56 | "onClick": [Function],
57 | "secondary": false,
58 | "t": "popupErrorButtonMessage",
59 | "title": null,
60 | "type": "button",
61 | },
62 | "ref": null,
63 | "rendered": null,
64 | "type": [Function],
65 | },
66 | ],
67 | "type": "div",
68 | },
69 | Symbol(enzyme.__nodes__): Array [
70 | Object {
71 | "instance": null,
72 | "key": undefined,
73 | "nodeType": "host",
74 | "props": Object {
75 | "children": Array [
76 | ,
80 | ,
88 | ],
89 | "className": "error",
90 | },
91 | "ref": null,
92 | "rendered": Array [
93 | Object {
94 | "instance": null,
95 | "key": undefined,
96 | "nodeType": "function",
97 | "props": Object {
98 | "innerHTML": true,
99 | "t": "popupErrorMessage",
100 | },
101 | "ref": null,
102 | "rendered": null,
103 | "type": [Function],
104 | },
105 | Object {
106 | "instance": null,
107 | "key": undefined,
108 | "nodeType": "function",
109 | "props": Object {
110 | "disabled": false,
111 | "onClick": [Function],
112 | "secondary": false,
113 | "t": "popupErrorButtonMessage",
114 | "title": null,
115 | "type": "button",
116 | },
117 | "ref": null,
118 | "rendered": null,
119 | "type": [Function],
120 | },
121 | ],
122 | "type": "div",
123 | },
124 | ],
125 | Symbol(enzyme.__options__): Object {
126 | "adapter": ReactSixteenAdapter {
127 | "options": Object {
128 | "enableComponentDidUpdateOnSetState": true,
129 | "lifecycles": Object {
130 | "componentDidUpdate": Object {
131 | "onSetState": true,
132 | },
133 | "getDerivedStateFromProps": true,
134 | "getSnapshotBeforeUpdate": true,
135 | "setState": Object {
136 | "skipsComponentDidUpdateOnNullish": true,
137 | },
138 | },
139 | },
140 | },
141 | },
142 | }
143 | `;
144 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Error/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Error";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Loading/Loading.css:
--------------------------------------------------------------------------------
1 | @keyframes loading {
2 | to {
3 | transform: rotate(360deg);
4 | }
5 | }
6 |
7 | .loading {
8 | height: calc(var(--lh) * 14);
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 |
14 | .loading__icon {
15 | border-radius: 50%;
16 | width: calc(var(--lh) - 2px);
17 | height: calc(var(--lh) - 2px);
18 | border: 2px solid var(--color-gray-light);
19 | border-top-color: var(--color-primary);
20 | animation-name: loading;
21 | animation-duration: calc(var(--animation-duration) * 4);
22 | animation-iteration-count: infinite;
23 | animation-timing-function: cubic-bezier(.3, .1, .3, .85);
24 | }
25 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Loading/Loading.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Paragraph } from "theme";
3 | import "./Loading.css";
4 |
5 | const Loading = props => (
6 |
9 | );
10 |
11 | export default Loading;
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Loading/__tests__/Loading.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import Loading from "../Loading";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Loading/__tests__/__snapshots__/Loading.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "host",
19 | "props": Object {
20 | "children": ,
23 | "className": "loading",
24 | },
25 | "ref": null,
26 | "rendered": Object {
27 | "instance": null,
28 | "key": undefined,
29 | "nodeType": "host",
30 | "props": Object {
31 | "className": "loading__icon",
32 | },
33 | "ref": null,
34 | "rendered": null,
35 | "type": "div",
36 | },
37 | "type": "div",
38 | },
39 | Symbol(enzyme.__nodes__): Array [
40 | Object {
41 | "instance": null,
42 | "key": undefined,
43 | "nodeType": "host",
44 | "props": Object {
45 | "children": ,
48 | "className": "loading",
49 | },
50 | "ref": null,
51 | "rendered": Object {
52 | "instance": null,
53 | "key": undefined,
54 | "nodeType": "host",
55 | "props": Object {
56 | "className": "loading__icon",
57 | },
58 | "ref": null,
59 | "rendered": null,
60 | "type": "div",
61 | },
62 | "type": "div",
63 | },
64 | ],
65 | Symbol(enzyme.__options__): Object {
66 | "adapter": ReactSixteenAdapter {
67 | "options": Object {
68 | "enableComponentDidUpdateOnSetState": true,
69 | "lifecycles": Object {
70 | "componentDidUpdate": Object {
71 | "onSetState": true,
72 | },
73 | "getDerivedStateFromProps": true,
74 | "getSnapshotBeforeUpdate": true,
75 | "setState": Object {
76 | "skipsComponentDidUpdateOnNullish": true,
77 | },
78 | },
79 | },
80 | },
81 | },
82 | }
83 | `;
84 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Loading";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedInView/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./LoggedInView";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedOutView/LoggedOutView.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { Paragraph, Button } from "theme";
3 |
4 | class LoggedOutView extends PureComponent {
5 | render() {
6 | return (
7 |
18 | );
19 | }
20 |
21 | handeOpenOptionPageButtonClick = () =>
22 | chrome.tabs.create({
23 | url: "/options/options.html"
24 | });
25 | }
26 |
27 | export default LoggedOutView;
28 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedOutView/__tests__/LoggedOutView.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import LoggedOutView from "../LoggedOutView";
5 |
6 | describe("", () => {
7 | it("should render correctly", () => {
8 | const tree = shallow();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedOutView/__tests__/__snapshots__/LoggedOutView.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
7 | Symbol(enzyme.__renderer__): Object {
8 | "batchedUpdates": [Function],
9 | "getNode": [Function],
10 | "render": [Function],
11 | "simulateError": [Function],
12 | "simulateEvent": [Function],
13 | "unmount": [Function],
14 | },
15 | Symbol(enzyme.__node__): Object {
16 | "instance": null,
17 | "key": undefined,
18 | "nodeType": "host",
19 | "props": Object {
20 | "children": Array [
21 | ,
25 | ,
33 | ],
34 | },
35 | "ref": null,
36 | "rendered": Array [
37 | Object {
38 | "instance": null,
39 | "key": undefined,
40 | "nodeType": "function",
41 | "props": Object {
42 | "innerHTML": true,
43 | "t": "popupLoggedOutViewMessage",
44 | },
45 | "ref": null,
46 | "rendered": null,
47 | "type": [Function],
48 | },
49 | Object {
50 | "instance": null,
51 | "key": undefined,
52 | "nodeType": "function",
53 | "props": Object {
54 | "disabled": false,
55 | "onClick": [Function],
56 | "secondary": false,
57 | "t": "popupLoggedOutViewButtonMessage",
58 | "title": null,
59 | "type": "button",
60 | },
61 | "ref": null,
62 | "rendered": null,
63 | "type": [Function],
64 | },
65 | ],
66 | "type": "div",
67 | },
68 | Symbol(enzyme.__nodes__): Array [
69 | Object {
70 | "instance": null,
71 | "key": undefined,
72 | "nodeType": "host",
73 | "props": Object {
74 | "children": Array [
75 | ,
79 | ,
87 | ],
88 | },
89 | "ref": null,
90 | "rendered": Array [
91 | Object {
92 | "instance": null,
93 | "key": undefined,
94 | "nodeType": "function",
95 | "props": Object {
96 | "innerHTML": true,
97 | "t": "popupLoggedOutViewMessage",
98 | },
99 | "ref": null,
100 | "rendered": null,
101 | "type": [Function],
102 | },
103 | Object {
104 | "instance": null,
105 | "key": undefined,
106 | "nodeType": "function",
107 | "props": Object {
108 | "disabled": false,
109 | "onClick": [Function],
110 | "secondary": false,
111 | "t": "popupLoggedOutViewButtonMessage",
112 | "title": null,
113 | "type": "button",
114 | },
115 | "ref": null,
116 | "rendered": null,
117 | "type": [Function],
118 | },
119 | ],
120 | "type": "div",
121 | },
122 | ],
123 | Symbol(enzyme.__options__): Object {
124 | "adapter": ReactSixteenAdapter {
125 | "options": Object {
126 | "enableComponentDidUpdateOnSetState": true,
127 | "lifecycles": Object {
128 | "componentDidUpdate": Object {
129 | "onSetState": true,
130 | },
131 | "getDerivedStateFromProps": true,
132 | "getSnapshotBeforeUpdate": true,
133 | "setState": Object {
134 | "skipsComponentDidUpdateOnNullish": true,
135 | },
136 | },
137 | },
138 | },
139 | },
140 | }
141 | `;
142 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/components/LoggedOutView/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./LoggedOutView";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/components/Online/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Online";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Main/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Main";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Nav/Nav.css:
--------------------------------------------------------------------------------
1 | .nav {
2 | font: inheit;
3 | margin: 0;
4 | padding: 0;
5 | display: inline;
6 | height: var(--lh);
7 | line-height: var(--lh);
8 | color: var(--color-gray);
9 | }
10 |
11 | .nav:hover {
12 | color: var(--color-primary);
13 | }
14 |
15 | .nav--active {
16 | color: var(--color-primary);
17 | }
18 |
19 |
20 | .nav__spacer {
21 | margin: 0 calc(var(--lh) / 4);
22 | }
23 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Nav/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 |
5 | import { updateView, getInitialView } from "redux-popup/actions/view";
6 | import "./Nav.css";
7 |
8 | export class Nav extends Component {
9 | componentDidMount() {
10 | this.props.getInitialView();
11 | window.addEventListener("keydown", this.handleKeydown);
12 | }
13 |
14 | componentWillUnmount() {
15 | window.removeEventListener("keydown", this.handleKeydown);
16 | }
17 |
18 | render() {
19 | const { username, online, view, error } = this.props;
20 | const shouldShowNav = username && online && !error;
21 |
22 | return (
23 | shouldShowNav && (
24 | <>
25 |
34 | {chrome.i18n.getMessage("popupBrowseAllText")}
35 |
36 |
37 | ‧
38 |
39 |
48 | {chrome.i18n.getMessage("popupAddURLText")}
49 |
50 | >
51 | )
52 | );
53 | }
54 |
55 | handleNavLinkClick = e => {
56 | e.preventDefault();
57 | this.props.updateView(e.target.dataset.location);
58 | };
59 |
60 | handleKeydown = e => {
61 | // ⌥ + 1
62 | if (e.keyCode === 49 && e.altKey) {
63 | e.preventDefault();
64 | this.props.updateView("all");
65 | }
66 | // ⌥ + 2
67 | else if (e.keyCode === 50 && e.altKey) {
68 | e.preventDefault();
69 | this.props.updateView("add");
70 | }
71 | };
72 | }
73 |
74 | Nav.propTypes = {
75 | username: PropTypes.string.isRequired,
76 | online: PropTypes.bool.isRequired,
77 | view: PropTypes.string.isRequired,
78 | error: PropTypes.bool.isRequired,
79 | updateView: PropTypes.func.isRequired,
80 | getInitialView: PropTypes.func.isRequired
81 | };
82 |
83 | const mapStateToProps = state => ({
84 | username: state.user.username,
85 | online: state.online,
86 | view: state.view,
87 | error: state.error
88 | });
89 |
90 | const mapDispatchToProps = dispatch => {
91 | return {
92 | updateView: location => dispatch(updateView(location)),
93 | getInitialView: () => dispatch(getInitialView())
94 | };
95 | };
96 |
97 | export default connect(
98 | mapStateToProps,
99 | mapDispatchToProps
100 | )(Nav);
101 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Nav/__tests__/Nav.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import { Nav } from "../Nav";
5 |
6 | describe("", () => {
7 | const defaultProps = {
8 | username: "pinbuddy",
9 | online: true,
10 | view: "all",
11 | error: false,
12 | updateView: jest.fn(),
13 | getInitialView: jest.fn()
14 | };
15 |
16 | it("should render correctly", () => {
17 | const tree = shallow();
18 | expect(tree).toMatchSnapshot();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Nav/__tests__/__snapshots__/Nav.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render correctly 1`] = `
4 | ShallowWrapper {
5 | Symbol(enzyme.__root__): [Circular],
6 | Symbol(enzyme.__unrendered__): ,
26 | Symbol(enzyme.__renderer__): Object {
27 | "batchedUpdates": [Function],
28 | "getNode": [Function],
29 | "render": [Function],
30 | "simulateError": [Function],
31 | "simulateEvent": [Function],
32 | "unmount": [Function],
33 | },
34 | Symbol(enzyme.__node__): Object {
35 | "instance": null,
36 | "key": undefined,
37 | "nodeType": "function",
38 | "props": Object {
39 | "children": Array [
40 |
47 | popupBrowseAllText
48 | ,
49 |
52 | ‧
53 | ,
54 |
61 | popupAddURLText
62 | ,
63 | ],
64 | },
65 | "ref": null,
66 | "rendered": Array [
67 | Object {
68 | "instance": null,
69 | "key": undefined,
70 | "nodeType": "host",
71 | "props": Object {
72 | "children": "popupBrowseAllText",
73 | "className": "nav nav--active",
74 | "data-location": "all",
75 | "href": "all",
76 | "onClick": [Function],
77 | "title": "popupBrowseAllTitle (alt + 1)",
78 | },
79 | "ref": null,
80 | "rendered": "popupBrowseAllText",
81 | "type": "a",
82 | },
83 | Object {
84 | "instance": null,
85 | "key": undefined,
86 | "nodeType": "host",
87 | "props": Object {
88 | "children": "‧",
89 | "className": "nav__spacer",
90 | },
91 | "ref": null,
92 | "rendered": "‧",
93 | "type": "span",
94 | },
95 | Object {
96 | "instance": null,
97 | "key": undefined,
98 | "nodeType": "host",
99 | "props": Object {
100 | "children": "popupAddURLText",
101 | "className": "nav",
102 | "data-location": "add",
103 | "href": "add",
104 | "onClick": [Function],
105 | "title": "popupAddURLTitle (alt + 2)",
106 | },
107 | "ref": null,
108 | "rendered": "popupAddURLText",
109 | "type": "a",
110 | },
111 | ],
112 | "type": Symbol(react.fragment),
113 | },
114 | Symbol(enzyme.__nodes__): Array [
115 | Object {
116 | "instance": null,
117 | "key": undefined,
118 | "nodeType": "function",
119 | "props": Object {
120 | "children": Array [
121 |
128 | popupBrowseAllText
129 | ,
130 |
133 | ‧
134 | ,
135 |
142 | popupAddURLText
143 | ,
144 | ],
145 | },
146 | "ref": null,
147 | "rendered": Array [
148 | Object {
149 | "instance": null,
150 | "key": undefined,
151 | "nodeType": "host",
152 | "props": Object {
153 | "children": "popupBrowseAllText",
154 | "className": "nav nav--active",
155 | "data-location": "all",
156 | "href": "all",
157 | "onClick": [Function],
158 | "title": "popupBrowseAllTitle (alt + 1)",
159 | },
160 | "ref": null,
161 | "rendered": "popupBrowseAllText",
162 | "type": "a",
163 | },
164 | Object {
165 | "instance": null,
166 | "key": undefined,
167 | "nodeType": "host",
168 | "props": Object {
169 | "children": "‧",
170 | "className": "nav__spacer",
171 | },
172 | "ref": null,
173 | "rendered": "‧",
174 | "type": "span",
175 | },
176 | Object {
177 | "instance": null,
178 | "key": undefined,
179 | "nodeType": "host",
180 | "props": Object {
181 | "children": "popupAddURLText",
182 | "className": "nav",
183 | "data-location": "add",
184 | "href": "add",
185 | "onClick": [Function],
186 | "title": "popupAddURLTitle (alt + 2)",
187 | },
188 | "ref": null,
189 | "rendered": "popupAddURLText",
190 | "type": "a",
191 | },
192 | ],
193 | "type": Symbol(react.fragment),
194 | },
195 | ],
196 | Symbol(enzyme.__options__): Object {
197 | "adapter": ReactSixteenAdapter {
198 | "options": Object {
199 | "enableComponentDidUpdateOnSetState": true,
200 | "lifecycles": Object {
201 | "componentDidUpdate": Object {
202 | "onSetState": true,
203 | },
204 | "getDerivedStateFromProps": true,
205 | "getSnapshotBeforeUpdate": true,
206 | "setState": Object {
207 | "skipsComponentDidUpdateOnNullish": true,
208 | },
209 | },
210 | },
211 | },
212 | },
213 | }
214 | `;
215 |
--------------------------------------------------------------------------------
/src/popup/components/App/components/Nav/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Nav";
2 |
--------------------------------------------------------------------------------
/src/popup/components/App/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./App";
2 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | PinBuddy — Popup
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/popup/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 | import { createStore, applyMiddleware } from "redux";
4 | import { Provider } from "react-redux";
5 | import thunk from "redux-thunk";
6 |
7 | import App from "./components/App";
8 | import reducers from "redux-popup/reducers";
9 | import "theme/index.css";
10 |
11 | const store = createStore(reducers, applyMiddleware(thunk));
12 |
13 | render(
14 |
15 |
16 | ,
17 | document.getElementById("root")
18 | );
19 |
--------------------------------------------------------------------------------
/src/popup/redux/actions/error.js:
--------------------------------------------------------------------------------
1 | export const errorShowAction = () => {
2 | return {
3 | type: "ERROR_SHOW"
4 | };
5 | };
6 |
7 | export const errorHideAction = () => {
8 | return {
9 | type: "ERROR_HIDE"
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/popup/redux/actions/loading.js:
--------------------------------------------------------------------------------
1 | export const loadingShowAction = () => {
2 | return {
3 | type: "LOADING_SHOW"
4 | };
5 | };
6 |
7 | export const loadingHideAction = () => {
8 | return {
9 | type: "LOADING_HIDE"
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/popup/redux/actions/online.js:
--------------------------------------------------------------------------------
1 | export const checkConnection = online => {
2 | return {
3 | type: "CHECK_ONLINE",
4 | online
5 | };
6 | };
7 |
8 | export const wentOnline = () => {
9 | return {
10 | type: "WENT_ONLINE"
11 | };
12 | };
13 |
14 | export const wentOffline = () => {
15 | return {
16 | type: "WENT_OFFLINE"
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/popup/redux/actions/posts.js:
--------------------------------------------------------------------------------
1 | import { loadingShowAction, loadingHideAction } from "./loading";
2 | import { errorShowAction } from "./error";
3 |
4 | export const postsGet = () => {
5 | return (dispatch, getState) => {
6 | dispatch(loadingShowAction());
7 |
8 | const { username, token } = getState().user;
9 |
10 | chrome.storage.local.get(["posts", "postsFetched"], result => {
11 | const { posts, postsFetched } = result;
12 |
13 | // Don't fetch new posts more often than 5 minutes
14 | // https://pinboard.in/api/#limits
15 | const now = Date.now();
16 | const outdatedPosts = now - postsFetched > 300000;
17 |
18 | if (outdatedPosts) {
19 | fetch(
20 | `https://api.pinboard.in/v1/posts/all?auth_token=${username}:${token}&format=json`
21 | )
22 | .then(newPostsData => newPostsData.json())
23 | .then(newPosts => {
24 | chrome.storage.local.set(
25 | {
26 | posts: newPosts,
27 | postsFetched: now
28 | },
29 | () => {
30 | dispatch({
31 | type: "POSTS_GET",
32 | posts: newPosts
33 | });
34 | }
35 | );
36 | })
37 | .catch(() => {
38 | dispatch(loadingHideAction());
39 | dispatch(errorShowAction());
40 | })
41 | .finally(() => {
42 | dispatch(loadingHideAction());
43 | });
44 | } else {
45 | dispatch(loadingHideAction());
46 | dispatch({
47 | type: "POSTS_GET",
48 | posts
49 | });
50 | }
51 | });
52 | };
53 | };
54 |
55 | export const postsDelete = (href, message, contextMessage) => {
56 | return (dispatch, getState) => {
57 | const { username, token } = getState().user;
58 | const { posts } = getState();
59 |
60 | const newState = posts.filter(post => post.href !== href);
61 |
62 | fetch(
63 | `https://api.pinboard.in/v1/posts/delete?auth_token=${username}:${token}&format=json&url=${encodeURIComponent(
64 | href
65 | )}`
66 | )
67 | .then(res => res.json())
68 | .then(resJSON => {
69 | if (resJSON.result_code === "done") {
70 | chrome.storage.local.set({ posts: newState }, () => {
71 | chrome.runtime.sendMessage("check current");
72 | dispatch({
73 | type: "POSTS_DELETE",
74 | href
75 | });
76 | });
77 |
78 | chrome.storage.sync.get(["enableSystemNotifications"], result => {
79 | if (result.enableSystemNotifications) {
80 | chrome.notifications.create({
81 | type: "basic",
82 | iconUrl: "/icons/icon-128.png",
83 | title: chrome.i18n.getMessage("notificationDeleteSucessful"),
84 | message,
85 | contextMessage
86 | });
87 | }
88 | });
89 | } else {
90 | dispatch(errorShowAction());
91 | }
92 | })
93 | .catch(() => {
94 | dispatch(errorShowAction());
95 | });
96 | };
97 | };
98 |
99 | export const postsAdd = postInfo => {
100 | return (dispatch, getState) => {
101 | dispatch(loadingShowAction());
102 |
103 | const { username, token } = getState().user;
104 | const { title, url, description, tags, privatePost, readLater } = postInfo;
105 |
106 | fetch(
107 | `https://api.pinboard.in/v1/posts/add?auth_token=${username}:${token}&format=json&url=${encodeURIComponent(
108 | url
109 | )}&description=${title}&extended=${description}&tags=${tags}&shared=${
110 | privatePost ? "no" : "yes"
111 | }&toread=${readLater ? "yes" : "no"}`
112 | )
113 | .then(res => res.json())
114 | .then(resJSON => {
115 | if (resJSON.result_code === "done") {
116 | const now = new Date();
117 | const yyyy = now.getFullYear();
118 | const mm = now.getMonth() + 1;
119 | const dd = now.getDate();
120 |
121 | const newPost = {
122 | href: url,
123 | description: title,
124 | extended: description,
125 | time: `${yyyy}-${mm}-${dd}`,
126 | shared: privatePost ? "no" : "yes",
127 | toread: readLater ? "yes" : "no",
128 | tags: tags,
129 | hash: Math.random().toString()
130 | };
131 |
132 | chrome.storage.local.get(["posts"], result => {
133 | const currentPosts = result.posts.filter(post => post.href !== url);
134 | const newState = [newPost, ...currentPosts];
135 |
136 | chrome.storage.local.set({ posts: newState }, () => {
137 | chrome.runtime.sendMessage("check current");
138 |
139 | chrome.storage.sync.get(["enableSystemNotifications"], result => {
140 | if (result.enableSystemNotifications) {
141 | chrome.notifications.create(
142 | {
143 | type: "basic",
144 | iconUrl: "/icons/icon-128.png",
145 | title: chrome.i18n.getMessage(
146 | "notificationSaveSucessful"
147 | ),
148 | message: title,
149 | contextMessage: description
150 | },
151 | () => {
152 | window.close();
153 | }
154 | );
155 | } else {
156 | window.close();
157 | }
158 | });
159 | });
160 | });
161 | } else {
162 | dispatch(errorShowAction());
163 | }
164 | })
165 | .catch(() => {
166 | dispatch(errorShowAction());
167 | })
168 | .finally(() => {
169 | dispatch(loadingHideAction());
170 | });
171 | };
172 | };
173 |
--------------------------------------------------------------------------------
/src/popup/redux/actions/user.js:
--------------------------------------------------------------------------------
1 | export const getUserInfo = () => {
2 | return dispatch => {
3 | chrome.storage.local.get(["username", "token"], userInfo => {
4 | if (userInfo.username && userInfo.token) {
5 | dispatch({
6 | type: "GET_USER_INFO",
7 | userInfo: userInfo
8 | });
9 | }
10 | });
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/src/popup/redux/actions/view.js:
--------------------------------------------------------------------------------
1 | export const updateView = location => {
2 | return {
3 | type: "UPDATE_VIEW",
4 | location
5 | };
6 | };
7 |
8 | export const getInitialView = () => {
9 | return dispatch => {
10 | chrome.storage.sync.get(["defaultView"], result => {
11 | if (result.defaultView) {
12 | dispatch(updateView(result.defaultView));
13 | }
14 | });
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/error.js:
--------------------------------------------------------------------------------
1 | const reducerError = (state = false, action) => {
2 | if (action.type === "ERROR_SHOW") {
3 | return true;
4 | }
5 |
6 | if (action.type === "ERROR_HIDE") {
7 | return false;
8 | }
9 |
10 | return state;
11 | };
12 |
13 | export default reducerError;
14 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import user from "./user";
3 | import online from "./online";
4 | import view from "./view";
5 | import posts from "./posts";
6 | import loading from "./loading";
7 | import error from "./error";
8 |
9 | export default combineReducers({
10 | user,
11 | online,
12 | view,
13 | posts,
14 | loading,
15 | error
16 | });
17 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/loading.js:
--------------------------------------------------------------------------------
1 | const reducerLoading = (state = false, action) => {
2 | if (action.type === "LOADING_SHOW") {
3 | return true;
4 | }
5 |
6 | if (action.type === "LOADING_HIDE") {
7 | return false;
8 | }
9 |
10 | return state;
11 | };
12 |
13 | export default reducerLoading;
14 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/online.js:
--------------------------------------------------------------------------------
1 | const reducerOffline = (state = true, action) => {
2 | if (action.type === "CHECK_ONLINE") {
3 | return action.online;
4 | } else if (action.type === "WENT_ONLINE") {
5 | return true;
6 | } else if (action.type === "WENT_OFFLINE") {
7 | return false;
8 | }
9 |
10 | return state;
11 | };
12 |
13 | export default reducerOffline;
14 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/posts.js:
--------------------------------------------------------------------------------
1 | const initialState = [];
2 |
3 | const reducerPosts = (state = initialState, action) => {
4 | if (action.type === "POSTS_GET") {
5 | return action.posts;
6 | }
7 |
8 | if (action.type === "POSTS_DELETE") {
9 | const newState = state.filter(post => {
10 | return post.href !== action.href;
11 | });
12 | return newState;
13 | }
14 |
15 | return state;
16 | };
17 |
18 | export default reducerPosts;
19 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/user.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | username: "",
3 | token: ""
4 | };
5 |
6 | const reducerAuth = (state = initialState, action) => {
7 | if (action.type === "GET_USER_INFO") {
8 | return {
9 | ...state,
10 | ...action.userInfo
11 | };
12 | }
13 |
14 | return state;
15 | };
16 |
17 | export default reducerAuth;
18 |
--------------------------------------------------------------------------------
/src/popup/redux/reducers/view.js:
--------------------------------------------------------------------------------
1 | const reducerView = (state = "all", action) => {
2 | if (action.type === "UPDATE_VIEW") {
3 | return action.location;
4 | }
5 |
6 | return state;
7 | };
8 |
9 | export default reducerView;
10 |
--------------------------------------------------------------------------------
/src/theme/Button/Button.css:
--------------------------------------------------------------------------------
1 | .button {
2 | margin-bottom: var(--lh);
3 | font: inherit;
4 | line-height: calc(var(--lh) * 1.5);
5 | padding: 0 var(--lh);
6 | border: 0;
7 | background-color: var(--color-primary);
8 | color: #fff;
9 | border-radius: var(--border-radius);
10 | cursor: pointer;
11 | font-weight: 400;
12 | }
13 |
14 | .button--secondary {
15 | background-color: var(--color-secondary);
16 | }
17 |
18 | .button:disabled {
19 | background-color: var(--color-gray);
20 | cursor: not-allowed;
21 | }
22 |
--------------------------------------------------------------------------------
/src/theme/Button/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import "./Button.css";
4 |
5 | const Button = ({ type, disabled, t, title, onClick, secondary }) => (
6 |
15 | );
16 |
17 | Button.propTypes = {
18 | t: PropTypes.string.isRequired,
19 | type: PropTypes.string,
20 | title: PropTypes.string,
21 | disabled: PropTypes.bool,
22 | onClick: PropTypes.func,
23 | secondary: PropTypes.bool
24 | };
25 |
26 | Button.defaultProps = {
27 | type: "button",
28 | title: null,
29 | disabled: false,
30 | onClick: null,
31 | secondary: false
32 | };
33 |
34 | export default Button;
35 |
--------------------------------------------------------------------------------
/src/theme/Button/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Button";
2 |
--------------------------------------------------------------------------------
/src/theme/Checkbox/Checkbox.css:
--------------------------------------------------------------------------------
1 | @keyframes checkboxPop {
2 | 0% {
3 | opacity: 0;
4 | transform: scale(0) rotate(-45deg);
5 | }
6 |
7 | 80% {
8 | opacity: .5;
9 | transform: scale(1.5) rotate(-45deg);
10 | }
11 |
12 | 100% {
13 | opacity: 1;
14 | transform: rotate(-45deg);
15 | }
16 | }
17 |
18 | .checkbox {
19 | margin-bottom: var(--lh);
20 | }
21 |
22 | .checkbox__input {
23 | position: absolute;
24 | opacity: 0;
25 | }
26 |
27 | .checkbox__label {
28 | position: relative;
29 | display: block;
30 | padding-left: var(--lh);
31 | }
32 |
33 | .checkbox__label::before,
34 | .checkbox__label::after {
35 | position: absolute;
36 | content: '';
37 | }
38 |
39 | .checkbox label::before {
40 | height: 16px;
41 | width: 16px;
42 | border: 1px solid var(--color-gray);
43 | left: 0;
44 | top: calc(var(--lh) / 2 - 8px);
45 | border-radius: var(--border-radius);
46 | }
47 |
48 | .checkbox__label::after {
49 | height: 4px;
50 | width: 6px;
51 | border-left: 2px solid var(--color-text);
52 | border-bottom: 2px solid var(--color-text);
53 | left: 5px;
54 | top: calc(var(--lh) / 2 - 3px);
55 | animation-name: checkboxPop;
56 | animation-duration: var(--animation-duration);
57 | animation-fill-mode: both;
58 | }
59 |
60 | .checkbox__input + .checkbox__label::after {
61 | content: none;
62 | }
63 |
64 | .checkbox__input:checked + .checkbox__label::after {
65 | content: '';
66 | }
67 |
68 | .checkbox__input:focus + .checkbox__label::before {
69 | outline: -webkit-focus-ring-color auto 5px;
70 | }
71 |
--------------------------------------------------------------------------------
/src/theme/Checkbox/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Checkbox.css";
5 |
6 | const Checkbox = ({ id, label, onChange, checked }) => (
7 |
8 |
15 |
18 |
19 | );
20 |
21 | Checkbox.propTypes = {
22 | id: PropTypes.string.isRequired,
23 | label: PropTypes.string.isRequired,
24 | checked: PropTypes.bool.isRequired,
25 | onChange: PropTypes.func.isRequired
26 | };
27 |
28 | export default Checkbox;
29 |
--------------------------------------------------------------------------------
/src/theme/Checkbox/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Checkbox";
2 |
--------------------------------------------------------------------------------
/src/theme/Error/Error.css:
--------------------------------------------------------------------------------
1 | .error {
2 | margin-bottom: var(--lh);
3 | color: var(--color-error);
4 | }
5 |
--------------------------------------------------------------------------------
/src/theme/Error/Error.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Error.css";
5 |
6 | const Error = ({ innerHTML, t }) => {
7 | return innerHTML ? (
8 |
9 | ) : (
10 | {t}
11 | );
12 | };
13 |
14 | Error.propTypes = {
15 | t: PropTypes.string.isRequired,
16 | innerHTML: PropTypes.bool.isRequired
17 | };
18 |
19 | Error.defaultProps = {
20 | innerHTML: false
21 | };
22 |
23 | export default Error;
24 |
--------------------------------------------------------------------------------
/src/theme/Error/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Error";
2 |
--------------------------------------------------------------------------------
/src/theme/Input/Input.css:
--------------------------------------------------------------------------------
1 | .input {
2 | position: relative;
3 | }
4 |
5 | .input__label {
6 | /* font-size: .875rem; */
7 | }
8 |
9 | .input__input {
10 | border: 1px solid var(--color-gray);
11 | display: block;
12 | width: 100%;
13 | font: inherit;
14 | line-height: calc(var(--lh) * 1.5 - 2px);
15 | padding: 0 calc(var(--lh) / 2);
16 | margin-bottom: var(--lh);
17 | border-radius: var(--border-radius);
18 | }
19 |
20 | .input__input:invalid {
21 | border: 1px solid var(--color-error);
22 | }
23 |
24 | .input__message {
25 | display: none;
26 | position: absolute;
27 | top: 0;
28 | right: 0;
29 | }
30 |
31 | .input__input:invalid + .input__message {
32 | display: block;
33 | color: var(--color-error);
34 | }
35 |
--------------------------------------------------------------------------------
/src/theme/Input/Input.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Input.css";
5 |
6 | const Input = React.forwardRef(
7 | (
8 | {
9 | id,
10 | label,
11 | onChange,
12 | value,
13 | placeholder,
14 | type,
15 | autoComplete,
16 | autoCorrect,
17 | autoCapitalize,
18 | spellCheck,
19 | autoFocus,
20 | pattern,
21 | invalidMessage
22 | },
23 | ref
24 | ) => (
25 |
26 |
29 |
30 |
45 |
46 | {invalidMessage &&
{invalidMessage}
}
47 |
48 | )
49 | );
50 |
51 | Input.propTypes = {
52 | id: PropTypes.string.isRequired,
53 | label: PropTypes.string.isRequired,
54 | type: PropTypes.string,
55 | onChange: PropTypes.func.isRequired,
56 | autoComplete: PropTypes.string,
57 | autoCorrect: PropTypes.string,
58 | autoCapitalize: PropTypes.string,
59 | spellCheck: PropTypes.string,
60 | autoFocus: PropTypes.bool,
61 | pattern: PropTypes.string,
62 | invalidMessage: PropTypes.string
63 | };
64 |
65 | Input.defaultProps = {
66 | type: "text",
67 | autoComplete: null,
68 | autoCorrect: null,
69 | autoCapitalize: null,
70 | spellCheck: null,
71 | autoFocus: null,
72 | pattern: null,
73 | invalidMessage: null
74 | };
75 |
76 | export default Input;
77 |
--------------------------------------------------------------------------------
/src/theme/Input/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Input";
2 |
--------------------------------------------------------------------------------
/src/theme/Link/Link.css:
--------------------------------------------------------------------------------
1 | .link {
2 | color: var(--color-primary);
3 | }
4 |
--------------------------------------------------------------------------------
/src/theme/Link/Link.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Link.css";
5 |
6 | const Link = ({ t, url, title, blank }) => (
7 |
14 | {t}
15 |
16 | );
17 |
18 | Link.propTypes = {
19 | t: PropTypes.string.isRequired,
20 | url: PropTypes.string.isRequired,
21 | title: PropTypes.string.isRequired,
22 | blank: PropTypes.bool
23 | };
24 |
25 | Link.defaultProps = {
26 | blank: false
27 | };
28 |
29 | export default Link;
30 |
--------------------------------------------------------------------------------
/src/theme/Link/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Link";
2 |
--------------------------------------------------------------------------------
/src/theme/Paragraph/Paragraph.css:
--------------------------------------------------------------------------------
1 | .paragraph {
2 | margin-bottom: var(--lh);
3 | }
4 |
--------------------------------------------------------------------------------
/src/theme/Paragraph/Paragraph.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Paragraph.css";
5 |
6 | const Paragraph = ({ innerHTML, t }) => {
7 | return innerHTML ? (
8 |
9 | ) : (
10 | {t}
11 | );
12 | };
13 |
14 | Paragraph.propTypes = {
15 | t: PropTypes.string.isRequired,
16 | innerHTML: PropTypes.bool
17 | };
18 |
19 | Paragraph.defaultProps = {
20 | innerHTML: false
21 | };
22 |
23 | export default Paragraph;
24 |
--------------------------------------------------------------------------------
/src/theme/Paragraph/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Paragraph";
2 |
--------------------------------------------------------------------------------
/src/theme/Quote/Quote.css:
--------------------------------------------------------------------------------
1 | .quote {
2 | margin-bottom: var(--lh);
3 | border: 1px solid var(--color-gray-light);
4 | padding: calc(var(--lh) - 1px);
5 | font-style: italic;
6 | }
7 |
8 | .quote::before,
9 | .quote::after {
10 | content: '"';
11 | }
12 |
--------------------------------------------------------------------------------
/src/theme/Quote/Quote.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Quote.css";
5 |
6 | const Quote = ({ t }) => {t}
;
7 |
8 | Quote.propTypes = {
9 | t: PropTypes.string.isRequired
10 | };
11 |
12 | export default Quote;
13 |
--------------------------------------------------------------------------------
/src/theme/Quote/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Quote";
2 |
--------------------------------------------------------------------------------
/src/theme/Select/Select.css:
--------------------------------------------------------------------------------
1 | .select {
2 | margin-bottom: var(--lh);
3 | position: relative;
4 | }
5 |
6 | .select::after {
7 | position: absolute;
8 | right: calc(var(--lh) / 2);
9 | bottom: calc(var(--lh) / 2);
10 | content: '';
11 | width: 0;
12 | height: 0;
13 | border-left: calc(var(--lh) / 4) solid transparent;
14 | border-right: calc(var(--lh) / 4) solid transparent;
15 | border-top: calc(var(--lh) / 4) solid var(--color-text);
16 | transform: translateY(-50%) scale(.5);
17 | }
18 |
19 | .select__label {
20 | display: block;
21 | }
22 |
23 | .select__select {
24 | display: block;
25 | width: 100%;
26 | height: calc(var(--lh) * 1.5);
27 | padding: 0 calc(var(--lh) / 2);
28 | font: inherit;
29 | border: 1px solid var(--color-gray);
30 | -webkit-appearance: none;
31 | border-radius: var(--border-radius);
32 | background-color: #fff;
33 | }
34 |
--------------------------------------------------------------------------------
/src/theme/Select/Select.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Select.css";
5 |
6 | const Select = ({ id, label, onChange, options, selected }) => (
7 |
8 |
11 |
23 |
24 | );
25 |
26 | Select.propTypes = {
27 | id: PropTypes.string.isRequired,
28 | label: PropTypes.string.isRequired,
29 | onChange: PropTypes.func.isRequired,
30 | options: PropTypes.array.isRequired,
31 | selected: PropTypes.string.isRequired
32 | };
33 |
34 | export default Select;
35 |
--------------------------------------------------------------------------------
/src/theme/Select/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Select";
2 |
--------------------------------------------------------------------------------
/src/theme/Title/Title.css:
--------------------------------------------------------------------------------
1 | .title {
2 | margin-bottom: var(--lh);
3 | font-size: var(--fz);
4 | font-weight: 700;
5 | }
6 |
--------------------------------------------------------------------------------
/src/theme/Title/Title.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Title.css";
5 |
6 | const Title = ({ t }) => {t}
;
7 |
8 | Title.propTypes = {
9 | t: PropTypes.string.isRequired
10 | };
11 |
12 | export default Title;
13 |
--------------------------------------------------------------------------------
/src/theme/Title/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Title";
2 |
--------------------------------------------------------------------------------
/src/theme/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | box-sizing: border-box;
4 | padding: 0;
5 | }
6 |
7 | :root {
8 | --fz: 1rem;
9 | --lh: 1.733333333rem;
10 | --color-text: #222;
11 | --color-primary: #00f;
12 | --color-secondary: #b41;
13 | --color-error: #f00;
14 | --color-gray: #aaa;
15 | --color-gray-light: #ddd;
16 | --border-radius: 3px;
17 | --animation-duration: 200ms;
18 | }
19 |
20 | html {
21 | font-size: 93.75%;
22 | }
23 |
24 | body {
25 | color: var(--color-text);
26 | font-size: var(--fz);
27 | font-family: -apple-system,BlinkMacSystemFont,segoe ui,Helvetica,Arial,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol;
28 | line-height: var(--lh);
29 |
30 | /* background-image: linear-gradient(rgba(50, 50, 50, .15) 1px, transparent 1px);
31 | background-size: 100% 26px; */
32 | }
33 |
34 | a {
35 | color: var(--color-primary);
36 | }
37 |
38 | hr {
39 | display: block;
40 | height: var(--lh);
41 | border: 0;
42 | box-shadow: 0 -1px 0 var(--color-gray);
43 | }
44 |
45 | ul {
46 | margin-bottom: var(--lh);
47 | list-style: none;
48 | }
49 |
50 | code {
51 | color: var(--color-secondary);
52 | font: inherit;
53 | font-family: SFMono-Regular,Consolas,liberation mono,Menlo,Courier,monospace;
54 | }
55 |
56 | @keyframes fadeIn {
57 | 0% {
58 | opacity: 0;
59 | }
60 |
61 | 100% {
62 | opacity: 1;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/theme/index.js:
--------------------------------------------------------------------------------
1 | export { default as Button } from "./Button";
2 | export { default as Checkbox } from "./Checkbox";
3 | export { default as Error } from "./Error";
4 | export { default as Input } from "./Input";
5 | export { default as Link } from "./Link";
6 | export { default as Paragraph } from "./Paragraph";
7 | export { default as Quote } from "./Quote";
8 | export { default as Select } from "./Select";
9 | export { default as Title } from "./Title";
10 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const CopyWebpackPlugin = require("copy-webpack-plugin");
4 | const CleanWebpackPlugin = require("clean-webpack-plugin");
5 |
6 | module.exports = {
7 | entry: {
8 | "background/background": "./src/background/",
9 | "options/options": "./src/options/",
10 | "popup/popup": "./src/popup/"
11 | },
12 | module: {
13 | rules: [
14 | {
15 | enforce: "pre",
16 | test: /\.js$/,
17 | exclude: /node_modules/,
18 | loader: "eslint-loader"
19 | },
20 | {
21 | test: /\.js$/,
22 | exclude: /node_modules/,
23 | loader: "babel-loader"
24 | },
25 | {
26 | test: /\.css$/,
27 | use: ["style-loader", "css-loader"]
28 | }
29 | ]
30 | },
31 | resolve: {
32 | alias: {
33 | theme: path.resolve(__dirname, "src/theme/"),
34 | "redux-popup": path.resolve(__dirname, "src/popup/redux/"),
35 | "redux-options": path.resolve(__dirname, "src/options/redux/")
36 | }
37 | },
38 | plugins: [
39 | new CleanWebpackPlugin(["dist", "pinbuddy*.zip"]),
40 | new HtmlWebpackPlugin({
41 | filename: "popup/popup.html",
42 | chunks: ["popup/popup"],
43 | template: "./src/popup/index.html"
44 | }),
45 | new HtmlWebpackPlugin({
46 | filename: "options/options.html",
47 | chunks: ["options/options"],
48 | template: "./src/options/index.html"
49 | }),
50 | new CopyWebpackPlugin([
51 | {
52 | from: "./src/manifest.json"
53 | },
54 | {
55 | from: "./src/icons/.",
56 | to: "./icons"
57 | },
58 | {
59 | from: "./src/_locales/",
60 | to: "./_locales"
61 | }
62 | ])
63 | ],
64 | devtool: "cheap-module-source-map"
65 | };
66 |
--------------------------------------------------------------------------------