├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENCE
├── README.md
├── TODO.md
├── components
├── BottomBar.js
├── FilterIcon.js
├── Footer.js
├── GlobalStyles.js
├── Header.js
├── LinkCard.js
├── LinkIcon.js
├── LinksList.js
├── Loader.js
├── LoadingCard.js
├── MLinkCard.js
├── Meta.js
├── Notification.js
├── PageInfo.js
├── Search.js
├── Sidebar.js
└── Snackbar.js
├── contributing.md
├── hocs
├── ContainerPage.js
├── PublicPage.js
└── SecretPage.js
├── lib
├── analytics.js
├── db.js
└── tags.json
├── next.config.js
├── now.json
├── package.json
├── pages
├── about.js
├── bookmarks.js
├── index.js
├── my-links.js
├── profile.js
├── submit-link.bak.js
└── submit-link.js
├── push-sw.js
├── server.js
├── static
├── favicons
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── manifest.json
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── mstile-70x70.png
│ └── safari-pinned-tab.svg
└── tos.html
├── utils
├── authenticate.js
├── index.js
├── offlineInstaller.js
└── redirect.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | #node
2 | node_modules
3 | .log
4 | .DS_Store
5 | .idea
6 |
7 | #nextjs
8 | .next
9 | out
10 |
11 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at dev@vinaypuppal.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright 2017 VinayPuppal
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/feross/standard) [](https://github.com/prettier/prettier) [](https://pwa-directory.appspot.com/pwas/5676600884461568#t=Linklet%20App&bg=%233f51b5&c=%23ffffff)
2 |
3 | # Linklet
4 |
5 | We've have tons of resources shared daily in our Freecodecamp Hyderabad Whatsapp group on various development topics. There has always been two main complaints
6 |
7 | * It's hard to find the resources shared in the group
8 | * It's only available to our Whatsapp members.
9 |
10 | So I extracted all those resources and created a website that lists all the resources we have shared in the group since our inception. You can also now directly share resources on the website, and search for them using keywords and filter them based on particular date range. We have over 1200 links available on the site right now. Machine Learning, Web Development, Android Development, Python, whatever you're interested in, you'll find resources for it here.
11 |
12 | ## Tech Stack used
13 |
14 | * Frontend - [Next.js](https://github.com/zeit/next.js)
15 | * [Backend API](https://github.com/vinaypuppal/linklet-api) - Node.js ([express.js](expressjs.com))
16 | * Database - [MongoDB](https://www.mongodb.com/)
17 | * Hosted on - [now.sh](https://now.sh)
18 | * Database hosted on - [mlab](http://mlab.com)
19 |
20 | ## Community Ports
21 |
22 | [Android App](https://github.com/M-ZubairAhmed/Linklet-Android) (Under active development) by **[@M-ZubairAhmed](https://github.com/M-ZubairAhmed)**
23 |
24 | ## Contributing
25 |
26 | Please read this [contributing.md](https://github.com/vinaypuppal/linklet-app/blob/master/contributing.md)
27 |
28 | ## Authors
29 |
30 | * VinayPuppal ([@vinaypuppal](https://vinaypuppal.com))
31 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | - [x] Add search Bar
2 | - [x] Add Github Authentication
3 | - [] Allow users to post links with tags
4 | - [x] Allow users to view links posted by them in seperate view
5 | - [x] Implement settings for push notifications
6 | - [x] Show link open count
7 | - [x] Allow users to bookmark links which appear in bookmarks section of app
8 | - [x] Allow users to sort links
9 |
10 |
--------------------------------------------------------------------------------
/components/BottomBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import FaUser from 'react-icons/lib/md/face'
4 | import FaHeart from 'react-icons/lib/fa/bookmark-o'
5 |
6 | const IconPlaceHolder = () => (
7 |
8 |
9 |
10 |
17 |
18 | )
19 |
20 | export default class BottomBar extends React.Component {
21 | constructor (props) {
22 | super(props)
23 | this.state = {
24 | show: true,
25 | interactive: false
26 | }
27 | }
28 | componentDidMount () {
29 | this.setState({
30 | interactive: true
31 | })
32 | const input = document.querySelector('.react-autosuggest__input')
33 | ? document.querySelector('.react-autosuggest__input')
34 | : document.querySelector('input')
35 | if (input) {
36 | input.addEventListener('focus', this.handelFocus.bind(this))
37 | input.addEventListener('blur', this.handelBlur.bind(this))
38 | }
39 | }
40 | componentWillUnMount () {
41 | const input = document.querySelector('.react-autosuggest__input')
42 | ? document.querySelector('.react-autosuggest__input')
43 | : document.querySelector('input')
44 | if (input) {
45 | input.removeEventListener('focus', this.handelFocus.bind(this))
46 | input.removeEventListener('blur', this.handelBlur.bind(this))
47 | }
48 | }
49 | handelFocus () {
50 | this.setState({
51 | show: false
52 | })
53 | }
54 | handelBlur () {
55 | this.setState({
56 | show: true
57 | })
58 | }
59 | render () {
60 | const { url: { pathname } } = this.props
61 | return (
62 |
63 |
64 |
169 |
170 |
274 |
275 | )
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/components/FilterIcon.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
3 |
7 |
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import FaGithub from 'react-icons/lib/fa/github'
2 |
3 | export default () => (
4 |
97 | )
98 |
--------------------------------------------------------------------------------
/components/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
3 |
329 |
330 | )
331 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Meta from './Meta'
3 | import GlobalStyles from './GlobalStyles'
4 | import LinkIcon from './LinkIcon'
5 | import FilterIcon from './FilterIcon'
6 | import BottomBar from './BottomBar'
7 | import NotificationBtn from './Notification'
8 | import FaSpinner from 'react-icons/lib/fa/spinner'
9 |
10 | export default class Header extends React.Component {
11 | constructor (props) {
12 | super(props)
13 | this.state = {
14 | interactive: false
15 | }
16 | }
17 | componentDidMount () {
18 | this.setState({
19 | interactive: true
20 | })
21 | }
22 | render () {
23 | return (
24 |
143 | )
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/components/LinkCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import gen from 'color-generator'
3 | import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'
4 | import format from 'date-fns/format'
5 | import isThisMonth from 'date-fns/is_this_month'
6 | import LazyLoad from 'react-lazyload'
7 | import { truncateString } from '../utils'
8 | import Highlighter from 'react-highlight-words'
9 | import FaHeart from 'react-icons/lib/fa/bookmark'
10 | import FaEye from 'react-icons/lib/fa/eye'
11 |
12 | export default class LinkCard extends React.Component {
13 | constructor (props) {
14 | super(props)
15 | this.state = {
16 | bgColor: 'rgb(128, 102, 1)'
17 | }
18 | }
19 | componentDidMount () {
20 | this.setState({
21 | bgColor: gen(0.99, 0.5).hexString()
22 | })
23 | }
24 | render () {
25 | const { link, query: { search } = {}, user } = this.props
26 | const likedLinkClass =
27 | user && link.bookmarkedBy && ~link.bookmarkedBy.indexOf(user._id)
28 | ? 'liked'
29 | : ''
30 | return (
31 |
32 |
33 |
34 | {link.image && (
35 |
36 |
42 |
43 | )}
44 |
45 |
46 |
47 |
54 |
55 |
56 | {link.description ? (
57 |
62 | ) : (
63 | 'No Description'
64 | )}
65 |
66 |
67 |
138 |
364 |
365 | )
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/components/LinkIcon.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
3 |
7 |
11 |
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/components/LinksList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 | import NProgress from 'nprogress'
4 | import Pagination from 'rc-pagination'
5 | import MLinkCard from './MLinkCard'
6 | // import LinkCard from './LinkCard'
7 | import PageInfo from './PageInfo'
8 | import SearchBar from '../components/Search'
9 | import db from '../lib/db'
10 | import SnackBar from '../components/Snackbar'
11 |
12 | export default class LinksList extends React.Component {
13 | constructor (props) {
14 | super(props)
15 | this.state = {
16 | show: false,
17 | message: ''
18 | }
19 | }
20 | onSanckbarClose () {
21 | console.log('snackbar closed')
22 | this.setState({
23 | show: false,
24 | message: ''
25 | })
26 | }
27 | handelOpen (_id) {
28 | console.log(_id)
29 | db
30 | .incrementView(_id)
31 | .then(({ data }) => {
32 | this.changeRoute()
33 | })
34 | .catch(console.log)
35 | }
36 | handelLike (_id, e) {
37 | console.log(e)
38 | e && e.preventDefault()
39 | console.log(_id)
40 | if (!this.props.user) {
41 | this.setState({
42 | show: true,
43 | message: 'Login to "Bookmark" this link'
44 | })
45 | return
46 | }
47 | NProgress.start()
48 | db
49 | .likeLink(_id)
50 | .then(({ data }) => {
51 | NProgress.done()
52 | if (~data.bookmarkedBy.indexOf(this.props.user._id)) {
53 | this.setState({
54 | show: true,
55 | message: 'Successfully "Bookmarked" this link'
56 | })
57 | }
58 | this.changeRoute()
59 | })
60 | .catch(e => {
61 | NProgress.done()
62 | this.setState({
63 | show: true,
64 | message: 'Some error occured!...'
65 | })
66 | console.log(e)
67 | })
68 | }
69 | handelSort (e) {
70 | console.log(e.target.value)
71 | this.changeRoute(1, true, e.target.value)
72 | }
73 | changeRoute (current, scroll, sort) {
74 | const { url } = this.props
75 | const { query } = url
76 | const start = query && query.start
77 | const end = query && query.end
78 | const search = query && query.search
79 | sort = sort || (query && query.sort) || -1
80 | current =
81 | typeof current === 'undefined' ? (query.page ? query.page : 1) : current
82 | if (start && end) {
83 | if (search) {
84 | Router.push(
85 | `${url.pathname}?start=${start}&end=${end}&page=${current}&search=${search}&sort=${sort}`
86 | )
87 | .then(() => scroll && window.scrollTo(0, 0))
88 | .catch(e => console.log(e))
89 | } else {
90 | Router.push(
91 | `${url.pathname}?start=${start}&end=${end}&page=${current}&sort=${sort}`
92 | )
93 | .then(() => scroll && window.scrollTo(0, 0))
94 | .catch(e => console.log(e))
95 | }
96 | } else {
97 | if (search) {
98 | Router.push(
99 | `${url.pathname}?page=${current}&search=${search}&sort=${sort}`
100 | )
101 | .then(() => scroll && window.scrollTo(0, 0))
102 | .catch(e => console.log(e))
103 | } else {
104 | Router.push(`${url.pathname}?page=${current}&sort=${sort}`)
105 | .then(() => scroll && window.scrollTo(0, 0))
106 | .catch(e => console.log(e))
107 | }
108 | }
109 | }
110 | renderLinks (isMobile, links, user, query) {
111 | // if (isMobile) {
112 | return links.map(link => (
113 |
121 | ))
122 | /* return links.map(link =>
123 |
131 | ) */
132 | }
133 | render () {
134 | const {
135 | data: { links, totalLinks, perPage, page },
136 | url,
137 | user,
138 | isMobile
139 | } = this.props
140 | const { query } = url
141 | return (
142 |
143 |
144 |
151 |
152 | {this.renderLinks(isMobile, links, user, query)}
153 |
154 |
155 |
this.changeRoute(current, true)}
160 | />
161 |
162 |
167 | {this.state.message}
168 |
169 |
213 |
214 | )
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/components/Loader.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
185 |
186 | )
187 |
--------------------------------------------------------------------------------
/components/LoadingCard.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
92 |
93 | )
94 |
--------------------------------------------------------------------------------
/components/MLinkCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { truncateString } from '../utils'
3 | import Highlighter from 'react-highlight-words'
4 | import LazyLoad from 'react-lazyload'
5 | import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'
6 | import format from 'date-fns/format'
7 | import isThisMonth from 'date-fns/is_this_month'
8 | import FaBookmark from 'react-icons/lib/fa/bookmark'
9 | import FaBookmarkO from 'react-icons/lib/fa/bookmark-o'
10 | import FaEye from 'react-icons/lib/fa/eye'
11 | import FaWA from 'react-icons/lib/fa/whatsapp'
12 | import FaExt from 'react-icons/lib/fa/external-link'
13 |
14 | export default class MLinkCard extends React.Component {
15 | render () {
16 | const { link, query: { search } = {}, user } = this.props
17 | const likedLinkClass =
18 | user && link.bookmarkedBy && ~link.bookmarkedBy.indexOf(user._id)
19 | ? 'liked'
20 | : ''
21 | const defaultBg =
22 | 'https://res.cloudinary.com/vinaypuppal/image/upload/v1493986755/diagmonds-light_libvwv.png'
23 | return (
24 |
25 | this.props.handelOpen(link._id)}
27 | className='open'
28 | rel='noopener'
29 | href={link.url}
30 | target='_blank'
31 | >
32 |
33 |
34 |
41 |
42 |
43 | {link.description ? (
44 |
49 | ) : (
50 | 'No Description'
51 | )}
52 |
53 |
54 |
55 | {link.image ? (
56 |
57 |
58 |
64 |
65 |
66 | ) : (
67 |
75 | )}
76 |
77 |
78 |
79 |
80 | {link._creator ? (
81 |
87 |
88 |
94 |
95 | {link._creator.username}
96 |
97 | ) : (
98 |
99 | Added From Whatsapp Group
100 |
101 | )}
102 |
103 | {' '}
104 | -{' '}
105 | {isThisMonth(link.timestamp)
106 | ? distanceInWordsToNow(link.timestamp) + ' ' + 'ago'
107 | : format(link.timestamp, 'MMM, Do YYYY')}
108 |
109 |
110 |
124 |
125 |
126 |
127 | {link.views || 0}
128 |
129 |
130 |
140 |
141 |
269 |
270 | )
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/components/Meta.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 |
3 | export default ({ title }) => (
4 |
5 |
6 |
10 |
11 |
16 |
22 |
28 |
29 |
34 |
35 |
39 |
40 | {title}
41 |
45 |
46 |
47 |
48 |
52 |
53 |
57 |
58 |
59 |
60 |
61 |
62 | )
63 |
--------------------------------------------------------------------------------
/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import fetch from 'isomorphic-unfetch'
3 | import SnackBar from './Snackbar'
4 | import NProgress from 'nprogress'
5 |
6 | import db from '../lib/db'
7 |
8 | export default class Notification extends React.Component {
9 | constructor (props) {
10 | super(props)
11 | this.state = {
12 | checked: false,
13 | show: false,
14 | message: ''
15 | }
16 | }
17 | componentDidMount () {
18 | if (Notification.permission === 'denied') {
19 | console.log('User has blocked push notification.')
20 | return
21 | }
22 | // Get `push notification` subscription
23 | // If `serviceWorker` is registered and ready
24 | if ('serviceWorker' in navigator) {
25 | navigator.serviceWorker.ready.then(registration => {
26 | registration.pushManager
27 | .getSubscription()
28 | .then(subscription => {
29 | // If already access granted, enable push button status
30 | if (subscription) {
31 | this.setState({
32 | checked: true
33 | })
34 | } else {
35 | this.setState({
36 | checked: false
37 | })
38 | }
39 | })
40 | .catch(error => {
41 | console.error('Error occurred while enabling push ', error)
42 | this.setState({
43 | show: true,
44 | message: 'Error occurred while enabling push '
45 | })
46 | })
47 | })
48 | }
49 | }
50 | subscribePush () {
51 | if ('serviceWorker' in navigator) {
52 | navigator.serviceWorker.ready.then(registration => {
53 | if (!registration.pushManager) {
54 | this.setState({
55 | show: true,
56 | message: "Sorry, Push notification isn't supported in your browser."
57 | })
58 | return
59 | }
60 | NProgress.start()
61 | // To subscribe `push notification` from push manager
62 | registration.pushManager
63 | .subscribe({
64 | userVisibleOnly: true // Always show notification when received
65 | })
66 | .then(subscription => {
67 | console.info('Push notification subscribed.')
68 | console.log(subscription)
69 | return this.saveSubscriptionID(subscription)
70 | })
71 | .then(() => {
72 | NProgress.done()
73 | this.setState({
74 | checked: true,
75 | show: true,
76 | message: 'Successfully subscribed to push notifications.'
77 | })
78 | })
79 | .catch(error => {
80 | NProgress.done()
81 | this.setState({
82 | checked: false,
83 | show: true,
84 | message: 'Push notification subscription failed'
85 | })
86 | console.error('Push notification subscription error: ', error)
87 | })
88 | })
89 | } else {
90 | this.setState({
91 | show: true,
92 | message: 'Some scripts are not loaded yet, try again after sometime'
93 | })
94 | }
95 | }
96 | unsubscribePush () {
97 | if ('serviceWorker' in navigator) {
98 | navigator.serviceWorker.ready.then(registration => {
99 | // Get `push subscription`
100 | registration.pushManager
101 | .getSubscription()
102 | .then(subscription => {
103 | // If no `push subscription`, then return
104 | if (!subscription) {
105 | this.setState({
106 | show: true,
107 | message: 'Unable to unregister push notification.'
108 | })
109 | return
110 | }
111 | NProgress.start()
112 | // Unsubscribe `push notification`
113 | subscription
114 | .unsubscribe()
115 | .then(() => {
116 | console.info('Push notification unsubscribed.')
117 | console.log(subscription)
118 | return this.deleteSubscriptionID(subscription)
119 | })
120 | .then(() => {
121 | NProgress.done()
122 | this.setState({
123 | checked: false,
124 | show: true,
125 | message: 'Successfully unsubscribed from push notifications.'
126 | })
127 | })
128 | .catch(function (error) {
129 | NProgress.done()
130 | console.error(error)
131 | this.setState({
132 | show: true,
133 | message: 'Failed to unsubscribe push notification.'
134 | })
135 | })
136 | })
137 | .catch(error => {
138 | console.error(error)
139 | console.error('Failed to unsubscribe push notification.')
140 | this.setState({
141 | show: true,
142 | message: 'Failed to unsubscribe push notification.'
143 | })
144 | })
145 | })
146 | } else {
147 | this.setState({
148 | show: true,
149 | message: 'Some scripts are not loaded yet, try again after sometime'
150 | })
151 | }
152 | }
153 | saveSubscriptionID (subscription) {
154 | var subscriptionId =
155 | subscription.endpoint.split('gcm/send/')[1] ||
156 | subscription.endpoint.split(/\/wpush\/v\d\//)[1]
157 |
158 | console.log('Subscription ID', subscriptionId)
159 | return fetch(`${db.baseUrl}/subscriptions`, {
160 | method: 'POST',
161 | body: JSON.stringify({
162 | subscriptionId: subscriptionId,
163 | subscription: subscription
164 | }),
165 | headers: {
166 | 'Content-Type': 'application/json'
167 | }
168 | })
169 | }
170 | deleteSubscriptionID (subscription) {
171 | var subscriptionId = subscription.endpoint.split('gcm/send/')[1]
172 | return fetch(`${db.baseUrl}/subscriptions/` + subscriptionId, {
173 | method: 'DELETE'
174 | })
175 | }
176 |
177 | handelInput (e) {
178 | console.log('clicked')
179 | if (this.state.checked) {
180 | console.log('unsubscribing')
181 | this.unsubscribePush()
182 | } else {
183 | console.log('subscribing')
184 | this.subscribePush()
185 | }
186 | }
187 | onSanckbarClose () {
188 | console.log('snackbar closed')
189 | this.setState({
190 | show: false,
191 | message: ''
192 | })
193 | }
194 | render () {
195 | return (
196 |
197 |
198 |
203 |
204 |
205 |
210 | {this.state.message}
211 |
212 |
264 |
265 | )
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/components/PageInfo.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import format from 'date-fns/format'
3 |
4 | const renderInfo = (query, totalLinks, url) => {
5 | if (query && query.start && query.end) {
6 | if (query.search) {
7 | return (
8 |
9 | Total: {totalLinks} {' '}
10 | {url.pathname === '/bookmarks' ? 'bookmark/s' : 'link/s'} were found
11 | from
12 |
13 | {' ' +
14 | `${format(Number(query.start), 'MMM, Do YYYY')} to ${format(
15 | Number(query.end),
16 | 'MMM, Do YYYY'
17 | )} ${query.search ? `containing ${query.search} word` : ''}` +
18 | ' '}
19 |
20 | clear
21 |
22 |
23 |
48 |
49 | )
50 | } else {
51 | return (
52 |
53 | Total: {totalLinks} {' '}
54 | {url.pathname === '/bookmarks' ? 'bookmark/s' : 'link/s'} were found
55 | from
56 |
57 | {' ' +
58 | `${format(Number(query.start), 'MMM, Do YYYY')} to ${format(
59 | Number(query.end),
60 | 'MMM, Do YYYY'
61 | )}` +
62 | ' '}
63 |
64 | clear
65 |
66 |
67 |
93 |
94 | )
95 | }
96 | } else {
97 | if (query && query.search) {
98 | return (
99 |
100 | Total: {totalLinks} {' '}
101 | {url.pathname === '/bookmarks' ? 'bookmark/s' : 'link/s'} were found
102 | containing word
103 |
104 | {' ' + query.search + ' '}
105 |
106 | clear
107 |
108 |
109 |
135 |
136 | )
137 | } else {
138 | return (
139 |
140 | Total: {totalLinks} {' '}
141 | {url.pathname === '/bookmarks' ? 'bookmark/s' : 'link/s'} were added
142 | till today.
143 |
156 |
157 | )
158 | }
159 | }
160 | }
161 |
162 | export default ({ query, page, totalLinks, url, handelSort }) => {
163 | const sort = (query && query.sort) || -1
164 | return (
165 |
166 |
{renderInfo(query, totalLinks, url)}
167 |
168 |
Page: {page}
169 |
170 |
Sort By:
171 |
172 | New to Old
173 | Old to New
174 | Views - high to low
175 | Views - low to high
176 | Bookmarks - high to low
177 | Bookmarks - low to high
178 |
179 |
180 |
181 |
239 |
240 | )
241 | }
242 |
--------------------------------------------------------------------------------
/components/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 | import Autosuggest from 'react-autosuggest'
4 | import isMobile from 'ismobilejs'
5 | import fetch from 'isomorphic-unfetch'
6 |
7 | // calculate suggestions for any given input value.
8 | function getSuggestions (value) {
9 | const inputValue = value.trim().toLowerCase()
10 | const inputLength = inputValue.length
11 | return inputLength === 0
12 | ? []
13 | : window.tags.filter(
14 | tag => tag.toLowerCase().slice(0, inputLength) === inputValue
15 | )
16 | }
17 |
18 | // When suggestion is clicked, Autosuggest needs to populate the input field
19 | // based on the clicked suggestion.
20 | function getSuggestionValue (suggestion) {
21 | return suggestion
22 | }
23 |
24 | // render suggestions.
25 | function renderSuggestion (suggestion) {
26 | return {suggestion}
27 | }
28 |
29 | export default class Search extends React.Component {
30 | constructor (props) {
31 | super(props)
32 | this.state = {
33 | suggestions: [],
34 | value: this.props.query
35 | ? this.props.query.search ? this.props.query.search : ''
36 | : ''
37 | }
38 | }
39 | componentDidMount () {
40 | window.tags = [
41 | 'web',
42 | 'javascript',
43 | 'code',
44 | 'programming',
45 | 'learning',
46 | 'development',
47 | 'react',
48 | 'software',
49 | 'apps',
50 | 'css',
51 | 'developers',
52 | 'app'
53 | ]
54 | fetch(
55 | 'https://cdn.rawgit.com/vinaypuppal/linklet-app/aca4b5d1/lib/tags.json'
56 | )
57 | .then(r => r.json())
58 | .then(data => {
59 | window.tags = data
60 | })
61 | }
62 | componentWillReceiveProps (nextProps) {
63 | this.setState({
64 | value: nextProps.query
65 | ? nextProps.query.search ? nextProps.query.search : ''
66 | : ''
67 | })
68 | }
69 | onChange (event, { newValue }) {
70 | this.setState({
71 | value: newValue
72 | })
73 | }
74 | // Autosuggest will call this function every time you need to update suggestions.
75 | onSuggestionsFetchRequested ({ value }) {
76 | this.setState({
77 | suggestions: getSuggestions(value)
78 | })
79 | }
80 |
81 | // Autosuggest will call this function every time users clears input.
82 | onSuggestionsClearRequested () {
83 | this.setState({
84 | suggestions: []
85 | })
86 | }
87 | onKeyDown (e) {
88 | if (e.keyCode === 13) {
89 | this.onSuggestionSelected(e, { suggestionValue: this.state.value })
90 | }
91 | }
92 | // On user selects a suggestion fetch repos
93 | onSuggestionSelected (event, { suggestionValue }) {
94 | if (this.props.query) {
95 | let { start, end } = this.props.query
96 | if (start && end) {
97 | Router.push(
98 | `${this.props.url
99 | .pathname}?start=${start}&end=${end}&search=${suggestionValue}`
100 | )
101 | .then(() => window.scrollTo(0, 0))
102 | .catch(e => console.log(e))
103 | return
104 | }
105 | }
106 | Router.push(`${this.props.url.pathname}?search=${suggestionValue}`)
107 | .then(() => window.scrollTo(0, 0))
108 | .catch(e => console.log(e))
109 | }
110 | render () {
111 | console.log(isMobile.any)
112 | const { suggestions, value } = this.state
113 | // Autosuggest will pass through all these props to the input field.
114 | const inputProps = {
115 | placeholder: 'Type something here and press enter',
116 | value,
117 | onChange: this.onChange.bind(this),
118 | onKeyDown: this.onKeyDown.bind(this)
119 | }
120 | return (
121 |
135 | )
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import format from 'date-fns/format'
2 | import addDays from 'date-fns/add_days'
3 | import Router from 'next/router'
4 |
5 | const changeRoute = (start, end, props) => {
6 | const search = props.url.query
7 | ? props.url.query.search ? props.url.query.search : null
8 | : null
9 | const sort = props.url.query
10 | ? props.url.query.sort ? props.url.query.sort : null
11 | : null
12 | console.log({ start, end, search, sort })
13 | if (search) {
14 | if (sort) {
15 | Router.push(
16 | `${props.url
17 | .pathname}?start=${start}&end=${end}&search=${search}&sort=${sort}`
18 | )
19 | .then(() => window.scrollTo(0, 0))
20 | .catch(e => console.log(e))
21 | } else {
22 | Router.push(
23 | `${props.url.pathname}?start=${start}&end=${end}&search=${search}`
24 | )
25 | .then(() => window.scrollTo(0, 0))
26 | .catch(e => console.log(e))
27 | }
28 | } else {
29 | if (sort) {
30 | Router.push(
31 | `${props.url.pathname}?start=${start}&end=${end}&sort=${sort}`
32 | )
33 | .then(() => window.scrollTo(0, 0))
34 | .catch(e => console.log(e))
35 | } else {
36 | Router.push(`${props.url.pathname}?start=${start}&end=${end}`)
37 | .then(() => window.scrollTo(0, 0))
38 | .catch(e => console.log(e))
39 | }
40 | }
41 | }
42 |
43 | export default props => {
44 | const {
45 | today,
46 | yesterday,
47 | last7thDay,
48 | thisWeekStartDay,
49 | thisWeekLastDay,
50 | lastWeekStartDay,
51 | lastWeekLastDay,
52 | last30thDay,
53 | thisMonthStartDay,
54 | lastMonthStartDay,
55 | lastMonthEndDay
56 | } = props.filterOptions
57 |
58 | const todayStr = format(today, 'dddd, MMM Do')
59 | const yesterdayStr = format(yesterday, 'dddd, MMM Do')
60 | const last7Days = `${format(last7thDay, 'MMM Do')} - ${format(
61 | today,
62 | 'MMM Do'
63 | )}`
64 | const thisWeek = `${format(thisWeekStartDay, 'MMM Do')} - ${format(
65 | thisWeekLastDay,
66 | 'MMM Do'
67 | )}`
68 | const lastWeek = `${format(lastWeekStartDay, 'MMM Do')} - ${format(
69 | lastWeekLastDay,
70 | 'MMM Do'
71 | )}`
72 | const last30days = `${format(last30thDay, 'MMM Do')} - ${format(
73 | today,
74 | 'MMM Do'
75 | )}`
76 | const thisMonth = format(today, 'MMMM')
77 | const lastMonth = format(last30thDay, 'MMMM')
78 | return (
79 |
80 | Filter By
81 |
82 |
83 | # Day
84 |
85 |
86 |
87 | {
89 | console.log('clicked')
90 | props.toggleFilter()
91 | changeRoute(
92 | new Date(yesterday).getTime(),
93 | new Date(today).getTime(),
94 | props
95 | )
96 | }}
97 | id='today'
98 | type='radio'
99 | name='filter'
100 | />
101 |
102 |
103 | Today
104 | {todayStr}
105 |
106 |
107 |
108 |
109 | {
111 | props.toggleFilter()
112 | changeRoute(
113 | addDays(yesterday, -1).getTime(),
114 | new Date(yesterday).getTime(),
115 | props
116 | )
117 | }}
118 | id='yesterday'
119 | type='radio'
120 | name='filter'
121 | />
122 |
123 |
124 | Yesterday
125 | {yesterdayStr}
126 |
127 |
128 |
129 |
130 |
131 | # Week
132 |
133 |
134 |
135 | {
137 | props.toggleFilter()
138 | changeRoute(
139 | new Date(last7thDay).getTime(),
140 | new Date(today).getTime(),
141 | props
142 | )
143 | }}
144 | id='last-seven-days'
145 | name='filter'
146 | type='radio'
147 | />
148 |
149 |
150 | Last 7 days
151 | {last7Days}
152 |
153 |
154 |
155 |
156 | {
158 | props.toggleFilter()
159 | changeRoute(
160 | new Date(thisWeekStartDay).getTime(),
161 | new Date(thisWeekLastDay).getTime(),
162 | props
163 | )
164 | }}
165 | id='this-week'
166 | name='filter'
167 | type='radio'
168 | />
169 |
170 |
171 | This week
172 | {thisWeek}
173 |
174 |
175 |
176 |
177 | {
179 | props.toggleFilter()
180 | changeRoute(
181 | new Date(lastWeekStartDay).getTime(),
182 | new Date(lastWeekLastDay).getTime(),
183 | props
184 | )
185 | }}
186 | id='last-week'
187 | name='filter'
188 | type='radio'
189 | />
190 |
191 |
192 | Last week
193 | {lastWeek}
194 |
195 |
196 |
197 |
198 |
199 | # Month
200 |
201 |
202 |
203 | {
205 | props.toggleFilter()
206 | changeRoute(
207 | new Date(last30thDay).getTime(),
208 | new Date(today).getTime(),
209 | props
210 | )
211 | }}
212 | id='last-30-days'
213 | name='filter'
214 | type='radio'
215 | />
216 |
217 |
218 | Last 30 days
219 | {last30days}
220 |
221 |
222 |
223 |
224 | {
226 | props.toggleFilter()
227 | changeRoute(
228 | new Date(thisMonthStartDay).getTime(),
229 | new Date(today).getTime(),
230 | props
231 | )
232 | }}
233 | id='this-month'
234 | name='filter'
235 | type='radio'
236 | />
237 |
238 |
239 |
240 | This month
241 | {thisMonth}
242 |
243 |
244 |
245 |
246 | {
248 | props.toggleFilter()
249 | changeRoute(
250 | new Date(lastMonthStartDay).getTime(),
251 | new Date(lastMonthEndDay).getTime(),
252 | props
253 | )
254 | }}
255 | id='last-month'
256 | name='filter'
257 | type='radio'
258 | />
259 |
260 |
261 | Last month
262 | {lastMonth}
263 |
264 |
265 |
266 |
267 |
268 |
348 |
349 | )
350 | }
351 |
--------------------------------------------------------------------------------
/components/Snackbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default class SnackBar extends React.Component {
4 | constructor (props) {
5 | super(props)
6 | this.state = {
7 | showSnackBar: this.props.show,
8 | timer: this.props.timer || 4000
9 | }
10 | }
11 |
12 | componentWillReceiveProps (nextProps) {
13 | var { showSnackBar, timer } = this.state
14 | if (showSnackBar !== nextProps.show) {
15 | this.setState({
16 | showSnackBar: nextProps.show,
17 | timer: nextProps.timer
18 | })
19 |
20 | setTimeout(() => {
21 | this.setState({ showSnackBar: false })
22 | this.props.onClose && this.props.onClose()
23 | }, timer)
24 | }
25 | }
26 |
27 | render () {
28 | const { showSnackBar } = this.state
29 | return (
30 |
31 | {this.props.children}
32 |
59 |
60 | )
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | ## Contributing to Linklet App
2 |
3 | 1. Follow the instructions on this [Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) guide to install Git on your system.
4 | 2. Fork the project into your account and clone it locally
5 |
6 | ```bash
7 | # Get the latest snapshot
8 | git clone https://github.com/vinaypuppal/linklet-app linklet
9 |
10 | # Change directory
11 | cd linklet
12 | ```
13 | 3. Install dependencies using `npm install`
14 | 3. Create a branch specific to the issue or feature
15 | 4. Start the dev server using `npm run dev`
16 | 5. Make some changes to the code, [add them to the staging area and commit them](https://www.atlassian.com/git/tutorials/saving-changes/git-commit)
17 | 6. [Squash your commits](http://stackoverflow.com/questions/5189560/squash-my-last-x-commits-together-using-git/5201642#5201642)
18 | 7. [Submit a pull request](https://www.atlassian.com/git/tutorials/making-a-pull-request)
--------------------------------------------------------------------------------
/hocs/ContainerPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 | import NProgress from 'nprogress'
4 | import SnackBar from '../components/Snackbar'
5 | import { initGA, logEvent, logPageView } from '../lib/analytics'
6 |
7 | Router.onRouteChangeStart = () => {
8 | console.log('started listening')
9 | NProgress.start()
10 | }
11 | Router.onRouteChangeComplete = url => {
12 | logEvent('Navigation', `Navigated to ${url}`)
13 | NProgress.done()
14 | }
15 | Router.onRouteChangeError = () => {
16 | NProgress.done()
17 | }
18 |
19 | export default Page => {
20 | return class ContainerPage extends React.Component {
21 | static async getInitialProps (ctx) {
22 | try {
23 | let initialProps = {}
24 | if (Page.getInitialProps) {
25 | initialProps = await Page.getInitialProps(ctx)
26 | }
27 | return { ...initialProps }
28 | } catch (e) {
29 | throw e
30 | }
31 | }
32 | constructor (props) {
33 | super(props)
34 | this.state = {
35 | show: false,
36 | message: ''
37 | }
38 | this.handleAuthChange = this.handleAuthChange.bind(this)
39 | this.onSanckbarClose = this.onSanckbarClose.bind(this)
40 | this.updateNetworkStatus = this.updateNetworkStatus.bind(this)
41 | }
42 | onSanckbarClose () {
43 | console.log('snackbar closed')
44 | this.setState({
45 | show: false,
46 | message: ''
47 | })
48 | }
49 | handleAuthChange (eve) {
50 | if (eve.key === 'logout') {
51 | Router.push(`/?logout=${eve.newValue}`)
52 | }
53 | }
54 | updateNetworkStatus () {
55 | if (navigator.onLine) {
56 | const body = document.querySelector('body')
57 | body.style.filter = 'grayscale(0)'
58 | body.style.pointerEvents = 'auto'
59 | this.setState({
60 | show: true,
61 | message: "You are 'Online'"
62 | })
63 | } else {
64 | const body = document.querySelector('body')
65 | body.style.filter = 'grayscale(1)'
66 | body.style.pointerEvents = 'none'
67 | this.setState({
68 | show: true,
69 | message: "You are 'Offline'"
70 | })
71 | }
72 | }
73 | componentDidMount () {
74 | console.log('started')
75 | if (!window.GA_INITIALIZED) {
76 | initGA()
77 | window.GA_INITIALIZED = true
78 | }
79 | logPageView()
80 | require('../utils/offlineInstaller')
81 | window.addEventListener('storage', this.handleAuthChange, false)
82 | window.addEventListener('online', this.updateNetworkStatus, false)
83 | window.addEventListener('offline', this.updateNetworkStatus, false)
84 | window.HW_config = {
85 | selector: '#changelog', // CSS selector where to inject the badge
86 | account: 'xMjW37' // your account ID
87 | }
88 | if (document.getElementById('changelog-script')) {
89 | window.Headway.init(window.HW_config)
90 | } else {
91 | const s1 = document.createElement('script')
92 | s1.id = 'changelog-script'
93 | s1.async = true
94 | s1.src = 'https://cdn.headwayapp.co/widget.js'
95 | s1.charset = 'UTF-8'
96 | document.body.appendChild(s1)
97 | }
98 | if (!navigator.onLine) {
99 | const body = document.querySelector('body')
100 | body.style.filter = 'grayscale(1)'
101 | this.setState({
102 | show: true,
103 | message: "You are 'Offline'"
104 | })
105 | }
106 | }
107 |
108 | componentWillUnmount () {
109 | console.log('unmounted')
110 | window.removeEventListener('storage', this.handleAuthChange, false)
111 | window.removeEventListener('online', this.updateNetworkStatus, false)
112 | window.removeEventListener('offline', this.updateNetworkStatus, false)
113 | }
114 | render () {
115 | return (
116 |
117 |
118 |
123 | {this.state.message}
124 |
125 |
126 | )
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/hocs/PublicPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { loadUser } from '../utils/authenticate'
3 |
4 | export default Page => {
5 | return class PublicPage extends React.Component {
6 | static async getInitialProps (ctx) {
7 | try {
8 | const authData = await loadUser(ctx)
9 | let initialProps = {}
10 | if (Page.getInitialProps) {
11 | initialProps = await Page.getInitialProps(ctx)
12 | }
13 | if (!authData) {
14 | return { ...initialProps, isAuthenticated: false }
15 | }
16 | return { ...authData, ...initialProps }
17 | } catch (e) {
18 | throw e
19 | }
20 | }
21 | render () {
22 | return
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/hocs/SecretPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { loadUser } from '../utils/authenticate'
3 | import redirect from '../utils/redirect'
4 |
5 | export default Page => {
6 | return class SecretPage extends React.Component {
7 | static async getInitialProps (ctx) {
8 | try {
9 | const authData = await loadUser(ctx)
10 | let initialProps = {}
11 | if (!authData) {
12 | const pathName = ctx.req ? ctx.req.url : ctx.pathname
13 | if (pathName === '/profile') {
14 | if (Page.getInitialProps) {
15 | initialProps = await Page.getInitialProps(...ctx)
16 | }
17 | return { ...initialProps }
18 | }
19 | return redirect(ctx)
20 | }
21 | if (Page.getInitialProps) {
22 | initialProps = await Page.getInitialProps({ ...ctx })
23 | }
24 | return { ...authData, ...initialProps }
25 | } catch (e) {
26 | throw e
27 | }
28 | }
29 | render () {
30 | return
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/analytics.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga'
2 |
3 | export const initGA = () => {
4 | console.log('GA init')
5 | ReactGA.initialize('UA-96215468-1')
6 | }
7 | export const logPageView = () => {
8 | ReactGA.set({ page: window.location.pathname })
9 | ReactGA.pageview(window.location.pathname)
10 | }
11 |
12 | export const logEvent = (category = '', action = '') => {
13 | if (category && action) {
14 | ReactGA.event({ category, action })
15 | }
16 | }
17 |
18 | export const logException = (description = '', fatal = false) => {
19 | if (description) {
20 | ReactGA.exception({ description, fatal })
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/db.js:
--------------------------------------------------------------------------------
1 | import cookie from 'react-cookie'
2 | import fetch from 'isomorphic-unfetch'
3 | import { checkStatus } from '../utils'
4 |
5 | let baseUrl
6 |
7 | if (!process.browser) {
8 | baseUrl =
9 | process.env.NODE_ENV === 'development'
10 | ? 'https://linklet.app/api'
11 | : 'https://linklet.app/api'
12 | } else {
13 | baseUrl = 'https://linklet.app/api'
14 | }
15 |
16 | const getAll = ({ page = 1, search, myLinks, req, bookmarks, sort } = {}) => {
17 | let loginToken = cookie.load('loginToken')
18 | if (!loginToken) {
19 | loginToken = req && req.cookies && req.cookies.loginToken
20 | }
21 | console.log('loginToken', loginToken)
22 | console.log('myLinks', myLinks)
23 | let url
24 | if (myLinks) {
25 | url = search
26 | ? `${baseUrl}/links/me/all/?page=${page}&search=${search}&sort=${sort}`
27 | : `${baseUrl}/links/me/all/?page=${page}&sort=${sort}`
28 | } else if (bookmarks) {
29 | url = search
30 | ? `${baseUrl}/bookmarks/me/all/?page=${page}&search=${search}&sort=${sort}`
31 | : `${baseUrl}/bookmarks/me/all/?page=${page}&sort=${sort}`
32 | } else {
33 | url = search
34 | ? `${baseUrl}/links/all/?page=${page}&search=${search}&sort=${sort}`
35 | : `${baseUrl}/links/all/?page=${page}&sort=${sort}`
36 | }
37 | console.log(url)
38 | return fetch(url, {
39 | headers: {
40 | 'x-auth': loginToken || ''
41 | }
42 | })
43 | .then(checkStatus)
44 | .then(r => r.json())
45 | .then(res => ({ data: res }))
46 | }
47 |
48 | const getByFilter = ({
49 | start,
50 | end,
51 | page = 1,
52 | search,
53 | myLinks,
54 | req,
55 | bookmarks,
56 | sort
57 | } = {}) => {
58 | let loginToken = cookie.load('loginToken')
59 | if (!loginToken) {
60 | loginToken = req && req.cookies && req.cookies.loginToken
61 | }
62 | let url
63 | if (myLinks) {
64 | url =
65 | start && end && search
66 | ? `${baseUrl}/links/me/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
67 | : start && end
68 | ? `${baseUrl}/links/me/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
69 | : `${baseUrl}/links/me/filter/?page=${page}&sort=${sort}`
70 | } else if (bookmarks) {
71 | url =
72 | start && end && search
73 | ? `${baseUrl}/bookmarks/me/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
74 | : start && end
75 | ? `${baseUrl}/bookmarks/me/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
76 | : `${baseUrl}/bookmarks/me/filter/?page=${page}&sort=${sort}`
77 | } else {
78 | url =
79 | start && end && search
80 | ? `${baseUrl}/links/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
81 | : start && end
82 | ? `${baseUrl}/links/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
83 | : `${baseUrl}/links/filter/?page=${page}&sort=${sort}`
84 | }
85 |
86 | return fetch(url, {
87 | headers: {
88 | 'x-auth': loginToken || ''
89 | }
90 | })
91 | .then(checkStatus)
92 | .then(r => r.json())
93 | .then(res => ({ data: res }))
94 | }
95 |
96 | const getMetaData = link => {
97 | const url = `${baseUrl}/metadata?url=${link}`
98 | return fetch(url)
99 | .then(checkStatus)
100 | .then(r => r.json())
101 | .then(res => ({ data: res }))
102 | }
103 |
104 | const saveLink = data => {
105 | const loginToken = cookie.load('loginToken')
106 | const url = `${baseUrl}/links`
107 | return fetch(url, {
108 | method: 'POST',
109 | body: JSON.stringify(data),
110 | headers: {
111 | 'x-auth': loginToken || '',
112 | 'Content-Type': 'application/json'
113 | }
114 | })
115 | .then(checkStatus)
116 | .then(r => r.json())
117 | .then(res => ({ data: res }))
118 | }
119 |
120 | const incrementView = id => {
121 | const loginToken = cookie.load('loginToken')
122 | console.log(loginToken)
123 | const url = `${baseUrl}/links/${id}/views`
124 | return fetch(url, {
125 | method: 'PATCH',
126 | body: JSON.stringify({}),
127 | headers: {
128 | 'x-auth': loginToken || '',
129 | 'Content-Type': 'application/json'
130 | }
131 | })
132 | .then(checkStatus)
133 | .then(r => r.json())
134 | .then(res => ({ data: res }))
135 | }
136 |
137 | const likeLink = id => {
138 | const loginToken = cookie.load('loginToken')
139 | console.log(loginToken)
140 | const url = `${baseUrl}/links/${id}/bookmark`
141 | return fetch(url, {
142 | method: 'PATCH',
143 | body: JSON.stringify({}),
144 | headers: {
145 | 'x-auth': loginToken || '',
146 | 'Content-Type': 'application/json'
147 | }
148 | })
149 | .then(checkStatus)
150 | .then(r => r.json())
151 | .then(res => ({ data: res }))
152 | }
153 |
154 | export default {
155 | getAll,
156 | getByFilter,
157 | getMetaData,
158 | saveLink,
159 | baseUrl,
160 | incrementView,
161 | likeLink
162 | }
163 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
2 | const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
3 | const { ANALYZE } = process.env
4 |
5 | module.exports = {
6 | webpack: (config, { dev }) => {
7 | /* Enable only in Production */
8 | console.log(dev)
9 | if (!dev) {
10 | // Service Worker
11 | config.plugins.push(
12 | new SWPrecacheWebpackPlugin({
13 | filename: 'sw.js',
14 | minify: true,
15 | staticFileGlobsIgnorePatterns: [/\.next\//],
16 | staticFileGlobs: [
17 | 'static/**/*' // Precache all static files by default
18 | ],
19 | importScripts: ['/push-sw.js'],
20 | forceDelete: true,
21 | runtimeCaching: [
22 | {
23 | handler: 'fastest',
24 | urlPattern: /[.](png|jpg|svg)/
25 | },
26 | {
27 | handler: 'cacheFirst',
28 | urlPattern: /\/_next\/.*/,
29 | options: {
30 | cache: {
31 | name: 'nextjs-cache'
32 | }
33 | }
34 | },
35 | {
36 | handler: 'networkFirst',
37 | urlPattern: /^http.*/ // cache all files
38 | }
39 | ]
40 | })
41 | )
42 | if (ANALYZE) {
43 | config.plugins.push(
44 | new BundleAnalyzerPlugin({
45 | analyzerMode: 'server',
46 | analyzerPort: 8888,
47 | openAnalyzer: true
48 | })
49 | )
50 | }
51 | }
52 | return config
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linklet-app",
3 | "alias": "linklet-app.now.sh",
4 | "env": {
5 | "NODE_ENV": "production"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linklet-app",
3 | "dependencies": {
4 | "color-generator": "^0.1.0",
5 | "cookie-parser": "^1.4.3",
6 | "date-fns": "^1.28.5",
7 | "express": "^4.15.3",
8 | "ismobilejs": "^0.4.1",
9 | "isomorphic-unfetch": "^2.0.0",
10 | "next": "^5.0.0",
11 | "nprogress": "^0.2.0",
12 | "rc-pagination": "^1.10.1",
13 | "react": "16.0.0",
14 | "react-autosuggest": "^9.3.1",
15 | "react-cookie": "1.0.5",
16 | "react-dom": "16.0.0",
17 | "react-ga": "^2.2.0",
18 | "react-highlight-words": "^0.8.0",
19 | "react-icons": "^2.2.4",
20 | "react-lazyload": "^2.2.7",
21 | "react-modal-dialog": "^4.0.7",
22 | "sw-precache-webpack-plugin": "^0.11.4",
23 | "webpack-bundle-analyzer": "^2.8.3"
24 | },
25 | "scripts": {
26 | "dev": "NODE_ENV=development nodemon server.js -w server.js",
27 | "build": "next build",
28 | "start": "NODE_ENV=production node server",
29 | "deploy": "npm run lint && now",
30 | "alias": "now alias",
31 | "lint":
32 | "prettier 'utils/**/*.js' 'components/**/*.js' 'pages/**/*.js' 'lib/**/*.js' 'hocs/**/*.js' '*.js' --write --single-quote --no-semi && standard --fix",
33 | "precommit": "lint-staged",
34 | "analyze": "cross-env ANALYZE=1 next build"
35 | },
36 | "devDependencies": {
37 | "cross-env": "^5.0.1",
38 | "husky": "^0.14.3",
39 | "lint-staged": "^4.0.2",
40 | "nodemon": "^1.11.0",
41 | "prettier": "^1.5.3",
42 | "standard": "10.0.2"
43 | },
44 | "lint-staged": {
45 | "*.js": [
46 | "prettier --write --single-quote --no-semi",
47 | "standard --fix",
48 | "git add"
49 | ]
50 | },
51 | "version": "1.0.0",
52 | "repository": {
53 | "type": "git",
54 | "url": "git+https://github.com/vinaypuppal/linklet-app.git"
55 | },
56 | "keywords": [],
57 | "author": "Vinay Puppal (https://www.vinaypuppal.com/)",
58 | "license": "MIT",
59 | "bugs": {
60 | "url": "https://github.com/vinaypuppal/linklet-app/issues"
61 | },
62 | "homepage": "https://github.com/vinaypuppal/linklet-app#readme",
63 | "description": ""
64 | }
65 |
--------------------------------------------------------------------------------
/pages/about.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from '../components/Header'
3 | import Footer from '../components/Footer'
4 |
5 | export default class About extends React.Component {
6 | render () {
7 | return (
8 |
9 |
10 |
11 | About
12 |
13 | Linklet as of now contains the links which are shared in whatsapp
14 | freeCodeCamp Hyderabad group. Since many useful links were shared in
15 | the group so I thought to create an app where we can find all links
16 | easily based on particular date.
17 |
18 |
19 |
20 |
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/pages/bookmarks.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Header from '../components/Header'
3 | import Footer from '../components/Footer'
4 |
5 | import ContainerPage from '../hocs/ContainerPage'
6 | import SecretPage from '../hocs/SecretPage'
7 |
8 | import mobileJs from 'ismobilejs'
9 | import addDays from 'date-fns/add_days'
10 | import startOfWeek from 'date-fns/start_of_week'
11 | import lastDayOfWeek from 'date-fns/last_day_of_week'
12 | import addWeeks from 'date-fns/add_weeks'
13 | import startOfMonth from 'date-fns/start_of_month'
14 | import lastDayOfMonth from 'date-fns/last_day_of_month'
15 |
16 | import db from '../lib/db'
17 | import SideBar from '../components/Sidebar'
18 | import LinksList from '../components/LinksList'
19 |
20 | class Bookmarks extends Component {
21 | static async getInitialProps ({ query, req }) {
22 | const { start, end, page = 1, search, sort = -1 } = query
23 |
24 | const today = new Date()
25 | const yesterday = addDays(today, -1)
26 | const last7thDay = addDays(today, -7)
27 | const thisWeekStartDay = startOfWeek(today)
28 | const thisWeekLastDay = lastDayOfWeek(today)
29 | const lastWeekStartDay = startOfWeek(addWeeks(today, -1))
30 | const lastWeekLastDay = lastDayOfWeek(addWeeks(today, -1))
31 | const last30thDay = addDays(today, -31)
32 | const thisMonthStartDay = startOfMonth(today)
33 | const lastMonthStartDay = startOfMonth(last30thDay)
34 | const lastMonthEndDay = lastDayOfMonth(last30thDay)
35 |
36 | const filterOptions = {
37 | today,
38 | yesterday,
39 | last7thDay,
40 | thisWeekStartDay,
41 | thisWeekLastDay,
42 | lastWeekStartDay,
43 | lastWeekLastDay,
44 | last30thDay,
45 | thisMonthStartDay,
46 | lastMonthStartDay,
47 | lastMonthEndDay
48 | }
49 | let res
50 | try {
51 | if (start && end) {
52 | res = await db.getByFilter({
53 | start,
54 | end,
55 | page,
56 | search,
57 | req,
58 | bookmarks: true,
59 | sort
60 | })
61 | } else {
62 | res = await db.getAll({ page, search, req, bookmarks: true, sort })
63 | }
64 | } catch (e) {
65 | throw e
66 | }
67 | const isMobile = req
68 | ? mobileJs(req.headers['user-agent']).any
69 | : mobileJs.any
70 | return { data: res.data, filterOptions, isMobile }
71 | }
72 | constructor (props) {
73 | super(props)
74 | this.state = {
75 | toggleFilter: false,
76 | isShowingModal: false,
77 | from: null,
78 | to: null
79 | }
80 | }
81 | toggleFilter (e) {
82 | e && e.preventDefault()
83 | this.setState({
84 | toggleFilter: !this.state.toggleFilter
85 | })
86 | }
87 | render () {
88 | const { data, filterOptions, url, user, isMobile } = this.props
89 | return (
90 |
91 |
98 |
104 |
105 |
106 |
138 |
139 | )
140 | }
141 | }
142 |
143 | export default ContainerPage(SecretPage(Bookmarks))
144 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Header from '../components/Header'
3 | import Footer from '../components/Footer'
4 |
5 | import ContainerPage from '../hocs/ContainerPage'
6 | import PublicPage from '../hocs/PublicPage'
7 |
8 | import mobileJs from 'ismobilejs'
9 | import addDays from 'date-fns/add_days'
10 | import startOfWeek from 'date-fns/start_of_week'
11 | import lastDayOfWeek from 'date-fns/last_day_of_week'
12 | import addWeeks from 'date-fns/add_weeks'
13 | import startOfMonth from 'date-fns/start_of_month'
14 | import lastDayOfMonth from 'date-fns/last_day_of_month'
15 |
16 | import db from '../lib/db'
17 | import SideBar from '../components/Sidebar'
18 | import LinksList from '../components/LinksList'
19 |
20 | class Home extends Component {
21 | static async getInitialProps ({ query, req }) {
22 | const { start, end, page = 1, search, sort = -1 } = query
23 |
24 | const today = new Date()
25 | const yesterday = addDays(today, -1)
26 | const last7thDay = addDays(today, -7)
27 | const thisWeekStartDay = startOfWeek(today)
28 | const thisWeekLastDay = lastDayOfWeek(today)
29 | const lastWeekStartDay = startOfWeek(addWeeks(today, -1))
30 | const lastWeekLastDay = lastDayOfWeek(addWeeks(today, -1))
31 | const last30thDay = addDays(today, -31)
32 | const thisMonthStartDay = startOfMonth(today)
33 | const lastMonthStartDay = startOfMonth(last30thDay)
34 | const lastMonthEndDay = lastDayOfMonth(last30thDay)
35 |
36 | const filterOptions = {
37 | today,
38 | yesterday,
39 | last7thDay,
40 | thisWeekStartDay,
41 | thisWeekLastDay,
42 | lastWeekStartDay,
43 | lastWeekLastDay,
44 | last30thDay,
45 | thisMonthStartDay,
46 | lastMonthStartDay,
47 | lastMonthEndDay
48 | }
49 | let res
50 | try {
51 | if (start && end) {
52 | res = await db.getByFilter({ start, end, page, search, req, sort })
53 | } else {
54 | res = await db.getAll({ page, search, req, sort })
55 | }
56 | } catch (e) {
57 | throw e
58 | }
59 | const isMobile = req
60 | ? mobileJs(req.headers['user-agent']).any
61 | : mobileJs.any
62 | return { data: res.data, filterOptions, isMobile }
63 | }
64 | constructor (props) {
65 | super(props)
66 | this.state = {
67 | toggleFilter: false
68 | }
69 | }
70 | toggleFilter (e) {
71 | e && e.preventDefault()
72 | this.setState({
73 | toggleFilter: !this.state.toggleFilter
74 | })
75 | }
76 | render () {
77 | const { data, filterOptions, url, user, isMobile } = this.props
78 | return (
79 |
80 |
87 |
93 |
94 |
95 |
127 |
128 | )
129 | }
130 | }
131 |
132 | export default ContainerPage(PublicPage(Home))
133 |
--------------------------------------------------------------------------------
/pages/my-links.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Header from '../components/Header'
3 | import Footer from '../components/Footer'
4 |
5 | import ContainerPage from '../hocs/ContainerPage'
6 | import SecretPage from '../hocs/SecretPage'
7 |
8 | import mobileJs from 'ismobilejs'
9 | import addDays from 'date-fns/add_days'
10 | import startOfWeek from 'date-fns/start_of_week'
11 | import lastDayOfWeek from 'date-fns/last_day_of_week'
12 | import addWeeks from 'date-fns/add_weeks'
13 | import startOfMonth from 'date-fns/start_of_month'
14 | import lastDayOfMonth from 'date-fns/last_day_of_month'
15 |
16 | import db from '../lib/db'
17 | import SideBar from '../components/Sidebar'
18 | import LinksList from '../components/LinksList'
19 |
20 | class MyLinks extends Component {
21 | static async getInitialProps ({ query, req }) {
22 | const { start, end, page = 1, search, sort = -1 } = query
23 |
24 | const today = new Date()
25 | const yesterday = addDays(today, -1)
26 | const last7thDay = addDays(today, -7)
27 | const thisWeekStartDay = startOfWeek(today)
28 | const thisWeekLastDay = lastDayOfWeek(today)
29 | const lastWeekStartDay = startOfWeek(addWeeks(today, -1))
30 | const lastWeekLastDay = lastDayOfWeek(addWeeks(today, -1))
31 | const last30thDay = addDays(today, -31)
32 | const thisMonthStartDay = startOfMonth(today)
33 | const lastMonthStartDay = startOfMonth(last30thDay)
34 | const lastMonthEndDay = lastDayOfMonth(last30thDay)
35 |
36 | const filterOptions = {
37 | today,
38 | yesterday,
39 | last7thDay,
40 | thisWeekStartDay,
41 | thisWeekLastDay,
42 | lastWeekStartDay,
43 | lastWeekLastDay,
44 | last30thDay,
45 | thisMonthStartDay,
46 | lastMonthStartDay,
47 | lastMonthEndDay
48 | }
49 | let res
50 | try {
51 | if (start && end) {
52 | res = await db.getByFilter({
53 | start,
54 | end,
55 | page,
56 | search,
57 | req,
58 | myLinks: true,
59 | sort
60 | })
61 | } else {
62 | res = await db.getAll({ page, search, req, myLinks: true, sort })
63 | }
64 | } catch (e) {
65 | throw e
66 | }
67 | const isMobile = req
68 | ? mobileJs(req.headers['user-agent']).any
69 | : mobileJs.any
70 | return { data: res.data, filterOptions, isMobile }
71 | }
72 | constructor (props) {
73 | super(props)
74 | this.state = {
75 | toggleFilter: false,
76 | isShowingModal: false,
77 | from: null,
78 | to: null
79 | }
80 | }
81 | toggleFilter (e) {
82 | e && e.preventDefault()
83 | this.setState({
84 | toggleFilter: !this.state.toggleFilter
85 | })
86 | }
87 | render () {
88 | const { data, filterOptions, url, user, isMobile } = this.props
89 | return (
90 |
91 |
98 |
104 |
105 |
106 |
138 |
139 | )
140 | }
141 | }
142 |
143 | export default ContainerPage(SecretPage(MyLinks))
144 |
--------------------------------------------------------------------------------
/pages/profile.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import NProgress from 'nprogress'
3 | import Header from '../components/Header'
4 | import FaGithub from 'react-icons/lib/fa/github'
5 | import FaSignOut from 'react-icons/lib/fa/sign-out'
6 |
7 | import ContainerPage from '../hocs/ContainerPage'
8 | import PublicPage from '../hocs/PublicPage'
9 |
10 | import { login, logout } from '../utils/authenticate'
11 |
12 | class Profile extends React.Component {
13 | render () {
14 | return (
15 |
16 |
21 |
22 |
23 | {this.props.url.query && this.props.url.query.next === '/my-links'
24 | ? 'Please login to view links added by you!...'
25 | : this.props.url.query &&
26 | this.props.url.query.next === '/submit-link'
27 | ? 'Please login to submit new link'
28 | : this.props.url.query &&
29 | this.props.url.query.next === '/bookmarks'
30 | ? 'Please login to view links bookmarked by you!..'
31 | : ''}
32 |
33 | {this.props.isAuthenticated ? (
34 |
35 |
36 |
42 |
43 |
@{this.props.user.username}
44 | {this.props.user.name && (
45 |
46 | Name
47 | {this.props.user.name}
48 |
49 | )}
50 | {this.props.user.email && (
51 |
52 | Email
53 | {this.props.user.email}
54 |
55 | )}
56 |
{
58 | NProgress.start()
59 | logout()
60 | }}
61 | >
62 |
63 | LogOut
64 |
65 |
66 | ) : (
67 |
68 |
69 |
74 |
{
76 | NProgress.start()
77 | login()
78 | }}
79 | >
80 |
81 | Login With Github
82 |
83 |
84 | )}
85 |
86 |
202 |
203 | )
204 | }
205 | }
206 |
207 | export default ContainerPage(PublicPage(Profile))
208 |
--------------------------------------------------------------------------------
/pages/submit-link.bak.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from '../components/Header'
3 | import NProgress from 'nprogress'
4 | import Router from 'next/router'
5 | import LinkCard from '../components/MLinkCard'
6 | import db from '../lib/db'
7 |
8 | import ContainerPage from '../hocs/ContainerPage'
9 | import SecretPage from '../hocs/SecretPage'
10 |
11 | class SubmitLink extends React.Component {
12 | constructor (props) {
13 | super(props)
14 | this.state = {
15 | info:
16 | 'Simply give us the URL we will fetch metadata and show preview before you can submit it to linklet',
17 | url: '',
18 | showPreview: false,
19 | linkData: {},
20 | error: ''
21 | }
22 | }
23 | componentDidMount () {
24 | this.input.focus()
25 | }
26 | handleFetch (e) {
27 | e && e.preventDefault()
28 | let url = this.state.url
29 | if (url && !/^https?:\/\//i.test(url)) {
30 | url = 'http://' + url
31 | }
32 | NProgress.start()
33 | db
34 | .getMetaData(url)
35 | .then(({ data }) => {
36 | console.log(data)
37 | NProgress.done()
38 | this.setState({
39 | info: 'Click Save to submit link to linklet',
40 | linkData: data,
41 | showPreview: true,
42 | error: ''
43 | })
44 | })
45 | .catch(e => {
46 | console.log(e)
47 | NProgress.done()
48 | this.setState({ error: e.message })
49 | })
50 | }
51 | handleSave () {
52 | console.log('---Saving---')
53 | NProgress.start()
54 | db
55 | .saveLink(this.state.linkData)
56 | .then(res => {
57 | NProgress.done()
58 | console.log(res)
59 | Router.push('/my-links')
60 | })
61 | .catch(e => {
62 | console.log(e)
63 | NProgress.done()
64 | let message
65 | if (e.message === '11000') {
66 | message =
67 | 'Sorry, its seems like this link already exist in linklet!...'
68 | } else {
69 | message = e.message
70 | }
71 | console.log(message)
72 | this.setState({ error: message })
73 | })
74 | }
75 | handleBack () {
76 | this.setState({
77 | info:
78 | 'Simply give us the URL we will fetch metadata and show preview before you can submit it to linklet',
79 | linkData: {},
80 | showPreview: false,
81 | error: ''
82 | })
83 | }
84 | render () {
85 | return (
86 |
87 |
92 |
93 | {this.state.error}
94 | {!this.state.showPreview && (
95 |
116 | )}
117 | {this.state.showPreview && (
118 |
127 | )}
128 | {this.state.showPreview && (
129 |
130 |
131 |
135 | Back
136 |
137 |
138 |
139 | save
140 |
141 |
142 | )}
143 | {this.state.info}
144 |
145 |
333 |
334 | )
335 | }
336 | }
337 |
338 | export default ContainerPage(SecretPage(SubmitLink))
339 |
--------------------------------------------------------------------------------
/pages/submit-link.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from '../components/Header'
3 |
4 | import ContainerPage from '../hocs/ContainerPage'
5 | import SecretPage from '../hocs/SecretPage'
6 |
7 | class SubmitLink extends React.Component {
8 | constructor (props) {
9 | super(props)
10 | this.state = {
11 | info:
12 | 'Temporarly we disabled links submission since we saw lot of spam submission recently. We are working on a new version which will be released soon.'
13 | }
14 | }
15 | render () {
16 | return (
17 |
18 |
23 |
24 | {this.state.info}
25 |
26 |
70 |
71 | )
72 | }
73 | }
74 |
75 | export default ContainerPage(SecretPage(SubmitLink))
76 |
--------------------------------------------------------------------------------
/push-sw.js:
--------------------------------------------------------------------------------
1 | /* global self, clients */
2 | // triggered everytime, when a push notification is received.
3 | console.log('Reached Here v6')
4 |
5 | let url
6 |
7 | self.addEventListener('push', function (event) {
8 | console.info('Event: Push')
9 | console.log(event.data && event.data.json())
10 | const playload = (event.data && event.data.json()) || {}
11 | var title = playload.title || 'No Title'
12 |
13 | var body = {
14 | body: playload.body || 'No Body',
15 | icon:
16 | playload.icon ||
17 | 'https://linklet.ml/static/favicons/android-chrome-192x192.png',
18 | badge: 'https://linklet.ml/static/favicons/mstile-70x70.png'
19 | }
20 | url = playload.body
21 |
22 | event.waitUntil(self.registration.showNotification(title, body))
23 | })
24 |
25 | self.addEventListener('notificationclick', function (event) {
26 | console.log(event.notification)
27 | event.notification.close() // Close the notification
28 | event.waitUntil(clients.openWindow(url || '/'))
29 | })
30 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const path = require('path')
3 | const next = require('next')
4 | const cookieParser = require('cookie-parser')
5 |
6 | const dev = process.env.NODE_ENV !== 'production'
7 | const app = next({ dir: '.', dev })
8 | const handle = app.getRequestHandler()
9 |
10 | app
11 | .prepare()
12 | .then(() => {
13 | const server = express()
14 | server.use(cookieParser())
15 |
16 | // Handling login
17 | server.use((req, res, next) => {
18 | if (!req.query.loginToken) return next()
19 | console.log(req.query)
20 | res.cookie('loginToken', req.query.loginToken, {
21 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
22 | httpOnly: false
23 | })
24 | if (req.query.next) {
25 | return res.redirect(req.query.next)
26 | }
27 | return res.redirect('/')
28 | })
29 |
30 | // Handling logout
31 | server.use((req, res, next) => {
32 | if (!req.query.logout) return next()
33 |
34 | res.cookie('loginToken', null, {
35 | expires: new Date(Date.now() - 1000),
36 | httpOnly: false
37 | })
38 |
39 | return res.redirect(req._parsedUrl.pathname)
40 | })
41 |
42 | // serve service worker
43 | server.get('/sw.js', (req, res) =>
44 | res.sendFile(path.resolve('./.next/sw.js'))
45 | )
46 | server.get('/push-sw.js', (req, res) =>
47 | res.sendFile(path.resolve('./push-sw.js'))
48 | )
49 |
50 | // tos
51 | server.get('/tos', (req, res) =>
52 | res.sendFile(path.resolve('./static/tos.html'))
53 | )
54 |
55 | server.get('*', (req, res) => handle(req, res))
56 |
57 | server.listen(process.env.PORT || 3000, err => {
58 | if (err) throw err
59 |
60 | console.log('> App running on port', process.env.PORT || 3000)
61 | })
62 | })
63 | .catch(ex => {
64 | console.error(ex.stack)
65 | process.exit(1)
66 | })
67 |
--------------------------------------------------------------------------------
/static/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #3f51b5
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/static/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/favicon.ico
--------------------------------------------------------------------------------
/static/favicons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Linklet App",
3 | "short_name": "Linklet",
4 | "description": "The app that helps you find the best links shared by FCC community",
5 | "start_url": "/",
6 | "theme_color": "#253592",
7 | "background_color": "#3f51b5",
8 | "display": "standalone",
9 | "orientation": "portrait",
10 | "gcm_sender_id": "73138728776",
11 | "icons": [
12 | {
13 | "src": "/static/favicons/android-chrome-192x192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "/static/favicons/android-chrome-512x512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/static/favicons/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/mstile-144x144.png
--------------------------------------------------------------------------------
/static/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/static/favicons/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/mstile-310x150.png
--------------------------------------------------------------------------------
/static/favicons/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/mstile-310x310.png
--------------------------------------------------------------------------------
/static/favicons/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderplex-org/linklet-app/dca7f5691b0aca4c473bc9ab13da3cc469754765/static/favicons/mstile-70x70.png
--------------------------------------------------------------------------------
/static/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
27 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/static/tos.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
15 |
21 |
27 |
28 |
33 |
34 |
38 | Linklet | Terms of Service and Privacy Policy
39 |
40 |
44 |
55 |
56 |
57 | Linklet Terms of Service and Privacy Policy
58 |
59 | 1. Terms
60 |
61 | By accessing the website at https://linklet.ml , you are agreeing to be bound by these
62 | terms of service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable
63 | local laws. If you do not agree with any of these terms, you are prohibited from using or accessing this site. The materials
64 | contained in this website are protected by applicable copyright and trademark law.
65 |
66 | 2. Use License
67 |
68 |
69 |
70 | Permission is granted to temporarily download one copy of the materials (information or software) on Linklet's website for
71 | personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under
72 | this license you may not:
73 |
74 |
75 | modify or copy the materials;
76 | use the materials for any commercial purpose, or for any public display (commercial or non-commercial);
77 | attempt to decompile or reverse engineer any software contained on Linklet's website;
78 | remove any copyright or other proprietary notations from the materials; or
79 | transfer the materials to another person or "mirror" the materials on any other server.
80 |
81 |
82 | This license shall automatically terminate if you violate any of these restrictions and may be terminated by Linklet
83 | at any time. Upon terminating your viewing of these materials or upon the termination of this license, you must destroy
84 | any downloaded materials in your possession whether in electronic or printed format.
85 |
86 |
87 | 3. Disclaimer
88 |
89 |
90 | The materials on Linklet's website are provided on an 'as is' basis. Linklet makes no warranties, expressed or implied,
91 | and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions
92 | of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation
93 | of rights.
94 | Further, Linklet does not warrant or make any representations concerning the accuracy, likely results, or reliability
95 | of the use of the materials on its website or otherwise relating to such materials or on any sites linked to this site.
96 |
97 |
98 | 4. Limitations
99 |
100 | In no event shall Linklet or its suppliers be liable for any damages (including, without limitation, damages for loss of
101 | data or profit, or due to business interruption) arising out of the use or inability to use the materials on Linklet's
102 | website, even if Linklet or a Linklet authorized representative has been notified orally or in writing of the possibility
103 | of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability
104 | for consequential or incidental damages, these limitations may not apply to you.
105 |
106 | 5. Accuracy of materials
107 |
108 | The materials appearing on Linklet's website could include technical, typographical, or photographic errors. Linklet does
109 | not warrant that any of the materials on its website are accurate, complete or current. Linklet may make changes to the
110 | materials contained on its website at any time without notice. However Linklet does not make any commitment to update
111 | the materials.
112 |
113 |
114 | 6. Links
115 |
116 | Linklet has not reviewed all of the sites linked to its website and is not responsible for the contents of any such linked
117 | site. The inclusion of any link does not imply endorsement by Linklet of the site. Use of any such linked website is
118 | at the user's own risk.
119 |
120 | 7. Modifications
121 |
122 | Linklet may revise these terms of service for its website at any time without notice. By using this website you are agreeing
123 | to be bound by the then current version of these terms of service.
124 |
125 | 8. Governing Law
126 |
127 | These terms and conditions are governed by and construed in accordance with the laws of Telanga,India and you irrevocably
128 | submit to the exclusive jurisdiction of the courts in that State or location.
129 |
130 | Privacy Policy
131 |
132 | Your privacy is important to us.
133 |
134 | It is Linklet's policy to respect your privacy regarding any information we may collect while operating our website. Accordingly,
135 | we have developed this privacy policy in order for you to understand how we collect, use, communicate, disclose and otherwise
136 | make use of personal information. We have outlined our privacy policy below.
137 |
138 |
139 | We will collect personal information by lawful and fair means and, where appropriate, with the knowledge or consent of
140 | the individual concerned.
141 | Before or at the time of collecting personal information, we will identify the purposes for which information is being
142 | collected.
143 |
144 | We will collect and use personal information solely for fulfilling those purposes specified by us and for other ancillary
145 | purposes, unless we obtain the consent of the individual concerned or as required by law.
146 | Personal data should be relevant to the purposes for which it is to be used, and, to the extent necessary for those purposes,
147 | should be accurate, complete, and up-to-date.
148 | We will protect personal information by using reasonable security safeguards against loss or theft, as well as unauthorized
149 | access, disclosure, copying, use or modification.
150 | We will make readily available to customers information about our policies and practices relating to the management of
151 | personal information.
152 | We will only retain personal information for as long as necessary for the fulfilment of those purposes.
153 |
154 |
155 | We are committed to conducting our business in accordance with these principles in order to ensure that the confidentiality
156 | of personal information is protected and maintained. Linklet may change this privacy policy from time to time at Linklet's
157 | sole discretion.
158 |
159 |
160 |
--------------------------------------------------------------------------------
/utils/authenticate.js:
--------------------------------------------------------------------------------
1 | /* global location */
2 | import db from '../lib/db'
3 | import cookie from 'react-cookie'
4 | import fetch from 'isomorphic-unfetch'
5 | import { checkStatus } from '../utils'
6 |
7 | export function login () {
8 | const href = `${db.baseUrl}/login/github?appRedirectUrl=${encodeURIComponent(
9 | location.href
10 | )}`
11 | location.href = href
12 | }
13 |
14 | export function logout () {
15 | const loginToken = cookie.load('loginToken')
16 | const camebackUrl = `${location.href}?logout=1`
17 | // It's important to send the loginToken since that's the way
18 | // how we say our auth server to logout the user
19 | const href = `${db.baseUrl}/logout?loginToken=${loginToken}&appRedirectUrl=${encodeURIComponent(
20 | camebackUrl
21 | )}`
22 | window.localStorage.removeItem('sharedState')
23 | window.localStorage.setItem('logout', Date.now())
24 | location.href = href
25 | }
26 |
27 | const fetchUser = async loginToken => {
28 | try {
29 | const result = await fetch(`${db.baseUrl}/users/me`, {
30 | headers: {
31 | 'x-auth': loginToken
32 | }
33 | })
34 | .then(checkStatus)
35 | .then(r => r.json())
36 | return result
37 | } catch (e) {
38 | return Promise.reject(e)
39 | }
40 | }
41 |
42 | export async function loadUser ({ req, res }) {
43 | try {
44 | const loginToken = cookie.load('loginToken')
45 | if (!req) {
46 | // client side
47 | if (loginToken) {
48 | const localUser = JSON.parse(window.localStorage.getItem('sharedState'))
49 | if (localUser && loadUser._id) {
50 | return {
51 | user: localUser,
52 | isAuthenticated: true,
53 | fetchedFrom: 'LOCAL'
54 | }
55 | } else {
56 | // fetch user from api using loginToken
57 | const user = await fetchUser(loginToken)
58 | window.localStorage.setItem('sharedState', JSON.stringify(user))
59 | return {
60 | user,
61 | isAuthenticated: true,
62 | fetchedFrom: 'CLIENT_API'
63 | }
64 | }
65 | }
66 | } else {
67 | // server side
68 | const loginToken = req.cookies && req.cookies.loginToken
69 | if (loginToken) {
70 | const user = await fetchUser(loginToken)
71 | return {
72 | user,
73 | isAuthenticated: true,
74 | fetchedFrom: 'SERVER_API'
75 | }
76 | }
77 | }
78 | } catch (e) {
79 | return Promise.reject(e)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | const truncateString = (str, num) => {
2 | // Clear out that junk in your trunk
3 | if (str.length > num && num <= 3) {
4 | return str.slice(0, num) + '...'
5 | } else if (str.length > num) {
6 | return str.slice(0, num - 3) + '...'
7 | }
8 | return str
9 | }
10 |
11 | const checkStatus = res => {
12 | if (res.ok) {
13 | return res
14 | }
15 | return res.json().then(err => {
16 | console.log(err)
17 | return Promise.reject(new Error(err.message || err.code))
18 | })
19 | }
20 |
21 | export { truncateString, checkStatus }
22 |
--------------------------------------------------------------------------------
/utils/offlineInstaller.js:
--------------------------------------------------------------------------------
1 | if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
2 | navigator.serviceWorker
3 | .register('/sw.js', {
4 | scope: './'
5 | })
6 | .then(function (reg) {
7 | console.log('sw rules here')
8 | reg.onupdatefound = function () {
9 | const installingWorker = reg.installing
10 |
11 | installingWorker.onstatechange = function () {
12 | switch (installingWorker.state) {
13 | case 'installed':
14 | if (navigator.serviceWorker.controller) {
15 | console.log('New or updated content is available.')
16 | } else {
17 | console.log('Content is now available offline!')
18 | }
19 | break
20 | case 'redundant':
21 | console.log('The installing serviceWorker became redundant.')
22 | break
23 | }
24 | }
25 | }
26 | })
27 | .catch(function (e) {
28 | console.error('Error during service worker registration:', e)
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/utils/redirect.js:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 |
3 | export default (
4 | ctx,
5 | to = `/profile?next=${encodeURIComponent(
6 | ctx.req ? ctx.req.url : ctx.pathname
7 | )}`
8 | ) => {
9 | console.log(ctx)
10 | if (ctx.res) {
11 | ctx.res.writeHead && ctx.res.writeHead(302, { Location: to })
12 | ctx.res.end && ctx.res.end()
13 | return {}
14 | } else {
15 | return Router.push(to)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------