├── README.md
├── package.json
├── public
├── backpack.png
├── favicon.ico
├── favicon3.ico
├── index.html
├── manifest.json
└── preview.png
├── src
├── Components
│ ├── Buttons
│ │ ├── AddButton.css
│ │ ├── AddButton.js
│ │ ├── FilterButton.css
│ │ ├── FilterButton.js
│ │ ├── GetButton.css
│ │ ├── GetButton.js
│ │ ├── MoreButton.js
│ │ └── test.js
│ ├── ContentBox
│ │ ├── ContentBox.css
│ │ ├── ContentBox.js
│ │ └── test.js
│ ├── Filters
│ │ ├── Filters.css
│ │ ├── Filters.js
│ │ ├── Search
│ │ │ ├── Search.css
│ │ │ ├── Search.js
│ │ │ └── test.js
│ │ └── test.js
│ ├── Header
│ │ ├── Header.css
│ │ ├── Header.js
│ │ └── test.js
│ ├── Home
│ │ ├── Home.css
│ │ ├── Home.js
│ │ ├── __snapshots__
│ │ │ └── test.js.snap
│ │ └── test.js
│ ├── Product
│ │ ├── Info
│ │ │ ├── Info.css
│ │ │ ├── Info.js
│ │ │ ├── Tag
│ │ │ │ ├── Tag.css
│ │ │ │ ├── Tag.js
│ │ │ │ └── test.js
│ │ │ ├── __snapshots__
│ │ │ │ └── test.js.snap
│ │ │ └── test.js
│ │ ├── PreviewImage
│ │ │ ├── PreviewImage.css
│ │ │ ├── PreviewImage.js
│ │ │ └── test.js
│ │ ├── Product.css
│ │ ├── Product.js
│ │ ├── Voting
│ │ │ ├── Voting.css
│ │ │ ├── Voting.js
│ │ │ └── test.js
│ │ ├── __snapshots__
│ │ │ └── test.js.snap
│ │ └── test.js
│ ├── Routes
│ │ └── Routes.js
│ ├── Signup
│ │ ├── Signup.css
│ │ ├── Signup.js
│ │ ├── __snapshots__
│ │ │ └── test.js.snap
│ │ └── test.js
│ ├── Submit
│ │ ├── Form
│ │ │ ├── Form.css
│ │ │ ├── Form.js
│ │ │ └── test.js
│ │ ├── Submit.css
│ │ ├── Submit.js
│ │ └── test.js
│ └── types.js
├── index.css
├── index.js
├── logo.svg
├── registerServiceWorker.js
└── setupTests.js
└── yarn.lock
/README.md:
--------------------------------------------------------------------------------
1 | # 🏫 Studddent
2 | A curated list of online student discounts for developers and designers. Upvote/Downvote and add your own links 🎒.
3 |
4 | ## Todo:
5 | #### Pending Page (33%)
6 | - ~~Page for viewing pending posts (https://studddent.com/pending)~~
7 | - Integrate passport
8 | - Allow admin to accept/reject/modify submissions from pending page
9 |
10 | #### Tags (50%)
11 | - ~~Tags (Design, Develop, Utility, Bundle)~~
12 | - ~~Filtering~~
13 | - Add tags on submission
14 | - Remove Utility tag, add Education and Other tags
15 |
16 | #### Other (60%)
17 | - ~~npm scripts for dev vs. production~~
18 | - ~~Collect email on submission, send thanks if submission accepted~~
19 | - ~~experiment w/ affiliate links~~ Decided not to use affiliate
20 | - Improve post ordering
21 | - Finish promote page / figure out pricing
22 |
23 |
24 | 
25 |
26 | [Link](https://studddent.com/)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "studddentv2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "enzyme": "^3.3.0",
8 | "enzyme-adapter-react-16": "^1.1.1",
9 | "fetch-mock": "^6.4.3",
10 | "react": "^16.4.0",
11 | "react-addons-css-transition-group": "^15.6.2",
12 | "react-addons-test-utils": "^15.6.2",
13 | "react-dom": "^16.4.0",
14 | "react-router": "^4.2.0",
15 | "react-router-dom": "^4.2.2",
16 | "react-scripts": "1.1.4",
17 | "react-test-renderer": "^16.4.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject"
24 | },
25 | "jest": {
26 | "collectCoverageFrom": [
27 | "src/Components/**/*.js"
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/public/backpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/calumptrck/Studddent/06bb6912decfbc8cf23a3cefdad928fef2aac251/public/backpack.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/calumptrck/Studddent/06bb6912decfbc8cf23a3cefdad928fef2aac251/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon3.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/calumptrck/Studddent/06bb6912decfbc8cf23a3cefdad928fef2aac251/public/favicon3.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Studddent — Best Online Student Discounts
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/calumptrck/Studddent/06bb6912decfbc8cf23a3cefdad928fef2aac251/public/preview.png
--------------------------------------------------------------------------------
/src/Components/Buttons/AddButton.css:
--------------------------------------------------------------------------------
1 | a {
2 | display: inline-block;
3 | }
4 |
5 | .addDiscount {
6 | background-image: linear-gradient(-90deg,rgb(71, 187, 255) 0,rgb(39, 151, 244) 100%);
7 | color: #fff;
8 | font-weight: 400;
9 | margin-top: 20px;
10 | margin-bottom: 50px;
11 | border-radius: 1em;
12 | font-size: 14px;
13 | letter-spacing: 0.6px;
14 | cursor: pointer;
15 | transition: all .24s cubic-bezier(0,.5,.51,1);
16 | }
17 |
18 | .addDiscount:hover {
19 | border-radius: .9em;
20 | }
21 |
22 | .grow2 {
23 | transition: all .24s cubic-bezier(0,.5,.51,1); }
24 | .grow2:hover { transform: scale(1.04); }
25 |
26 | .shadow2 {
27 | box-shadow: 0 12px 25px rgba(0,0,0,0.04);
28 | -moz-box-shadow: 0 12px 25px rgba(0,0,0,0.04);
29 | -webkit-box-shadow: 0 12px 25px rgba(0,0,0,0.04);
30 | }
31 |
32 | .shadow2:hover {
33 | box-shadow: 0 14px 25px rgba(0,0,0,0.08);
34 | -moz-box-shadow: 0 14px 25px rgba(0,0,0,0.08);
35 | -webkit-box-shadow: 0 14px 25px rgba(0,0,0,0.08);
36 | }
--------------------------------------------------------------------------------
/src/Components/Buttons/AddButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './AddButton.css'
3 |
4 | const AddButton = ({children, path}) =>
5 |
6 |
7 |
8 |
9 | export default AddButton
--------------------------------------------------------------------------------
/src/Components/Buttons/FilterButton.css:
--------------------------------------------------------------------------------
1 |
2 | .filterButton {
3 | float: left;
4 | padding-right: 30px;
5 | font-size: 15px;
6 | font-weight: bold;
7 | color: rgb(135, 135, 135);
8 | letter-spacing: 0.6px;
9 | cursor: pointer;
10 | transition: all .26s cubic-bezier(0,.5,.51,1);
11 | background: none;
12 | border: none;
13 | border-radius: 1.5em;
14 | padding: 10px 18px 10px 18px;
15 | letter-spacing: 0.9px;
16 | margin-left: 10px;
17 | }
18 |
19 | .active {
20 | background-image: linear-gradient(-180deg,#FF6A57 0,#FF2F68 100%);
21 | color: #fff;
22 | box-shadow: 0 15px 25px rgba(0,0,0,0.06);
23 | }
24 |
25 | .filterButton:hover {
26 | background-image: linear-gradient(-180deg,#FF6A57 0,#FF2F68 100%);
27 | color: #fff;
28 | box-shadow: 0 15px 25px rgba(0,0,0,0.06);
29 | transform: scale(1.06);
30 | }
31 |
32 | button:focus {outline:0;}
33 |
34 | @media screen and (max-width: 992px) {
35 | .filters {
36 | margin-left: 15px;
37 | }
38 |
39 | .filterButton {
40 | margin-left: 0px;
41 | margin-right: 15px;
42 | }
43 |
44 | .filters > :last-child {margin-right: 0 !important; }
45 | }
46 |
47 | @media screen and (max-width: 537px) {
48 | .filters {
49 | margin-left: 0px;
50 |
51 | }
52 | .filterButton {
53 | margin-right: 0px;
54 | font-size: 12px;
55 | padding: 10px 14px 10px 14px;
56 | }
57 | }
--------------------------------------------------------------------------------
/src/Components/Buttons/FilterButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './FilterButton.css'
3 |
4 | const FilterButton = ({children, id, className, onClick}) =>
5 |
6 |
7 | export default FilterButton
--------------------------------------------------------------------------------
/src/Components/Buttons/GetButton.css:
--------------------------------------------------------------------------------
1 | a {
2 | display: inline-block;
3 | }
4 |
5 | .getButton {
6 | background-color: #fff;
7 | border: solid 2px rgb(39, 151, 244);
8 | font-family: Roboto, sans-serif;
9 | color: rgb(39, 151, 244);
10 | font-weight: 600;
11 | margin-top: 20px;
12 | border-radius: .8em;
13 | padding: 7px 35px 7px 35px;
14 | font-size: 17px;
15 | letter-spacing: 0.9px;
16 | cursor: pointer;
17 | transition: all .24s cubic-bezier(0,.5,.51,1) padding 3s;
18 | display: inline-block;
19 | backface-visibility: hidden;
20 | }
21 |
22 | .getButton:hover {
23 | border: none;
24 | background-image: linear-gradient(-90deg,rgb(71, 187, 255) 0,rgb(39, 151, 244) 100%);
25 | color: #fff;
26 | padding: 9px 24px 9px 24px;
27 | }
28 |
29 | .grow2 {
30 | transition: all .24s cubic-bezier(0,.5,.51,1); }
31 | .grow2:hover { transform: scale(1.08); }
32 |
33 | .shadow2 {
34 | box-shadow: 0 12px 25px rgba(0,0,0,0.04);
35 | -moz-box-shadow: 0 12px 25px rgba(0,0,0,0.04);
36 | -webkit-box-shadow: 0 12px 25px rgba(0,0,0,0.04);
37 | }
38 |
39 | .shadow2:hover {
40 | box-shadow: 0 14px 25px rgba(0,0,0,0.08);
41 | -moz-box-shadow: 0 14px 25px rgba(0,0,0,0.08);
42 | -webkit-box-shadow: 0 14px 25px rgba(0,0,0,0.08);
43 | }
44 |
45 | @media screen and (max-width: 535px) {
46 | .getButton {
47 | padding: 7px 20px 7px 20px;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/Components/Buttons/GetButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './GetButton.css'
3 |
4 | const GetButton = ({url, children}) =>
5 |
6 |
7 |
8 |
9 | export default GetButton
--------------------------------------------------------------------------------
/src/Components/Buttons/MoreButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './GetButton.css'
3 |
4 | const MoreButton = ({children, showMore, type}) =>
5 |
6 |
7 | export default MoreButton
--------------------------------------------------------------------------------
/src/Components/Buttons/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import AddButton from './AddButton';
5 | import FilterButton from './FilterButton';
6 | import GetButton from './GetButton';
7 | import MoreButton from './MoreButton';
8 |
9 | const filterClick = jest.fn();
10 |
11 |
12 | describe('Buttons Render', () => {
13 | it('MoreButton renders without crashing', () => {
14 | const div = document.createElement('div');
15 | ReactDOM.render(MoreButton, div);
16 | ReactDOM.unmountComponentAtNode(div);
17 | });
18 |
19 | it('AddButton renders without crashing', () => {
20 | const div = document.createElement('div');
21 | ReactDOM.render(AddButton, div);
22 | ReactDOM.unmountComponentAtNode(div);
23 | });
24 |
25 | it('FilterButton renders without crashing', () => {
26 | const div = document.createElement('div');
27 | ReactDOM.render(FilterButton, div);
28 | ReactDOM.unmountComponentAtNode(div);
29 | });
30 |
31 | it('GetButton renders without crashing', () => {
32 | const div = document.createElement('div');
33 | ReactDOM.render(GetButton, div);
34 | ReactDOM.unmountComponentAtNode(div);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/Components/ContentBox/ContentBox.css:
--------------------------------------------------------------------------------
1 | .contentBox {
2 | margin-top: 50px;
3 | width: 100%;
4 | display: block;
5 | border-radius: 1em;
6 | }
--------------------------------------------------------------------------------
/src/Components/ContentBox/ContentBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './ContentBox.css'
3 |
4 | const ContentBox = ({children}) =>
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 |
12 | export default ContentBox;
--------------------------------------------------------------------------------
/src/Components/ContentBox/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import ContentBox from './ContentBox';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
11 |
--------------------------------------------------------------------------------
/src/Components/Filters/Filters.css:
--------------------------------------------------------------------------------
1 | .filtersContainer {
2 | margin-top: 40px;
3 | display: block;
4 | height: 120px;
5 | }
6 |
7 | .filters {
8 | margin-top: 10px;
9 | float: right;
10 | }
11 |
12 |
13 | input {
14 | float: left;
15 | }
16 |
17 | @media screen and (max-width: 992px) {
18 | .filters {
19 | float: none;
20 | margin-left: 5px;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Components/Filters/Filters.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Filters.css'
3 |
4 | import Search from './Search/Search';
5 | import FilterButton from '../Buttons/FilterButton';
6 |
7 | const Filters = ({buttons, buttonClick, searchUpdate}) =>
8 |
9 |
10 |
11 | {buttons.map((button) =>
12 | buttonClick(button.id)}
16 | >{button.label.toUpperCase()}
17 |
18 | )}
19 |
20 |
21 |
22 |
23 | export default Filters;
--------------------------------------------------------------------------------
/src/Components/Filters/Search/Search.css:
--------------------------------------------------------------------------------
1 | input {
2 | height: 55px;
3 | width: 40%;
4 | padding: 1em;
5 | padding-left: 1.5em;
6 | margin-bottom: 10px;
7 | background-color: #F6F6F8;
8 | color: #450b0b;
9 | font-family: Roboto, Arial, sans-serif;
10 | font-size: 16px;
11 | border: 0;
12 | outline: none;
13 | border-radius: 2em;
14 | transition: all 150ms;
15 | border: solid 3px rgb(237, 237, 237);
16 | display: block;
17 | }
18 |
19 | input:focus {
20 | border: solid 3px #FF6A57;
21 | }
22 |
23 | @media screen and (max-width: 992px) {
24 | input {
25 | width: 100%;
26 | margin-bottom: 30px;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Components/Filters/Search/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Search.css'
3 |
4 | const Search = ({searchUpdate}) =>
5 |
6 |
7 |
8 | export default Search;
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Components/Filters/Search/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Search from './Search';
4 |
5 | // To Test - Number of callbacks after entering a word
6 |
7 | const searcUpdate = jest.fn();
8 |
9 | it('renders without crashing', () => {
10 | const div = document.createElement('div');
11 | ReactDOM.render(, div);
12 | ReactDOM.unmountComponentAtNode(div);
13 | });
14 |
15 |
--------------------------------------------------------------------------------
/src/Components/Filters/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Filters from './Filters';
4 | import renderer from 'react-test-renderer';
5 | import FilterButton from '../Buttons/FilterButton';
6 | import Enzyme from 'enzyme'
7 | import { shallow, mount, render } from 'enzyme'
8 | import Adapter from 'enzyme-adapter-react-16'
9 |
10 | Enzyme.configure({ adapter: new Adapter() })
11 |
12 |
13 | const buttons = [
14 | {
15 | id: 0,
16 | label: "ALL",
17 | active: true,
18 | },
19 | {
20 | id: 1,
21 | label: "DESIGN",
22 | active: false,
23 | },
24 | {
25 | id: 2,
26 | label: "DEVELOPMENT",
27 | active: false,
28 | },
29 | {
30 | id: 3,
31 | label: "UTILITY",
32 | active: false,
33 | }
34 | ]
35 |
36 | const onButtonClick = jest.fn()
37 |
38 | const buttonClick = (id) => {
39 | for (let i = 0; i < buttons.length; i++) {
40 | if (i === id) {
41 | buttons[i].active = true;
42 | } else {
43 | buttons[i].active = false;
44 | }
45 | }
46 | }
47 |
48 | describe('Filters', () => {
49 | it('renders without crashing', () => {
50 | const div = document.createElement('div');
51 | ReactDOM.render(, div);
52 | ReactDOM.unmountComponentAtNode(div);
53 | });
54 |
55 | test('Has one active button', () => {
56 | const expected = [{
57 | id: 0,
58 | label: "ALL",
59 | active: false,
60 | },
61 | {
62 | id: 1,
63 | label: "DESIGN",
64 | active: true,
65 | }]
66 | const wrapper = shallow()
67 | wrapper.find('.default').first().simulate('click')
68 | expect(buttons).toEqual(expect.arrayContaining(expected));
69 | });
70 | });
--------------------------------------------------------------------------------
/src/Components/Header/Header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | background-color: #fff;
4 | padding: 70px 0px 50px 0px;
5 | box-shadow: 0 18px 40px 0 hsla(0, 0%, 40%, 0.02);
6 | display: block;
7 | min-height: 170px;
8 | }
9 |
10 | .a:hover, a:visited, a:link, a:active {
11 | text-decoration: none;
12 | }
13 |
14 | p {
15 | margin-top: 1px;
16 | }
17 |
18 | .title {
19 | font-family: Work Sans;
20 | font-weight: 600;
21 | font-size: 36px;
22 | }
23 |
24 | a .title {
25 | color: #292B2C;
26 | }
27 |
28 | .desc {
29 | font-family: Work Sans;
30 | font-weight: 400;
31 | font-size: 19px;
32 | line-height: 22px;
33 | }
--------------------------------------------------------------------------------
/src/Components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Header.css'
3 |
4 |
5 | const Header = ({buttonText, children}) =>
6 |
7 |
8 | {children}
9 |
10 |
11 |
12 |
13 | export default Header;
14 |
--------------------------------------------------------------------------------
/src/Components/Header/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Header from './Header';
4 |
5 | const searcUpdate = jest.fn();
6 |
7 | it('renders without crashing', () => {
8 | const div = document.createElement('div');
9 | ReactDOM.render(, div);
10 | ReactDOM.unmountComponentAtNode(div);
11 | });
12 |
13 |
--------------------------------------------------------------------------------
/src/Components/Home/Home.css:
--------------------------------------------------------------------------------
1 | .viewMore {
2 | height: 80px;
3 | text-align: center;
4 | }
5 |
6 | .addTask-appear {
7 | opacity: 0.01;
8 | padding-bottom: 1px;
9 | margin-bottom: 15px;
10 | }
11 |
12 | .addTask-appear.addTask-appear-active {
13 | opacity: 1;
14 | padding-bottom: 18px;
15 | margin-bottom: 14px;
16 | transition: 700ms;
17 | }
18 |
19 | .addTask-enter {
20 | opacity: 0.01;
21 | width: 100%;
22 | height: 120px;
23 | background-color: #fff;
24 | border-radius: .75em;
25 | box-shadow: 0 2px 30px 0 hsla(0, 0%, 40%, 0.04);
26 | display: block;
27 | margin-bottom: 40px;
28 | }
29 |
30 | .addTask-enter.addTask-enter-active {
31 | opacity: 1;
32 | width: 100%;
33 | height: 150px;
34 | background-color: #fff;
35 | border-radius: .75em;
36 | box-shadow: 0 8px 30px 0 hsla(0, 0%, 40%, 0.08);
37 | display: block;
38 | margin-bottom: 40px;
39 | }
--------------------------------------------------------------------------------
/src/Components/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import axios from 'axios';
3 |
4 | import Header from '../Header/Header';
5 | import Signup from '../Signup/Signup';
6 | import ContentBox from '../ContentBox/ContentBox';
7 | import Filters from '../Filters/Filters';
8 | import Product from '../Product/Product';
9 | import MoreButton from '../Buttons/MoreButton';
10 | import AddButton from '../Buttons/AddButton';
11 | import './Home.css';
12 |
13 | const PATH_BASE = 'https://studddent.com/api';
14 |
15 | const isSearched = searchTerm => item =>
16 | item.name.toLowerCase().includes(searchTerm.toLowerCase());
17 |
18 | const vote = (id, type) => {
19 | axios.post(`${PATH_BASE}/votes/${type}`, {
20 | postid: id,
21 | postK: process.env.REACT_APP_PH,
22 | }).catch((error) => console.log(error));
23 | }
24 |
25 | class Home extends Component {
26 | _isMounted = false; // Independent from state
27 | constructor() {
28 | super();
29 | this.state = {
30 | count: 7,
31 | searchTerm: '',
32 | buttons: [
33 | {
34 | id: 0,
35 | label: "All",
36 | active: false,
37 | },
38 | {
39 | id: 1,
40 | label: "Design",
41 | active: false,
42 | },
43 | {
44 | id: 2,
45 | label: "Development",
46 | active: false,
47 | },
48 | {
49 | id: 3,
50 | label: "Utility",
51 | active: false,
52 | },
53 | ],
54 | products: null,
55 | filteredProducts: null,
56 | votes: JSON.parse(localStorage.getItem('votes')) || { up: [], down: [] },
57 | error: null,
58 | }
59 | this.filterButtonClick = this.filterButtonClick.bind(this);
60 | this.upVote = this.upVote.bind(this);
61 | this.downVote = this.downVote.bind(this);
62 | this.fetchTasks = this.fetchTasks.bind(this);
63 | this.showMore = this.showMore.bind(this);
64 | this.onSearchChange = this.onSearchChange.bind(this);
65 | }
66 |
67 | filterButtonClick(id) {
68 | let { products, filteredProducts } = this.state;
69 | const buttons = this.state.buttons
70 | for (let i = 0; i < buttons.length; i++) {
71 | if (i === id) {
72 | document.title = (id > 0)
73 | ? this.state.buttons[id].label+" Student Discounts"
74 | : "Studddent - Best Online Student Discounts.";
75 | buttons[i].active = true;
76 | filteredProducts = products.filter((product) =>
77 | i === 0 || product.tags.indexOf(buttons[i].label.toLowerCase()) >= 0);
78 | this.setState({
79 | filteredProducts,
80 | count: 7,
81 | })
82 | } else {
83 | buttons[i].active = false;
84 | }
85 | }
86 | this.setState({
87 | buttons,
88 | })
89 | }
90 |
91 | upVote(id) {
92 | let { up, down } = this.state.votes;
93 | let products = this.state.products;
94 | let product = products.find(product => product._id === id)
95 | if (up.indexOf(id) < 0) {
96 | if (down.indexOf(id) >= 0) {
97 | down.splice(down.indexOf(id), 1);
98 | product.votes.down--;
99 | product.votes.up++;
100 | vote(id, 5);
101 | } else {
102 | product.votes.up++;
103 | vote(id, 3);
104 | }
105 |
106 | this.setState({
107 | products: [...products, product],
108 | votes: {
109 | up: [...up, id],
110 | down,
111 | }
112 | }, () => localStorage.setItem('votes', JSON.stringify(this.state.votes)))
113 | } else {
114 | up.splice(up.indexOf(id), 1);
115 | product.votes.up--;
116 | vote(id, 2);
117 | this.setState({
118 | products: [...products, product],
119 | votes: {
120 | up,
121 | down,
122 | }
123 | }, () => localStorage.setItem('votes', JSON.stringify(this.state.votes)))
124 | }
125 | }
126 |
127 | downVote(id) {
128 | let { up, down } = this.state.votes;
129 | let products = this.state.products;
130 | let product = products.find(product => product._id === id)
131 | if (down.indexOf(id) < 0) {
132 | if (up.indexOf(id) >= 0) {
133 | up.splice(up.indexOf(id), 1);
134 | product.votes.up--;
135 | product.votes.down++;
136 | vote(id, 0);
137 | } else {
138 | product.votes.down++;
139 | vote(id, 1);
140 | }
141 | this.setState({
142 | products: [...products, product],
143 | votes: {
144 | up,
145 | down: [...down, id]
146 | }
147 | }, () => localStorage.setItem('votes', JSON.stringify(this.state.votes)))
148 | } else {
149 | down.splice(down.indexOf(id), 1);
150 | product.votes.down--;
151 | vote(id, 4);
152 | this.setState({
153 | products: [...products, product],
154 | votes: {
155 | up,
156 | down,
157 | }
158 | }, () => localStorage.setItem('votes', JSON.stringify(this.state.votes)))
159 | }
160 | }
161 |
162 | showMore() {
163 | this.setState((prevState) => ({
164 | count: prevState.count + 7,
165 | })
166 | )
167 | }
168 |
169 | fetchTasks() {
170 | const {pageId} = this.props;
171 | axios(`${PATH_BASE}/links`)
172 | .then(result => {
173 | this._isMounted && this.setState({ products: result.data, filteredProducts: result.data })
174 | pageId >= 0 && this.filterButtonClick(pageId)
175 | })
176 | .catch(error => this._isMounted && this.setState({ error: error }));
177 | }
178 |
179 | onSearchChange(event) {
180 | this.setState({ searchTerm: event.target.value });
181 | }
182 |
183 | componentDidMount() {
184 | this._isMounted = true;
185 | this.fetchTasks();
186 | }
187 |
188 | componentWillUnmount() {
189 | this._isMounted = false;
190 | }
191 |
192 | render() {
193 | const { buttons, products, votes, count, filteredProducts } = this.state;
194 | return (
195 |
196 |
201 |
202 |
203 | {filteredProducts &&
204 | filteredProducts.filter((post) =>
205 | post.accepted === true)
206 | .filter(isSearched(this.state.searchTerm))
207 | .slice(0, count).map((product) =>
208 |
214 | )}
215 |
216 | {products &&
217 | filteredProducts.filter(isSearched(this.state.searchTerm)).length > count &&
218 |
}
221 |
222 |
223 | {products && }
224 |
225 |
226 | );
227 | }
228 | }
229 |
230 | export default Home;
--------------------------------------------------------------------------------
/src/Components/Home/__snapshots__/test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Home has a valid snapshot 1`] = `
4 |
5 |
37 |
40 |
49 |
52 |
60 |
68 |
76 |
84 |
85 |
86 |
97 |
98 | `;
99 |
--------------------------------------------------------------------------------
/src/Components/Home/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Home from './Home';
4 | import renderer from 'react-test-renderer';
5 | import Enzyme from 'enzyme'
6 | import { shallow, mount, render } from 'enzyme'
7 | import Adapter from 'enzyme-adapter-react-16'
8 |
9 | Enzyme.configure({ adapter: new Adapter() })
10 |
11 | // Render & Snapshot
12 |
13 | describe('Home', () => {
14 | it('renders without crashing', () => {
15 | const div = document.createElement('div');
16 | ReactDOM.render(, div);
17 | ReactDOM.unmountComponentAtNode(div);
18 | });
19 |
20 | test('has a valid snapshot', () => {
21 | const component = renderer.create(
22 | );
23 | let tree = component.toJSON();
24 | expect(tree).toMatchSnapshot();
25 | });
26 |
27 | test('Search Updates State', () => {
28 | const wrap = mount(
29 |
30 | )
31 |
32 | wrap.find('input').simulate('change', {
33 | target: { value: 'Microsoft' }
34 | })
35 |
36 | wrap.update()
37 | expect(wrap.state('searchTerm')).toEqual('Microsoft');
38 | });
39 |
40 | });
41 |
42 | // Backend Integration
43 |
44 | const DELAY_MS = 1000
45 |
46 | const sleep = (ms) => {
47 | return new Promise(resolve => setTimeout(resolve, ms));
48 | }
49 |
50 | const fetchTasks = async (url) => {
51 | try {
52 | const response = await fetch(url)
53 | const responseJson = await response.json()
54 | return responseJson
55 | }
56 | catch (e) {
57 | console.log(`fetchResponseJson failed:`, e)
58 | }
59 | }
60 |
61 | describe(`API Updates State`, () => {
62 | test(`on Home componentDidMount`, async () => {
63 | const wrapper = shallow()
64 | await wrapper.instance().componentDidMount()
65 | await sleep(DELAY_MS+100)
66 | expect(wrapper.state('products')).not.toContain(null)
67 | expect(wrapper.state('error')).toEqual(null)
68 |
69 | // Force update to sync component with state
70 | wrapper.update()
71 |
72 | })
73 |
74 | test(`on vote`, async () => {
75 | const wrapper = mount()
76 | await wrapper.instance().componentDidMount()
77 | await sleep(DELAY_MS+100)
78 | const expectedCount1 = wrapper.state('products')[0].votes.down;
79 | const expectedCount2 = expectedCount1+1;
80 | await wrapper.instance().upVote('5a9c3bfc1f50583c497f0ee9')
81 | await sleep(250);
82 | await wrapper.instance().downVote('5a9c3bfc1f50583c497f0ee9')
83 | await sleep(250);
84 | expect(wrapper.state('products')[0].votes.down).toEqual(expectedCount2)
85 | await wrapper.instance().upVote('5a9c3bfc1f50583c497f0ee9')
86 | await sleep(250);
87 | await wrapper.instance().upVote('5a9c3bfc1f50583c497f0ee9')
88 | await sleep(250);
89 | expect(wrapper.state('products')[0].votes.down).toEqual(expectedCount1)
90 |
91 | })
92 |
93 | })
--------------------------------------------------------------------------------
/src/Components/Product/Info/Info.css:
--------------------------------------------------------------------------------
1 | .info {
2 | float: left;
3 | background-color: rgb(208, 208, 208);
4 | background-color: #fff;
5 | border-right: solid 2px #eee;
6 | border-left: solid 2px #eee;
7 | height: 100%;
8 | width: 60%;
9 | padding: 20px;
10 | }
11 |
12 | h1 {
13 | font-size: 23px;
14 | font-family: Roboto, sans-serif;
15 | font-weight: 500;
16 | }
17 |
18 | .promo {
19 | font-family: Roboto, sans-serif;
20 | margin-top: -3px;
21 | font-size: 15px;
22 | color: rgb(0, 124, 196);
23 | font-weight: 500;
24 | }
25 |
26 | .infoText {
27 | height: 80%;
28 | }
29 |
30 | @media screen and (max-width: 768px) {
31 | h1 {
32 | font-size: 21px;
33 | }
34 |
35 | .info {
36 | width: 55%;
37 | }
38 | }
39 |
40 | @media screen and (max-width: 535px) {
41 | .info {
42 | border-left: none;
43 | border-top: solid 2px #eee;
44 | border-bottom-left-radius: 0.75em;
45 | width: 70%;
46 | height: 150px;
47 | }
48 |
49 | h1 {
50 | font-size: 18px;
51 | }
52 | }
--------------------------------------------------------------------------------
/src/Components/Product/Info/Info.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Info.css'
3 |
4 | import Tag from './Tag/Tag';
5 |
6 | const Info = ({ title, promo, tags }) =>
7 |
8 |
9 |
{title}
10 |
{promo}
11 |
12 |
13 | {tags.map((tag) =>
14 |
15 | )}
16 |
17 |
18 |
19 | export default Info
--------------------------------------------------------------------------------
/src/Components/Product/Info/Tag/Tag.css:
--------------------------------------------------------------------------------
1 |
2 | .tags {
3 | display: block;
4 | }
5 |
6 | .tag {
7 | color: rgb(138, 138, 138);
8 | margin-right: 12px;
9 | padding: 6px 14px 6px 14px;
10 | border: solid 2px #eee;
11 | border-radius: .75em;
12 | display: inline;
13 | font-size: 13px;
14 | font-weight: 600;
15 | letter-spacing: 0.8px;
16 | user-select: none;
17 | }
18 |
19 | @media screen and (max-width: 535px) {
20 | .tags {
21 | margin-left: -4px;
22 | }
23 |
24 | .tag {
25 | font-size: 11px;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Components/Product/Info/Tag/Tag.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Tag.css'
3 |
4 | const Tag = ({ label }) =>
5 | {label}
6 |
7 | export default Tag
--------------------------------------------------------------------------------
/src/Components/Product/Info/Tag/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Tag from './Tag';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
11 |
--------------------------------------------------------------------------------
/src/Components/Product/Info/__snapshots__/test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Home has a valid snapshot 1`] = `
4 |
7 |
15 |
18 |
21 | DESIGN
22 |
23 |
26 | DEVELOPMENT
27 |
28 |
31 | UTILITY
32 |
33 |
34 |
35 | `;
36 |
--------------------------------------------------------------------------------
/src/Components/Product/Info/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Info from './Info';
4 | import renderer from 'react-test-renderer';
5 |
6 | const tags = ['design', 'development', 'utility']
7 |
8 | describe('Home', () => {
9 | it('renders without crashing', () => {
10 | const div = document.createElement('div');
11 | ReactDOM.render(, div);
12 | ReactDOM.unmountComponentAtNode(div);
13 | });
14 |
15 | test('has a valid snapshot', () => {
16 | const component = renderer.create(
17 | );
18 | let tree = component.toJSON();
19 | expect(tree).toMatchSnapshot();
20 | });
21 | });
--------------------------------------------------------------------------------
/src/Components/Product/PreviewImage/PreviewImage.css:
--------------------------------------------------------------------------------
1 |
2 | .previewImage {
3 | float: left;
4 | height: 100%;
5 | width: 20%;
6 | background-color: white;
7 | border-top-left-radius: 0.75em;
8 | border-bottom-left-radius: 0.75em;
9 | padding: 2px;
10 | white-space: nowrap;
11 | text-align: center; margin: 0 0;
12 | }
13 |
14 | img {
15 | vertical-align: middle;
16 | width: auto;
17 | max-width: 80%;
18 | max-height: 50px;
19 |
20 | }
21 |
22 | .helper { /* Vertical Center Thumbnails */
23 | display: inline-block;
24 | height: 100%;
25 | vertical-align: middle;
26 | }
27 |
28 | @media screen and (max-width: 535px) {
29 | .previewImage {
30 | float: none;
31 | height: 100px;
32 | width: 100%;
33 | border-bottom-left-radius: 0em;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Components/Product/PreviewImage/PreviewImage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './PreviewImage.css'
3 |
4 | const PreviewImage = ({ src }) =>
5 |
6 |
7 |

8 |
9 |
10 | export default PreviewImage
--------------------------------------------------------------------------------
/src/Components/Product/PreviewImage/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import PreviewImage from './PreviewImage';
4 |
5 |
6 | it('renders without crashing', () => {
7 | const div = document.createElement('div');
8 | ReactDOM.render(, div);
9 | ReactDOM.unmountComponentAtNode(div);
10 | });
11 |
12 |
--------------------------------------------------------------------------------
/src/Components/Product/Product.css:
--------------------------------------------------------------------------------
1 | .productBox {
2 | width: 100%;
3 | height: 150px;
4 | background-color: #fff;
5 | border-radius: .75em;
6 | box-shadow: 0 8px 30px 0 hsla(0, 0%, 40%, 0.08);
7 | display: block;
8 | margin-bottom: 40px;
9 | transition: all .24s cubic-bezier(0,.5,.51,1);
10 | }
11 |
12 | .productBox:hover {
13 | transform: scale(1.00);
14 | }
15 |
16 |
17 | @media screen and (max-width: 768px) {
18 | .productBox {
19 | height: 160px;
20 | }
21 | }
22 |
23 | @media screen and (max-width: 535px) {
24 | .productBox {
25 | height: 250px;
26 | margin-bottom: 50px;
27 | }
28 |
29 | .tags {
30 | margin-left: -4px;
31 | }
32 |
33 | .tag {
34 | font-size: 11px;
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/src/Components/Product/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Product.css'
3 |
4 | import PreviewImage from './PreviewImage/PreviewImage';
5 | import Info from './Info/Info';
6 | import Voting from './Voting/Voting';
7 |
8 | const Product = ({product, upVote, downVote, votes}) => {
9 | let upIndex = votes.up.indexOf(product._id);
10 | let downIndex = votes.down.indexOf(product._id);
11 | return (
12 |
13 |
14 |
15 |
= 0 ? "up" : downIndex >= 0 ? "down" : "neutral"} />
16 |
17 | );
18 | }
19 |
20 |
21 | export default Product;
--------------------------------------------------------------------------------
/src/Components/Product/Voting/Voting.css:
--------------------------------------------------------------------------------
1 | .voting {
2 | height: 100%;
3 | width: 20%;
4 | float: left;
5 | background-color: #fff;
6 | border-top-right-radius: 0.75em;
7 | border-bottom-right-radius: 0.75em;
8 | text-align: center;
9 | }
10 |
11 | .score {
12 | font-family: Roboto, sans-serif;
13 | font-size: 18px;
14 | font-weight: 600;
15 | margin-top: 12px;
16 | width: 100%;
17 | margin-top: 30px;
18 | margin-left: 30px;
19 |
20 | }
21 |
22 | .score a:visited, a:link, a:active {
23 | text-decoration: none;
24 | color: #ababab;
25 | }
26 |
27 | .score p {
28 | float: left;
29 | padding: 0px 15px 0px 15px;
30 | color: #717171;
31 | }
32 |
33 | .upThumb, .downThumb {
34 | cursor: pointer;
35 | }
36 |
37 | .score .upThumb, .downThumb {
38 | float: left;
39 | color: rgb(187, 187, 187);
40 | font-size: 24px;
41 | }
42 |
43 |
44 | .visit {
45 | height: 60px;
46 | }
47 |
48 | .upThumb .up {
49 | color: #0099F0;
50 | }
51 |
52 | .downThumb .down {
53 | color: #FF2F68;
54 | }
55 |
56 | @media screen and (max-width: 991px) {
57 | .score {
58 | margin-left: 20px;
59 | }
60 | }
61 |
62 | @media screen and (max-width: 768px) {
63 | .voting {
64 | width: 25%;
65 | }
66 | .score {
67 | margin-left: 14px;
68 | }
69 | }
70 |
71 | @media screen and (max-width: 535px) {
72 | .voting {
73 | border-top-right-radius: 0em;
74 | border-top: solid 2px #eee;
75 | width: 30%;
76 | height: 150px;
77 | }
78 |
79 | .score {
80 | margin-left: 7%;
81 | }
82 |
83 | .score p {
84 | padding: 0px 9% 0px 9%;
85 | }
86 | }
--------------------------------------------------------------------------------
/src/Components/Product/Voting/Voting.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Voting.css'
3 |
4 | import GetButton from '../../Buttons/GetButton'
5 |
6 | const Voting = ({ product, upVote, downVote, voteClass }) =>
7 |
8 |
9 | VIEW
10 |
11 |
12 |
upVote(product._id)} href="#">
13 |
{product.votes.up - product.votes.down}
14 |
downVote(product._id)}>
15 |
16 |
17 |
18 | export default Voting
--------------------------------------------------------------------------------
/src/Components/Product/Voting/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Voting from './Voting';
4 |
5 | const product = {
6 | "_id": "5a9c3bfc1f50583c497f0ee9",
7 | "name": "Student Developer Pack",
8 | "email": "calum@calum.co",
9 | "url": "https://education.github.com/pack",
10 | "feature": "Free While You're a Student",
11 | "__v": 0,
12 | "image": "https://dwa5x7aod66zk.cloudfront.net/assets/sdp-backpack-a64038716bf134f45e809ff86b9611fb97e41bbd2ccfa3181da73cf164d3c200.png",
13 | "votes": {
14 | "down": 22,
15 | "up": 391
16 | },
17 | "tags": [
18 | "development",
19 | "bundle"
20 | ],
21 | "features": [
22 | "GitHub Developer Account",
23 | "$50 DigitalOcean Credit",
24 | "Free Namecheap Domain"
25 | ],
26 | "accepted": true
27 | }
28 |
29 | const upVote = jest.fn()
30 | const downVote = jest.fn()
31 | const vote = jest.fn()
32 |
33 | it('renders without crashing', () => {
34 | const div = document.createElement('div');
35 | ReactDOM.render(, div);
36 | ReactDOM.unmountComponentAtNode(div);
37 | });
38 |
39 |
--------------------------------------------------------------------------------
/src/Components/Product/__snapshots__/test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Product has a valid snapshot 1`] = `
4 |
7 |
10 |
13 |

17 |
18 |
21 |
24 |
25 | Student Developer Pack
26 |
27 |
30 | Free While You're a Student
31 |
32 |
33 |
36 |
39 | DEVELOPMENT
40 |
41 |
44 | BUNDLE
45 |
46 |
47 |
48 |
51 |
65 |
68 |
73 |
77 |
78 |
79 | 369
80 |
81 |
85 |
89 |
90 |
91 |
92 |
93 | `;
94 |
--------------------------------------------------------------------------------
/src/Components/Product/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Product from './Product';
4 | import renderer from 'react-test-renderer';
5 |
6 | const product = {
7 | "_id": "5a9c3bfc1f50583c497f0ee9",
8 | "name": "Student Developer Pack",
9 | "email": "calum@calum.co",
10 | "url": "https://education.github.com/pack",
11 | "feature": "Free While You're a Student",
12 | "__v": 0,
13 | "image": "https://dwa5x7aod66zk.cloudfront.net/assets/sdp-backpack-a64038716bf134f45e809ff86b9611fb97e41bbd2ccfa3181da73cf164d3c200.png",
14 | "votes": {
15 | "down": 22,
16 | "up": 391
17 | },
18 | "tags": [
19 | "development",
20 | "bundle"
21 | ],
22 | "features": [
23 | "GitHub Developer Account",
24 | "$50 DigitalOcean Credit",
25 | "Free Namecheap Domain"
26 | ],
27 | "accepted": true
28 | }
29 |
30 | const votes = {
31 | up: [],
32 | down: [],
33 | }
34 |
35 | describe('Product', () => {
36 | it('renders without crashing', () => {
37 | const div = document.createElement('div');
38 | ReactDOM.render(, div);
39 | ReactDOM.unmountComponentAtNode(div);
40 | });
41 |
42 | test('has a valid snapshot', () => {
43 | const component = renderer.create(
44 | );
45 | let tree = component.toJSON();
46 | expect(tree).toMatchSnapshot();
47 | });
48 | });
--------------------------------------------------------------------------------
/src/Components/Routes/Routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route } from 'react-router';
3 | import Home from '../Home/Home';
4 | import Submit from '../Submit/Submit';
5 | import {Filters} from '../types';
6 |
7 | const Routes = () =>
8 |
9 | }/>
10 | } />
11 | } />
12 | } />
13 | } />
14 | } />
15 |
16 |
17 |
18 | export default Routes;
19 |
--------------------------------------------------------------------------------
/src/Components/Signup/Signup.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .signup {
4 | margin-top: 70px;
5 | display:inline-block;
6 | }
7 |
8 | .mailtitle {
9 | font-family: Work Sans, sans-serif;
10 | font-weight: 600;
11 | font-size: 25px;
12 | }
13 |
14 | .maildesc {
15 | font-family: Work Sans;
16 | font-weight: 400;
17 | font-size: 16px;
18 | line-height: 22px;
19 | }
20 |
21 | .subscribe {
22 | background-color: rgb(243, 228, 93);
23 | margin-bottom: 20px;
24 | border-radius: 0.75em;
25 | font-size: 15px;
26 | }
27 |
28 | .subscribe:hover {
29 | background-color: rgb(255, 235, 57), 228, 93;
30 | }
31 |
32 | .response {
33 | display: none;
34 | }
35 |
36 | .hiddenInput {
37 | position: absolute;
38 | left: -5000px;
39 | }
40 |
41 | .required {
42 | border-radius: 1em;
43 | }
44 |
45 | .grow2 {
46 | transition: all .24s cubic-bezier(0,.5,.51,1); }
47 | .grow2:hover { transform: scale(1.08); }
48 |
49 | .shadow2 {
50 | box-shadow: 0 12px 25px rgba(0,0,0,0.04);
51 | -moz-box-shadow: 0 12px 25px rgba(0,0,0,0.04);
52 | -webkit-box-shadow: 0 12px 25px rgba(0,0,0,0.04);
53 | }
54 |
55 | .shadow2:hover {
56 | box-shadow: 0 14px 25px rgba(0,0,0,0.08);
57 | -moz-box-shadow: 0 14px 25px rgba(0,0,0,0.08);
58 | -webkit-box-shadow: 0 14px 25px rgba(0,0,0,0.08);
59 | }
60 |
61 | .clear {
62 | height: 50px;
63 | margin-top: -40px;
64 | margin-bottom: 80px;
65 | }
--------------------------------------------------------------------------------
/src/Components/Signup/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Signup.css'
3 |
4 | import MoreButton from '../Buttons/MoreButton'
5 |
6 | const Signup = ({ url, children }) =>
7 |
8 |
Interested in this project?
9 |
Sign up below to receive notifications for trending discounts, and updates on the project.
10 |
31 |
32 |
33 | export default Signup
--------------------------------------------------------------------------------
/src/Components/Signup/__snapshots__/test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Home has a valid snapshot 1`] = `
4 |
7 |
10 | Interested in this project?
11 |
12 |
15 | Sign up below to receive notifications for trending discounts, and updates on the project.
16 |
17 |
85 |
86 | `;
87 |
--------------------------------------------------------------------------------
/src/Components/Signup/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Signup from './Signup';
4 | import renderer from 'react-test-renderer';
5 |
6 | const tags = ['design', 'development', 'utility']
7 |
8 | describe('Home', () => {
9 | it('renders without crashing', () => {
10 | const div = document.createElement('div');
11 | ReactDOM.render(, div);
12 | ReactDOM.unmountComponentAtNode(div);
13 | });
14 |
15 | test('has a valid snapshot', () => {
16 | const component = renderer.create(
17 | );
18 | let tree = component.toJSON();
19 | expect(tree).toMatchSnapshot();
20 | });
21 | });
--------------------------------------------------------------------------------
/src/Components/Submit/Form/Form.css:
--------------------------------------------------------------------------------
1 | .submit {
2 | font-family: Work Sans;
3 | font-weight: 600;
4 | font-size: 36px;
5 | }
6 |
7 | .submitButton {
8 | height: 80px;
9 | text-align: center;
10 | }
--------------------------------------------------------------------------------
/src/Components/Submit/Form/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Form.css'
3 |
4 | import MoreButton from '../../Buttons/MoreButton';
5 |
6 | const Form = ({url, children}) =>
7 |
53 |
54 | export default Form
--------------------------------------------------------------------------------
/src/Components/Submit/Form/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Form from './Form';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
11 |
--------------------------------------------------------------------------------
/src/Components/Submit/Submit.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | a:visited, a:link, a:active {
4 | text-decoration: none;
5 | color: #292B2C;
6 | }
--------------------------------------------------------------------------------
/src/Components/Submit/Submit.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './Submit.css'
3 |
4 | import Header from '../Header/Header'
5 | import ContentBox from '../ContentBox/ContentBox'
6 | import Form from './Form/Form'
7 |
8 | class Submit extends Component {
9 | render() {
10 | return (
11 |
19 | );
20 | }
21 | }
22 |
23 |
24 | export default Submit
--------------------------------------------------------------------------------
/src/Components/Submit/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Submit from './Submit';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
11 |
--------------------------------------------------------------------------------
/src/Components/types.js:
--------------------------------------------------------------------------------
1 | export const Filters = {
2 | All: 0,
3 | Design: 1,
4 | Development: 2,
5 | Utility: 3,
6 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .container {
2 | max-width: 850px;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | font-family: sans-serif;
9 | background-color: #F6F6F8;
10 | }
11 |
12 | .spacer {
13 | height: 100px;
14 | display: block;
15 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import Routes from './Components/Routes/Routes';
5 | import './index.css';
6 |
7 | ReactDOM.render((
8 |
9 |
10 | ) ,document.getElementById('root'));
11 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | const localStorageMock = {
2 | getItem: jest.fn(() => {
3 | return '{ "up":[], "down":[] }'
4 | }),
5 | setItem: jest.fn(),
6 | clear: jest.fn()
7 | };
8 | global.localStorage = localStorageMock;
9 |
10 | const JSONMock = {
11 | parse: jest.fn(),
12 | };
13 |
14 | global.localStorage = localStorageMock;
15 |
--------------------------------------------------------------------------------