├── .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 | [![Standard - JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![](https://img.shields.io/badge/lighthouse--score-96%2F100-blue.svg)](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 | 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 |
25 | 26 | 27 | 28 | 29 |

30 | Link 31 | let 32 |

33 |
34 | 56 | 142 |
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 | {link.title} 42 | 43 | )} 44 | 45 |
    46 |

    47 | 54 |

    55 |

    56 | {link.description ? ( 57 | 62 | ) : ( 63 | 'No Description' 64 | )} 65 |

    66 |
    67 |
    68 |
    69 | 70 | {isThisMonth(link.timestamp) 71 | ? 'Added ' + distanceInWordsToNow(link.timestamp) + ' ' + 'ago' 72 | : 'Added On ' + format(link.timestamp, 'MMM, Do YYYY')} 73 | 74 | 75 | { 79 | e.preventDefault() 80 | this.props.handelLike(link._id) 81 | }} 82 | href='#' 83 | > 84 | 85 | {link.bookmarkedBy ? link.bookmarkedBy.length : 0} 86 | 87 | 88 | 89 | {link.views || 0} 90 | 91 | 92 |
    93 |
    94 | 136 |
    137 |
    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 | {'No 64 | 65 |
    66 | ) : ( 67 |
    75 | )} 76 |
    77 |
    78 |
    79 |
    80 | {link._creator ? ( 81 | 87 | 88 | {link._creator.username} 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 |
  • 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 | 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 | 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 | {this.props.username} 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 | 65 |
    66 | ) : ( 67 |
    68 |
    69 |
    70 |
    71 |
    72 |
    73 |
    74 | 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 |
    96 |
    97 |
    98 | (this.input = node)} 100 | onChange={e => { 101 | this.setState({ url: e.target.value }) 102 | }} 103 | value={this.state.url} 104 | type='text' 105 | required 106 | /> 107 | 108 | 109 | 110 |
    111 |
    112 | 113 |
    114 |
    115 |
    116 | )} 117 | {this.state.showPreview && ( 118 |
      119 | {' '} 126 |
    127 | )} 128 | {this.state.showPreview && ( 129 |
    130 |
    131 | 137 |
    138 |
    139 | 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 |
    1. 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 |
      1. modify or copy the materials;
      2. 76 |
      3. use the materials for any commercial purpose, or for any public display (commercial or non-commercial);
      4. 77 |
      5. attempt to decompile or reverse engineer any software contained on Linklet's website;
      6. 78 |
      7. remove any copyright or other proprietary notations from the materials; or
      8. 79 |
      9. transfer the materials to another person or "mirror" the materials on any other server.
      10. 80 |
      81 |
    2. 82 |
    3. 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.
    4. 85 |
    86 | 87 |

    3. Disclaimer

    88 | 89 |
      90 |
    1. 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.
    2. 94 |
    3. 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.
    4. 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 | --------------------------------------------------------------------------------