├── .gitignore ├── LICENSE ├── README.md ├── components ├── Errorboundary.js ├── FeedList.js ├── ImageWithFallback.js ├── Layout.js ├── Navbar.js ├── PFLoader.js ├── SideNav.js └── TwitterList.js ├── contexts └── DeleteContext.js ├── models └── User.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.js ├── _app.js ├── _document.js ├── account.js ├── add.js ├── api │ ├── auth │ │ └── [...auth0].js │ ├── deleteAccount.js │ ├── deleteData.js │ ├── deleteFeed.js │ ├── findFeed.js │ └── handleUser.js ├── feed │ ├── [category].js │ └── [category] │ │ └── [list].js └── index.js ├── public ├── apple-touch-icon.png ├── cover.png ├── favicon.png ├── fonts │ └── SourceSans3VF-Roman.ttf.woff2 ├── img │ ├── 404.svg │ ├── chevron-down.svg │ ├── header.png │ ├── icon-192.png │ ├── icon-256.png │ ├── icon-512.png │ └── reddit-logo.png ├── logo.png └── site.webmanifest ├── styles ├── normalize.css └── styles.css └── utils ├── db.js └── getYTChannelAvatar.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GeekyChakri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](public/cover.png) 2 | 3 | # Pocket Feed 4 | 5 | The one-stop shop for content you love. 6 | 7 | ## Demo 8 | 9 | Here's a quick demo of the app. 10 | 11 | [Pocket Feed - The one stop shop for content you love](https://www.youtube.com/watch?v=ulBqEc0dbPI) 12 | 13 | ## Run Locally 14 | 15 | Clone the project 16 | 17 | ```bash 18 | git clone https://github.com/GeekyChakri/pocket-feed.git 19 | ``` 20 | 21 | Go to the project directory 22 | 23 | ```bash 24 | cd pocket-feed 25 | ``` 26 | 27 | Install dependencies 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | Create an .env file in root and add your variables 34 | 35 | ``` 36 | AUTH0_SECRET= 37 | AUTH0_BASE_URL= 38 | AUTH0_ISSUER_BASE_URL= 39 | AUTH0_CLIENT_ID= 40 | AUTH0_CLIENT_SECRET= 41 | MONGODB_URI= 42 | TWITTER_BEARER_TOKEN= 43 | YT_API_KEY= 44 | ``` 45 | 46 | Start the app 47 | 48 | ```bash 49 | npm run dev 50 | ``` 51 | 52 | ## Tech Stack 53 | 54 | - **NextJS** 55 | 56 | - **MongoDB** 57 | 58 | - **Auth0** 59 | 60 | - **Framer Motion** 61 | 62 | - **Vercel** 63 | 64 | ## Author 65 | 66 | [GeekyChakri](https://www.github.com/GeekyChakri) 67 | 68 | ## License 69 | 70 | [MIT](LICENSE) 71 | -------------------------------------------------------------------------------- /components/Errorboundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | // Define a state variable to track whether is an error or not 8 | this.state = { hasError: false }; 9 | } 10 | static getDerivedStateFromError(error) { 11 | // Update state so the next render will show the fallback UI 12 | 13 | return { hasError: true }; 14 | } 15 | componentDidCatch(error, errorInfo) { 16 | // You can use your own error logging service here 17 | console.log({ error, errorInfo }); 18 | } 19 | render() { 20 | // Check if the error is thrown 21 | if (this.state.hasError) { 22 | // You can render any custom fallback UI 23 | return ( 24 |
25 |

Oops, there is an error!

26 | 33 |
34 | ); 35 | } 36 | 37 | // Return children components in case of no error 38 | 39 | return this.props.children; 40 | } 41 | } 42 | 43 | export default ErrorBoundary; 44 | -------------------------------------------------------------------------------- /components/FeedList.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | import dynamic from "next/dynamic"; 4 | 5 | import dayjs from "dayjs"; 6 | import relativeTime from "dayjs/plugin/relativeTime"; 7 | 8 | import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; 9 | import { decode } from "html-entities"; 10 | import { HeadphonesAlt } from "@styled-icons/fa-solid/HeadphonesAlt"; 11 | import { ExternalLinkOutline } from "@styled-icons/evaicons-outline/ExternalLinkOutline"; 12 | import { ControllerPlay } from "@styled-icons/entypo/ControllerPlay"; 13 | 14 | const ReactPlayer = dynamic(() => import("react-player"), { 15 | ssr: false, 16 | }); 17 | const ModalVideo = dynamic(() => import("react-modal-video"), { 18 | ssr: false, 19 | }); 20 | 21 | dayjs.extend(relativeTime); 22 | 23 | function FeedList({ feedList, feedLink, feedTitle }) { 24 | let latestPodcastTitle = feedList[0]?.title || ""; 25 | let latestPodcastURL = ""; 26 | if (feedList[0]?.enclosures?.length) { 27 | latestPodcastURL = feedList[0].enclosures[0].url; 28 | } 29 | 30 | const shouldReduceMotion = useReducedMotion(); 31 | 32 | const [isOpen, setIsOpen] = useState(false); 33 | const [videoId, setVideoId] = useState(""); 34 | const [audioURL, setAudioURL] = useState(latestPodcastURL); 35 | const [podcastTitle, setPodcastTitle] = useState(latestPodcastTitle); 36 | const [isPlaying, setIsPlaying] = useState(false); 37 | 38 | const router = useRouter(); 39 | 40 | const isYouTube = router.query.category === "youtube"; 41 | const isPodcast = router.query.category === "podcast"; 42 | 43 | const handleOpen = () => { 44 | setIsOpen(true); 45 | document.body.style.overflowY = "hidden"; 46 | }; 47 | 48 | const handleClose = () => { 49 | setIsOpen(false); 50 | document.body.style.overflowY = "scroll"; 51 | }; 52 | 53 | const PodcastPlayer = () => { 54 | return ( 55 |
56 |

57 | Now Listening 58 | 59 |

60 |

{podcastTitle}

61 | 75 |
76 | ); 77 | }; 78 | 79 | return ( 80 | <> 81 |

{feedTitle}

82 | {isPodcast && feedList[0]?.enclosures.length ? : null} 83 | 84 | {feedList.map((item, i) => { 85 | return ( 86 | ({ 89 | y: shouldReduceMotion ? 0 : -60 * i, 90 | opacity: shouldReduceMotion ? 1 : 0, 91 | }), 92 | show: () => ({ 93 | y: 0, 94 | opacity: 1, 95 | transition: { 96 | delay: i * 0.05, 97 | }, 98 | }), 99 | exit: { 100 | opacity: 0, 101 | }, 102 | }} 103 | initial="hidden" 104 | custom={i} 105 | animate="show" 106 | exit="exit" 107 | href={isYouTube || isPodcast ? null : item.link} 108 | target="__blank" 109 | rel="noopener noreferrer" 110 | className="list__item" 111 | key={i} 112 | > 113 | 114 | 119 | {decode(item.title)} 120 | 121 | {decode(item.content) 122 | .replace(/(<([^>]+)>)/gi, "") 123 | .replace(/(&#[^;]+;)/gi, "") || 124 | decode(item.description) 125 | .replace(/(<([^>]+)>)/gi, "") 126 | .replace(/(&#[^;]+;)/gi, "")} 127 | 128 | 129 | 133 | {dayjs(item.created).fromNow()} 134 | 135 | {isYouTube && ( 136 | 147 | )} 148 | {isPodcast && feedList[0].enclosures.length ? ( 149 | 160 | ) : ( 161 | isPodcast && ( 162 | 168 | Listen 169 | 170 | 171 | ) 172 | )} 173 | 174 | 175 | 176 | 177 | ); 178 | })} 179 | 180 | 181 | 187 | {isYouTube ? "Visit Channel" : "Visit Page"} 188 | 189 | 190 | {isYouTube && ( 191 | 198 | )} 199 | 200 | ); 201 | } 202 | 203 | export default FeedList; 204 | -------------------------------------------------------------------------------- /components/ImageWithFallback.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import Image from "next/image"; 3 | 4 | import { createAvatar } from "@dicebear/core"; 5 | import { initials } from "@dicebear/collection"; 6 | 7 | const ImageWithFallback = ({ title, alt, src, ...props }) => { 8 | const [error, setError] = useState(null); 9 | 10 | const avatar = createAvatar(initials, { 11 | seed: title, 12 | size: 32, 13 | }).toDataUriSync(); 14 | 15 | useEffect(() => { 16 | setError(null); 17 | }, [src]); 18 | 19 | return ( 20 | {alt} 21 | ); 22 | }; 23 | 24 | export default ImageWithFallback; 25 | -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | import Navbar from "./Navbar"; 4 | import SideNav from "./SideNav"; 5 | 6 | function Layout({ children }) { 7 | const router = useRouter(); 8 | 9 | // console.log(router.pathname); 10 | 11 | let sideNavComponent; 12 | switch (router.pathname) { 13 | case "/": 14 | case "/404": 15 | sideNavComponent = null; 16 | break; 17 | default: 18 | sideNavComponent = ; 19 | } 20 | return ( 21 |
22 | 23 | {sideNavComponent} 24 | {children} 25 |
26 | ); 27 | } 28 | 29 | export default Layout; 30 | -------------------------------------------------------------------------------- /components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef, useContext } from "react"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | 5 | import { DeleteOutline } from "@styled-icons/material/DeleteOutline"; 6 | import { Delete } from "@styled-icons/material/Delete"; 7 | import { AddToList } from "@styled-icons/entypo/AddToList"; 8 | import { Darkreader } from "@styled-icons/simple-icons/Darkreader"; 9 | import { User } from "@styled-icons/boxicons-regular/User"; 10 | import { Logout } from "@styled-icons/material-rounded/Logout"; 11 | 12 | import { useUser } from "@auth0/nextjs-auth0"; 13 | import { DeleteContext } from "../contexts/DeleteContext"; 14 | 15 | function Navbar() { 16 | const { isActive, toggleActive } = useContext(DeleteContext); 17 | 18 | const [dropDown, setDropDown] = useState(false); 19 | 20 | const [isDarkMode, setDarkMode] = useState(false); 21 | 22 | const { user, error, isLoading } = useUser(); 23 | 24 | console.log(user); 25 | 26 | const dropDownRef = useRef(null); 27 | 28 | const router = useRouter(); 29 | 30 | const displayDropDown = () => { 31 | setDropDown((prevState) => !prevState); 32 | }; 33 | 34 | const handleClickOutside = (e) => { 35 | if (dropDownRef.current && !dropDownRef.current.contains(e.target)) { 36 | setDropDown(false); 37 | } 38 | }; 39 | 40 | const toggleDarkMode = () => { 41 | setDarkMode((prevMode) => !prevMode); 42 | }; 43 | 44 | useEffect(() => { 45 | const html = document.documentElement; 46 | document.addEventListener("mousedown", handleClickOutside); 47 | isDarkMode ? html.classList.add("dark") : html.classList.remove("dark"); 48 | return () => { 49 | document.removeEventListener("mousedown", handleClickOutside); 50 | }; 51 | }, [isDarkMode]); 52 | 53 | const Logo = () => ( 54 | 55 | logo 56 | 57 | ); 58 | 59 | const DropDown = () => { 60 | return ( 61 |
62 |
63 | profile-pic 70 |
71 | 72 |
73 |
74 | { 77 | router.push("/account"); 78 | setDropDown(false); 79 | }} 80 | > 81 | 82 | Account 83 | 84 |
85 | 86 | 92 |
93 |
94 | ); 95 | }; 96 | 97 | const AuthNav = () => ( 98 | 128 | ); 129 | 130 | if (isLoading) { 131 | return ( 132 | 136 | ); 137 | } 138 | 139 | if (user) { 140 | return ; 141 | } 142 | 143 | return ( 144 | 153 | ); 154 | } 155 | 156 | export default Navbar; 157 | -------------------------------------------------------------------------------- /components/PFLoader.js: -------------------------------------------------------------------------------- 1 | function PFLoader() { 2 | return ( 3 |
4 |

5 | PF 6 | . 7 |

8 |
9 | ); 10 | } 11 | 12 | export default PFLoader; 13 | -------------------------------------------------------------------------------- /components/SideNav.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useRouter } from "next/router"; 3 | 4 | import { Blog } from "@styled-icons/fa-solid/Blog"; 5 | import { Reddit, Youtube } from "@styled-icons/fa-brands"; 6 | import { Podcast } from "@styled-icons/fa-solid/Podcast"; 7 | import { News } from "@styled-icons/boxicons-regular/News"; 8 | 9 | function SideNav() { 10 | const router = useRouter(); 11 | const category = router.query.category; 12 | 13 | return ( 14 |
15 |
    16 |
  • 17 | 21 | 22 | Blog 23 | 24 |
  • 25 | {/*
  • 26 | 27 | 32 | 33 | Twitter 34 | 35 | 36 |
  • */} 37 |
  • 38 | 42 | 43 | Reddit 44 | 45 |
  • 46 |
  • 47 | 53 | 54 | Podcast 55 | 56 |
  • 57 |
  • 58 | 62 | 63 | News 64 | 65 |
  • 66 |
  • 67 | 73 | 74 | YouTube 75 | 76 |
  • 77 |
78 |
79 | ); 80 | } 81 | 82 | export default SideNav; 83 | -------------------------------------------------------------------------------- /components/TwitterList.js: -------------------------------------------------------------------------------- 1 | import { decode } from "html-entities"; 2 | import dayjs from "dayjs"; 3 | 4 | function TwitterList({ tweetList }) { 5 | console.log("MEDIA", tweetList.includes.media); 6 | 7 | return ( 8 | <> 9 |

{tweetList.includes.users[0].name}

10 | {tweetList.data.map((tweet, index) => { 11 | let imageUrl, imageIndex; 12 | if (tweetList.includes?.media) { 13 | imageIndex = tweetList.includes.media.findIndex( 14 | (item) => item.media_key === tweet?.attachments?.media_keys[0] 15 | ); 16 | 17 | // console.log(imageIndex); 18 | // console.log(tweetList.includes.users[0].username); 19 | 20 | imageUrl = 21 | tweetList.includes?.media[imageIndex]?.url || 22 | tweetList.includes?.media[imageIndex]?.preview_image_url; 23 | } 24 | 25 | return ( 26 | 33 | 34 | {dayjs(tweet.created_at).format("DD-MM-YYYY, hh:mm a")} 35 | 36 | {decode(tweet.text)} 37 | {tweetList.includes.media && imageIndex !== -1 && ( 38 | 43 | )} 44 | 45 | ); 46 | })} 47 | 53 | Visit Profile 54 | 55 | 56 | ); 57 | } 58 | 59 | export default TwitterList; 60 | -------------------------------------------------------------------------------- /contexts/DeleteContext.js: -------------------------------------------------------------------------------- 1 | import { useState, createContext } from "react"; 2 | 3 | export const DeleteContext = createContext(); 4 | 5 | function DeleteContextProvider({ children }) { 6 | const [isActive, setIsActive] = useState(false); 7 | 8 | const toggleActive = () => { 9 | setIsActive((prevState) => !prevState); 10 | }; 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | 19 | export default DeleteContextProvider; 20 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | user: String, 5 | feeds: [ 6 | { 7 | title: String, 8 | url: String, 9 | feedUrl: String, 10 | category: String, 11 | favicon: String, 12 | twitterId: String, 13 | feedId: String, 14 | }, 15 | ], 16 | }); 17 | 18 | const User = mongoose.models.User || mongoose.model("User", UserSchema); 19 | 20 | export default User; 21 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compiler: { 3 | removeConsole: process.env.NODE_ENV === "production", 4 | }, 5 | images: { 6 | remotePatterns: [ 7 | { 8 | hostname: "**", 9 | }, 10 | ], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pocket-feed", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@auth0/nextjs-auth0": "^1.5.0", 12 | "@dicebear/collection": "^7.0.4", 13 | "@dicebear/core": "^7.0.4", 14 | "@hughrun/feedfinder": "^1.0.11", 15 | "dayjs": "^1.10.6", 16 | "framer-motion": "^4.1.17", 17 | "html-entities": "^2.3.2", 18 | "mongoose": "^5.13.6", 19 | "nanoid": "^3.1.24", 20 | "next": "^13.4.11", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-hot-toast": "^2.1.0", 24 | "react-modal-video": "^2.0.1", 25 | "react-player": "^2.9.0", 26 | "react-responsive-modal": "^6.4.2", 27 | "rss-finder": "^2.1.5", 28 | "rss-to-json": "^2.0.2", 29 | "sharp": "^0.32.3", 30 | "styled-icons": "^10.47.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/404.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | function Custom404() { 4 | return ( 5 |
6 | 7 |

8 | 9 | An investigation is underway to find this missing page. 10 | 11 | 12 | Meanwhile you can read some interesting articles. 13 | 14 |

15 | 16 | 17 | Read Now 18 | 19 |
20 | ); 21 | } 22 | 23 | export default Custom404; 24 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | import Head from "next/head"; 4 | import Router from "next/router"; 5 | 6 | import { UserProvider } from "@auth0/nextjs-auth0"; 7 | 8 | import "../styles/normalize.css"; 9 | import "../styles/styles.css"; 10 | import "react-modal-video/css/modal-video.min.css"; 11 | 12 | import Layout from "../components/Layout"; 13 | import DeleteContextProvider from "../contexts/DeleteContext"; 14 | import PFLoader from "./../components/PFLoader"; 15 | import ErrorBoundary from "../components/Errorboundary"; 16 | 17 | function MyApp({ Component, pageProps }) { 18 | const [isLoading, setIsLoading] = useState(false); 19 | 20 | useEffect(() => { 21 | const start = () => { 22 | setIsLoading(true); 23 | }; 24 | const end = () => { 25 | setIsLoading(false); 26 | }; 27 | Router.events.on("routeChangeStart", start); 28 | Router.events.on("routeChangeComplete", end); 29 | Router.events.on("routeChangeError", end); 30 | return () => { 31 | Router.events.off("routeChangeStart", start); 32 | Router.events.off("routeChangeComplete", end); 33 | Router.events.off("routeChangeError", end); 34 | }; 35 | }, []); 36 | 37 | return ( 38 | <> 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 59 | 60 | 64 | 65 | 66 | 67 | 68 | Pocket Feed 69 | 70 | 71 | 72 | 73 | {isLoading ? ( 74 | 75 | ) : ( 76 | 77 | 78 | 79 | )} 80 | 81 | 82 | 83 | 84 | ); 85 | } 86 | 87 | export default MyApp; 88 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 15 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default MyDocument; 31 | -------------------------------------------------------------------------------- /pages/account.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { withPageAuthRequired, useUser } from "@auth0/nextjs-auth0"; 3 | 4 | import "react-responsive-modal/styles.css"; 5 | import { Modal } from "react-responsive-modal"; 6 | import toast, { Toaster } from "react-hot-toast"; 7 | 8 | import PFLoader from "./../components/PFLoader"; 9 | 10 | const toastStyles = { 11 | fontSize: "2rem", 12 | fontWeight: "600", 13 | backgroundColor: "#212529", 14 | color: "#fff", 15 | }; 16 | 17 | function Account() { 18 | const [open, setOpen] = useState(false); 19 | 20 | const { user, isLoading } = useUser(); 21 | 22 | const onOpenModal = () => setOpen(true); 23 | const onCloseModal = () => setOpen(false); 24 | 25 | const deleteAccount = async () => { 26 | toast.loading("Deleting...", { 27 | id: "delete", 28 | style: toastStyles, 29 | }); 30 | try { 31 | const urls = ["/api/deleteData", "/api/deleteAccount"]; 32 | const res = await Promise.all(urls.map((url) => fetch(url))); 33 | const status = res.every((r) => r.ok === true); 34 | console.log(status); 35 | if (!status) { 36 | toast.remove("delete"); 37 | toast.error("Something went wrong", { style: toastStyles }); 38 | onCloseModal(); 39 | return; 40 | } 41 | 42 | toast.remove("delete"); 43 | toast.success("Account Deleted", { style: toastStyles }); 44 | onCloseModal(); 45 | 46 | if (typeof window !== "undefined") { 47 | window.location.href = "/api/auth/logout"; 48 | } 49 | } catch (err) { 50 | toast.remove("delete"); 51 | toast.error(err.message, { style: toastStyles }); 52 | } 53 | }; 54 | 55 | if (isLoading) { 56 | return ; 57 | } 58 | 59 | console.log(user); 60 | return ( 61 |
62 |

Hello, {user?.nickname}

63 |
64 | Profile Pic 71 |

{user?.nickname}

72 |
73 |
74 | 77 |
78 | 79 | 99 |
100 |

Delete Account

101 |

102 | All your data will be deleted. This action cannot be undone. Are you 103 | sure ? 104 |

105 | 108 | 111 |
112 |
113 | 114 | 115 |
116 | ); 117 | } 118 | 119 | export default Account; 120 | 121 | export const getServerSideProps = withPageAuthRequired(); 122 | -------------------------------------------------------------------------------- /pages/add.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import router from "next/router"; 3 | 4 | import toast, { Toaster } from "react-hot-toast"; 5 | import { nanoid } from "nanoid"; 6 | import { withPageAuthRequired, useUser } from "@auth0/nextjs-auth0"; 7 | 8 | import ImageWithFallback from "../components/ImageWithFallback"; 9 | 10 | const toastStyles = { 11 | fontSize: "1.6rem", 12 | fontWeight: "600", 13 | backgroundColor: "#181818", 14 | color: "#fff", 15 | }; 16 | 17 | function Add() { 18 | const { user } = useUser(); 19 | 20 | console.log(user?.sub); 21 | 22 | const [formState, setFormState] = useState({ 23 | url: "", 24 | category: "youtube", 25 | }); 26 | 27 | const [feedData, setFeedData] = useState({}); 28 | 29 | const [isLoading, setIsLoading] = useState(false); 30 | 31 | const isObjectEmpty = Object.keys(feedData).length === 0; 32 | 33 | const addFeed = async () => { 34 | toast.loading("Adding...", { 35 | id: "add", 36 | style: toastStyles, 37 | }); 38 | console.log("CLICKED"); 39 | const feedId = nanoid(); 40 | try { 41 | const res = await fetch("/api/handleUser"); 42 | const data = await res.json(); 43 | console.log("USER_FEED_LIST", data); 44 | console.log(data?.[0]?.feeds); 45 | 46 | //CHECK IF FEED ALREADY EXISTS 47 | if (data?.[0]?.feeds) { 48 | const checkFeedExists = data[0].feeds.some( 49 | (feed) => feed.title === feedData.title 50 | ); 51 | if (checkFeedExists) { 52 | toast.remove("add"); 53 | toast("Feed Already Exists", { 54 | style: toastStyles, 55 | }); 56 | return; 57 | } 58 | } 59 | 60 | //UPDATE THE USER WITH A NEW FEED 61 | if (data.length) { 62 | console.log("USER ALREADY EXISTS"); 63 | const res = await fetch("/api/handleUser", { 64 | method: "PUT", 65 | headers: { 66 | "Content-Type": "application/json", 67 | }, 68 | body: JSON.stringify({ 69 | title: feedData.title, 70 | url: feedData.url || formState.url, 71 | feedUrl: feedData.feed, 72 | favicon: feedData.favicon, 73 | category: formState.category, 74 | twitterId: feedData.twitterId, 75 | feedId, 76 | }), 77 | }); 78 | console.log(res); 79 | if (!res.ok) { 80 | throw new Error("Something went wrong"); 81 | } 82 | toast.remove("add"); 83 | router.push(`/feed/${formState.category}`); 84 | return; 85 | } 86 | 87 | //IF USER DOESN'T EXIST CREATE A NEW USER 88 | if (!data.length) { 89 | console.log("USER DOESN'T EXIST"); 90 | const res = await fetch("/api/handleUser", { 91 | method: "POST", 92 | headers: { 93 | "Content-Type": "application/json", 94 | }, 95 | body: JSON.stringify({ 96 | user: user?.sub, 97 | feeds: [ 98 | { 99 | title: feedData.title, 100 | url: feedData.url || formState.url, 101 | feedUrl: feedData.feed, 102 | favicon: feedData.favicon, 103 | category: formState.category, 104 | twitterId: feedData.twitterId, 105 | feedId, 106 | }, 107 | ], 108 | }), 109 | }); 110 | if (!res.ok) { 111 | throw new Error("Something went wrong"); 112 | } 113 | toast.remove("add"); 114 | router.push(`/feed/${formState.category}`); 115 | } 116 | } catch (err) { 117 | console.log("ERROR"); 118 | toast.remove("add"); 119 | toast.error(err.message, { 120 | style: toastStyles, 121 | }); 122 | } 123 | }; 124 | 125 | const handleChange = (e) => { 126 | setFeedData({}); 127 | setFormState({ 128 | ...formState, 129 | [e.target.name]: e.target.value, 130 | }); 131 | }; 132 | 133 | const handleSubmit = async (e) => { 134 | setIsLoading(true); 135 | e.preventDefault(); 136 | 137 | try { 138 | const res = await fetch("/api/findFeed", { 139 | method: "POST", 140 | headers: { 141 | "Content-Type": "application/json", 142 | }, 143 | body: JSON.stringify(formState), 144 | }); 145 | console.log(res); 146 | const responseData = await res.json(); 147 | console.log(responseData); 148 | if (!res.ok) { 149 | throw new Error(responseData.msg); 150 | } 151 | setFeedData(responseData); 152 | setIsLoading(false); 153 | } catch (err) { 154 | setIsLoading(false); 155 | toast.error(err.message, { 156 | style: toastStyles, 157 | }); 158 | } 159 | }; 160 | 161 | console.log("TWITTER_ID", feedData.twitterId); 162 | 163 | return ( 164 |
165 |

Add a Feed

166 |
167 | 170 | 179 | 182 | 196 | {isObjectEmpty && ( 197 | 200 | )} 201 |
202 | {!isObjectEmpty && ( 203 |
204 | {feedData.favicon ? ( 205 | 214 | ) : ( 215 |
216 | {feedData.title.split("")[0]} 217 |
218 | )} 219 | 220 |

{feedData.title}

221 |

{feedData.url || formState.url}

222 | 225 |
226 | )} 227 | 228 |
229 | ); 230 | } 231 | 232 | export default Add; 233 | 234 | export const getServerSideProps = withPageAuthRequired(); 235 | -------------------------------------------------------------------------------- /pages/api/auth/[...auth0].js: -------------------------------------------------------------------------------- 1 | import { handleAuth, handleCallback } from "@auth0/nextjs-auth0"; 2 | 3 | export default handleAuth({ 4 | async callback(req, res) { 5 | try { 6 | await handleCallback(req, res, {}); 7 | } catch (error) { 8 | res.redirect("/"); 9 | } 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /pages/api/deleteAccount.js: -------------------------------------------------------------------------------- 1 | import { withApiAuthRequired, getSession } from "@auth0/nextjs-auth0"; 2 | 3 | const bodyData = { 4 | client_id: process.env.AUTH0_CLIENT_ID, 5 | client_secret: process.env.AUTH0_CLIENT_SECRET, 6 | audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`, 7 | grant_type: "client_credentials", 8 | }; 9 | 10 | export default withApiAuthRequired(async (req, res) => { 11 | const session = getSession(req, res); 12 | 13 | try { 14 | const response = await fetch( 15 | `${process.env.AUTH0_ISSUER_BASE_URL}/oauth/token`, 16 | { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify(bodyData), 22 | } 23 | ); 24 | const data = await response.json(); 25 | 26 | const user = await fetch( 27 | `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/users/${session?.user.sub}`, 28 | { 29 | method: "DELETE", 30 | headers: { 31 | Authorization: `Bearer ${data.access_token}`, 32 | }, 33 | } 34 | ); 35 | res.status(200).json({}); 36 | } catch (err) { 37 | console.log(err); 38 | res.status(500).json({ msg: "Something went wrong" }); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /pages/api/deleteData.js: -------------------------------------------------------------------------------- 1 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; 2 | 3 | import dbConnect from "../../utils/db"; 4 | import User from "../../models/User"; 5 | 6 | export default withApiAuthRequired(async (req, res) => { 7 | const { user } = await getSession(req, res); 8 | 9 | await dbConnect(); 10 | 11 | try { 12 | const response = await User.deleteOne({ user: user.sub }); 13 | res.status(200).json({}); 14 | } catch (err) { 15 | res.status(500).json({ msg: "Something went wrong" }); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /pages/api/deleteFeed.js: -------------------------------------------------------------------------------- 1 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; 2 | 3 | import dbConnect from "../../utils/db"; 4 | import User from "./../../models/User"; 5 | 6 | export default withApiAuthRequired(async (req, res) => { 7 | const { user } = getSession(req, res); 8 | console.log(user.sub); 9 | const { feedId } = req.body; 10 | 11 | await dbConnect(); 12 | 13 | try { 14 | const doc = await User.updateOne( 15 | { user: user.sub }, 16 | { $pull: { feeds: { feedId } } } 17 | ); 18 | // console.log(doc); 19 | res.status(200).json({}); 20 | } catch (err) { 21 | res.status(500).json({ msg: "Something went wrong" }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /pages/api/findFeed.js: -------------------------------------------------------------------------------- 1 | import feedFinder from "@hughrun/feedfinder"; 2 | import rssFinder from "rss-finder"; 3 | 4 | import { withApiAuthRequired } from "@auth0/nextjs-auth0"; 5 | import { getYTChannelAvatar } from "../../utils/getYTChannelAvatar"; 6 | 7 | export default withApiAuthRequired(async (req, res) => { 8 | const { url, category } = req.body; 9 | 10 | switch (category) { 11 | case "twitter": { 12 | const username = url.split("/").pop(); 13 | try { 14 | const response = await fetch( 15 | `https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,protected`, 16 | { 17 | headers: { 18 | "Content-Type": "application/json", 19 | Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`, 20 | }, 21 | } 22 | ); 23 | 24 | const user = await response.json(); 25 | 26 | if (user.errors) { 27 | return res.status(404).json({ msg: "User Not Found" }); 28 | } 29 | //If PROTECTED 30 | if (user.data.protected) { 31 | return res.status(401).json({ msg: "Private Account" }); 32 | } 33 | 34 | const { 35 | name: title, 36 | profile_image_url: favicon, 37 | id: twitterId, 38 | } = user.data; 39 | 40 | res.status(200).json({ 41 | title, 42 | favicon, 43 | twitterId, 44 | }); 45 | } catch (err) { 46 | console.log("ERROR", err); 47 | res.status(500).json({ msg: "Something went wrong" }); 48 | } 49 | break; 50 | } 51 | case "reddit": { 52 | try { 53 | const response = await rssFinder(`${url}.rss`); 54 | console.log(response); 55 | const data = { 56 | url: response.site.url, 57 | feed: response.feedUrls[0].url, 58 | title: response.site.title, 59 | favicon: "/img/reddit-logo.png", 60 | }; 61 | res.status(200).json(data); 62 | } catch (err) { 63 | console.log(err); 64 | res.status(500).json({ 65 | msg: "Something went wrong.", 66 | }); 67 | } 68 | break; 69 | } 70 | default: { 71 | try { 72 | const res1 = await rssFinder(url); 73 | console.log("RES1", res1); 74 | 75 | // YouTube FAVICON 76 | let ytFavicon; 77 | if (category === "youtube") { 78 | console.log("YOUTUBE"); 79 | const channelId = res1.feedUrls[0].url.split("?")[1].split("=")[1]; 80 | console.log("CHANNEL_ID", channelId); 81 | const ytChannelAvatarUrl = await getYTChannelAvatar(channelId); 82 | ytFavicon = ytChannelAvatarUrl; 83 | } 84 | 85 | console.log("YT_FAVICON", ytFavicon); 86 | 87 | if (!res1.feedUrls.length || res1.feedUrls.length > 3) { 88 | try { 89 | const res2 = await feedFinder.getFeed(url); 90 | console.log("RES2", res2); 91 | res.status(200).json({ 92 | ...res2, 93 | feed: res2.feed, 94 | favicon: category === "youtube" ? ytFavicon : res1.site.favicon, 95 | }); 96 | return; 97 | } catch (err) { 98 | console.log(err); 99 | return res.status(500).json({ msg: "No Feed Found" }); 100 | } 101 | } 102 | const data = { 103 | url: res1.site.url, 104 | feed: res1.feedUrls[0].url, 105 | title: res1.site.title, 106 | favicon: category === "youtube" ? ytFavicon : res1.site.favicon, 107 | }; 108 | res.status(200).json(data); 109 | } catch (err) { 110 | console.log("ERROR", err); 111 | res.status(500).json({ 112 | msg: "Something went wrong. Please make sure you've selected the correct category.", 113 | }); 114 | } 115 | } 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /pages/api/handleUser.js: -------------------------------------------------------------------------------- 1 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; 2 | 3 | // import { connect } from "../../utils/db"; 4 | import dbConnect from "../../utils/db"; 5 | import User from "../../models/User"; 6 | 7 | // connect(); 8 | 9 | export default withApiAuthRequired(async (req, res) => { 10 | console.log("REQ_BODY", req.body); 11 | console.log("REQ_METHOD", req.method); 12 | 13 | await dbConnect(); 14 | 15 | const { user } = await getSession(req, res); 16 | 17 | switch (req.method) { 18 | case "GET": { 19 | try { 20 | const data = await User.find({ user: user?.sub }); 21 | res.status(200).json(data); 22 | } catch (err) {} 23 | break; 24 | } 25 | case "POST": { 26 | try { 27 | const user = await User.create(req.body); 28 | res.status(200).json({ user }); 29 | } catch (err) {} 30 | break; 31 | } 32 | case "PUT": { 33 | try { 34 | const feed = await User.findOneAndUpdate( 35 | { user: user?.sub }, 36 | { 37 | $push: { 38 | feeds: { 39 | title: req.body.title, 40 | url: req.body.url, 41 | feedUrl: req.body.feedUrl, 42 | category: req.body.category, 43 | favicon: req.body.favicon, 44 | twitterId: req.body.twitterId, 45 | feedId: req.body.feedId, 46 | }, 47 | }, 48 | }, 49 | { 50 | new: true, 51 | } 52 | ); 53 | res.status(200).json(feed); 54 | } catch (err) { 55 | res.status(500).json({ msg: "Something went wrong" }); 56 | } 57 | break; 58 | } 59 | default: 60 | res.status(500).json({ msg: "Something went wrong" }); 61 | break; 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /pages/feed/[category].js: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from "react"; 2 | import { useRouter } from "next/router"; 3 | import Link from "next/link"; 4 | 5 | import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; 6 | import { ChevronRight } from "@styled-icons/boxicons-solid/ChevronRight"; 7 | import { Trash } from "@styled-icons/boxicons-solid/Trash"; 8 | import { Plus } from "@styled-icons/boxicons-regular/Plus"; 9 | import { FileBlank } from "@styled-icons/boxicons-regular/FileBlank"; 10 | import toast, { Toaster } from "react-hot-toast"; 11 | import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0"; 12 | 13 | import dbConnect from "../../utils/db"; 14 | import User from "./../../models/User"; 15 | import { DeleteContext } from "../../contexts/DeleteContext"; 16 | import ImageWithFallback from "../../components/ImageWithFallback"; 17 | 18 | const toastStyles = { 19 | fontSize: "2rem", 20 | fontWeight: "600", 21 | backgroundColor: "#181818", 22 | color: "#fff", 23 | }; 24 | 25 | function CategoryFeed({ feedData }) { 26 | const shouldReduceMotion = useReducedMotion(); 27 | 28 | const { isActive } = useContext(DeleteContext); 29 | 30 | const [feed, setFeed] = useState(feedData); 31 | 32 | const router = useRouter(); 33 | console.log(router.query.category); 34 | 35 | console.log("FEED", feed); 36 | 37 | const deleteFeed = async (feedId) => { 38 | try { 39 | const res = await fetch("/api/deleteFeed", { 40 | method: "POST", 41 | headers: { 42 | "Content-Type": "application/json", 43 | }, 44 | body: JSON.stringify({ feedId }), 45 | }); 46 | 47 | const responseData = await res.json(); 48 | if (!res.ok) { 49 | throw new Error(responseData.msg); 50 | } 51 | const prevFeed = [...feed]; 52 | const newFeed = prevFeed.filter((feed) => feed.feedId !== feedId); 53 | setFeed(newFeed); 54 | } catch (err) { 55 | toast.error(err.message, { 56 | style: toastStyles, 57 | }); 58 | } 59 | }; 60 | 61 | return ( 62 | <> 63 | {feed.length ? ( 64 |
65 | 66 | {feed.map((item, i) => ( 67 | ({ 72 | y: shouldReduceMotion ? 0 : -60 * i, 73 | opacity: shouldReduceMotion ? 1 : 0, 74 | }), 75 | show: () => ({ 76 | y: 0, 77 | opacity: 1, 78 | transition: { 79 | delay: i * 0.05, 80 | }, 81 | }), 82 | exit: { 83 | opacity: 0, 84 | }, 85 | }} 86 | initial="hidden" 87 | custom={i} 88 | animate="show" 89 | exit="exit" 90 | > 91 | 95 | {item.favicon ? ( 96 | 105 | ) : ( 106 | 107 | {item.title.split("")[0]} 108 | 109 | )} 110 | {item.title} 111 | 112 | 113 |
114 | {isActive ? ( 115 | 122 | ) : ( 123 | 133 | )} 134 |
135 |
136 | ))} 137 |
138 | 139 |
140 | ) : ( 141 |
142 | 143 |

No Feeds Yet

144 |

145 | The feed you add will appear here. 146 |

147 | 148 | 149 | Add a feed 150 | 151 |
152 | )} 153 | 154 | ); 155 | } 156 | 157 | export default CategoryFeed; 158 | 159 | export const getServerSideProps = withPageAuthRequired({ 160 | async getServerSideProps(context) { 161 | const { req, res, query } = context; 162 | 163 | await dbConnect(); 164 | 165 | // console.log("QUERY", query.category); 166 | 167 | const session = await getSession(req, res); 168 | 169 | try { 170 | const data = await User.find({ user: session?.user.sub }); 171 | 172 | console.log("SERVER_SIDE_DATA", data[0].feeds); 173 | 174 | const filteredData = data[0].feeds.filter( 175 | (item) => item.category === query.category 176 | ); 177 | console.log("FILTERED_DATA", filteredData); 178 | 179 | return { 180 | props: { 181 | feedData: JSON.parse(JSON.stringify(filteredData)), 182 | }, 183 | }; 184 | } catch (err) { 185 | return { 186 | props: { 187 | err: "Something went wrong", 188 | feedData: [], 189 | }, 190 | }; 191 | } 192 | }, 193 | }); 194 | -------------------------------------------------------------------------------- /pages/feed/[category]/[list].js: -------------------------------------------------------------------------------- 1 | import { getSession, withPageAuthRequired } from "@auth0/nextjs-auth0"; 2 | import parse from "rss-to-json"; 3 | 4 | import dbConnect from "../../../utils/db"; 5 | import User from "./../../../models/User"; 6 | import FeedList from "../../../components/FeedList"; 7 | 8 | function List({ feedList, feedLink, feedTitle }) { 9 | console.log(feedLink); 10 | // console.log("ROUTER", router.query.category); 11 | // console.log(router.query.id); 12 | 13 | return ( 14 |
15 | 21 |
22 | ); 23 | } 24 | 25 | export default List; 26 | 27 | export const getServerSideProps = withPageAuthRequired({ 28 | async getServerSideProps(context) { 29 | const { req, res, query } = context; 30 | console.log(query.id); 31 | 32 | await dbConnect(); 33 | 34 | const session = await getSession(req, res); 35 | 36 | try { 37 | const data = await User.findOne({ user: session?.user.sub }).select({ 38 | feeds: { $elemMatch: { feedId: query.id } }, 39 | }); 40 | console.log("SERVER_DATA", data); 41 | 42 | if (query.category === "twitter") { 43 | const response = await fetch( 44 | `https://api.twitter.com/2/users/${data.feeds[0].twitterId}/tweets?expansions=attachments.media_keys,author_id&exclude=retweets,replies&media.fields=url,preview_image_url&tweet.fields=created_at&user.fields=username,verified&max_results=5`, 45 | { 46 | headers: { 47 | "Content-Type": "application/json", 48 | Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`, 49 | }, 50 | } 51 | ); 52 | 53 | const userTweets = await response.json(); 54 | 55 | console.log(userTweets); 56 | 57 | return { 58 | props: { 59 | tweetList: userTweets, 60 | }, 61 | }; 62 | } 63 | 64 | console.log(data.feeds[0].feedUrl); 65 | 66 | const feed = await parse(data.feeds[0].feedUrl); 67 | 68 | return { 69 | props: { 70 | feedList: JSON.parse(JSON.stringify(feed.items.splice(0, 10))), 71 | feedLink: feed?.link[1].href || feed.link, 72 | feedTitle: feed.title, 73 | }, 74 | }; 75 | } catch (err) { 76 | return { 77 | props: { 78 | msg: "Something went wrong", 79 | feedList: [], 80 | feedLink: "", 81 | tweetList: [], 82 | }, 83 | }; 84 | } 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import dynamic from "next/dynamic"; 3 | import Image from "next/image"; 4 | 5 | import { getSession } from "@auth0/nextjs-auth0"; 6 | import { motion, useReducedMotion } from "framer-motion"; 7 | 8 | import { ChevronRight } from "@styled-icons/entypo/ChevronRight"; 9 | import { Video } from "@styled-icons/fa-solid/Video"; 10 | 11 | const ModalVideo = dynamic(() => import("react-modal-video"), { 12 | ssr: false, 13 | }); 14 | 15 | function Home() { 16 | const [isOpen, setIsOpen] = useState(false); 17 | 18 | const shouldReduceMotion = useReducedMotion(); 19 | const container = { 20 | hidden: { opacity: shouldReduceMotion ? 1 : 0 }, 21 | show: { 22 | opacity: 1, 23 | transition: { 24 | staggerChildren: 0.5, 25 | }, 26 | }, 27 | }; 28 | 29 | const item = { 30 | hidden: { opacity: shouldReduceMotion ? 1 : 0 }, 31 | show: { opacity: 1 }, 32 | }; 33 | 34 | const fadeInRight = { 35 | hidden: { 36 | x: shouldReduceMotion ? 0 : 60, 37 | opacity: shouldReduceMotion ? 1 : 0, 38 | }, 39 | show: { 40 | x: 0, 41 | opacity: 1, 42 | transition: { 43 | duration: 0.5, 44 | }, 45 | }, 46 | }; 47 | 48 | const fadeInLeft = { 49 | hidden: { 50 | x: shouldReduceMotion ? 0 : -60, 51 | opacity: shouldReduceMotion ? 1 : 0, 52 | }, 53 | show: { 54 | x: 0, 55 | opacity: 1, 56 | transition: { 57 | duration: 0.5, 58 | }, 59 | }, 60 | }; 61 | 62 | return ( 63 | <> 64 | 70 | 71 | 72 | 73 | More Signal. Less Noise. 74 | 75 | 76 | The one stop shop for content you love. 77 | 78 | 79 | 80 | 81 | TRY NOW 82 | 83 | 84 | 88 | 89 | 90 | Made with love by{" "} 91 | 97 | Chakri 98 | 99 | 100 | 101 | 102 | illustration 108 | 109 | 110 | setIsOpen(false)} 116 | /> 117 | 118 | ); 119 | } 120 | 121 | export default Home; 122 | 123 | export async function getServerSideProps(context) { 124 | const { req, res } = context; 125 | const session = getSession(req, res); 126 | 127 | if (session?.user) { 128 | return { 129 | redirect: { 130 | destination: "/feed/blog", 131 | permanent: false, 132 | }, 133 | }; 134 | } 135 | return { 136 | props: {}, 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/cover.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/SourceSans3VF-Roman.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/fonts/SourceSans3VF-Roman.ttf.woff2 -------------------------------------------------------------------------------- /public/img/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/chevron-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/img/header.png -------------------------------------------------------------------------------- /public/img/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/img/icon-192.png -------------------------------------------------------------------------------- /public/img/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/img/icon-256.png -------------------------------------------------------------------------------- /public/img/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/img/icon-512.png -------------------------------------------------------------------------------- /public/img/reddit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/img/reddit-logo.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekychakri/pocket-feed/b58e4b519d6f204d36933d117b0261925c266e98/public/logo.png -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Pocket Feed", 3 | "name": "Pocket Feed", 4 | "description": "The one-stop shop for content you love", 5 | "start_url": "/", 6 | "background_color": "#fcf8f4", 7 | "theme_color": "#fcf8f4", 8 | "display": "standalone", 9 | "orientation": "portrait-primary", 10 | "icons": [ 11 | { 12 | "src": "img/icon-192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "./img/icon-256.png", 18 | "type": "image/png", 19 | "sizes": "256x256" 20 | }, 21 | { 22 | "src": "./img/icon-512.png", 23 | "type": "image/png", 24 | "sizes": "512x512" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type="button"], 199 | [type="reset"], 200 | [type="submit"] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type="button"]::-moz-focus-inner, 210 | [type="reset"]::-moz-focus-inner, 211 | [type="submit"]::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type="button"]:-moz-focusring, 222 | [type="reset"]:-moz-focusring, 223 | [type="submit"]:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type="checkbox"], 273 | [type="radio"] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type="number"]::-webkit-inner-spin-button, 283 | [type="number"]::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type="search"] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10+. 339 | */ 340 | 341 | template { 342 | display: none; 343 | } 344 | 345 | /** 346 | * Add the correct display in IE 10. 347 | */ 348 | 349 | [hidden] { 350 | display: none; 351 | } 352 | -------------------------------------------------------------------------------- /styles/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Source Sans 3 VF"; 3 | font-weight: 200 900; 4 | font-style: normal; 5 | font-stretch: normal; 6 | font-display: swap; 7 | src: url("/fonts/SourceSans3VF-Roman.ttf.woff2") format("woff2"); 8 | } 9 | 10 | html { 11 | --color-bg: #fcf8f4; 12 | --color-text: #181818; 13 | --color-secondary-dark: #212529; 14 | --color-card: #ffffff; 15 | --color-white: #ffffff; 16 | --color-dark: #212121; 17 | --color-btn: #212529; 18 | --color-btn-text: #ffffff; 19 | --color-border: #eaeaea; 20 | --color-label: #eaeaea; 21 | --color-nav-bg: #fcf8f4; 22 | } 23 | 24 | html.dark { 25 | --color-bg: #121212; 26 | --color-text: #ffffff; 27 | --color-card: #212529; 28 | --color-btn: #ffffff; 29 | --color-border: #242424; 30 | --color-btn-text: #181818; 31 | --color-label: #181818; 32 | --color-nav-bg: #181818; 33 | } 34 | 35 | * { 36 | margin: 0; 37 | padding: 0; 38 | box-sizing: border-box; 39 | } 40 | 41 | html { 42 | font-size: 62.5%; 43 | } 44 | 45 | html, 46 | body, 47 | #__next { 48 | height: 100%; 49 | } 50 | 51 | body { 52 | font-family: "Source Sans 3 VF", sans-serif; 53 | background-color: var(--color-bg); 54 | color: var(--color-text); 55 | line-height: 1.6; 56 | overflow-x: hidden; 57 | user-select: none; 58 | -webkit-font-smoothing: antialiased; 59 | -moz-osx-font-smoothing: grayscale; 60 | } 61 | 62 | /*Content*/ 63 | .content { 64 | min-height: 100%; 65 | display: flex; 66 | flex-direction: column; 67 | } 68 | 69 | /*Navigation*/ 70 | .navigation { 71 | background-color: var(--color-bg); 72 | position: sticky; 73 | top: 0; 74 | display: flex; 75 | align-items: center; 76 | padding: 1rem 2rem; 77 | box-shadow: rgb(0 0 0 / 5%) 0px 2px 9px; 78 | z-index: 10; 79 | } 80 | 81 | .navigation__logo { 82 | font-size: 4rem; 83 | font-weight: 700; 84 | margin-right: auto; 85 | display: flex; 86 | align-items: center; 87 | } 88 | 89 | .navigation > a { 90 | text-decoration: none; 91 | } 92 | 93 | .navigation__login, 94 | .navigation__signup { 95 | text-decoration: none; 96 | padding: 1rem 2rem; 97 | border-radius: 5px; 98 | background-color: var(--color-primary-dark); 99 | background-color: var(--color-btn); 100 | font-size: 1.6rem; 101 | font-weight: 600; 102 | letter-spacing: 0.5px; 103 | } 104 | 105 | .navigation__signup { 106 | margin-left: 2rem; 107 | color: var(--color-white); 108 | } 109 | 110 | .navigation__login { 111 | background-color: transparent; 112 | color: var(--color-text); 113 | } 114 | 115 | .navigation__login, 116 | .btn-video { 117 | opacity: 0.7; 118 | transition: opacity 0.2s; 119 | } 120 | 121 | .navigation__login:hover, 122 | .btn-video:hover { 123 | opacity: 1; 124 | } 125 | 126 | .navigation__dark-reader, 127 | .navigation__delete, 128 | .navigation__add { 129 | color: inherit; 130 | } 131 | 132 | .navigation__dark-reader, 133 | .navigation__delete { 134 | background-color: transparent; 135 | border: none; 136 | cursor: pointer; 137 | } 138 | 139 | /*DROP DOWN MENU*/ 140 | .dropdown { 141 | position: relative; 142 | } 143 | 144 | .dropdown__header { 145 | width: 4rem; 146 | height: 4rem; 147 | display: flex; 148 | } 149 | 150 | .dropdown__header-img { 151 | border-radius: 50%; 152 | object-fit: cover; 153 | cursor: pointer; 154 | } 155 | 156 | .dropdown__list { 157 | background-color: var(--color-card); 158 | padding: 1rem 2rem; 159 | position: absolute; 160 | top: 6.5rem; 161 | right: 0; 162 | display: none; 163 | border: 1px solid var(--color-btn); 164 | border-radius: 4px; 165 | box-shadow: 4px 4px 0 var(--color-btn); 166 | } 167 | 168 | .dropdown__list.active { 169 | display: block; 170 | } 171 | 172 | .dropdown__item { 173 | border-radius: 5px; 174 | cursor: pointer; 175 | transition: all 0.2s; 176 | color: var(--color-text); 177 | } 178 | 179 | .dropdown__item:hover { 180 | background-color: var(--color-text); 181 | color: var(--color-card); 182 | } 183 | 184 | .dropdown__link { 185 | text-decoration: none; 186 | font-size: 1.9rem; 187 | font-weight: 600; 188 | color: inherit; 189 | display: flex; 190 | align-items: center; 191 | padding: 1rem; 192 | } 193 | 194 | .dropdown__link-text { 195 | margin-left: 1rem; 196 | } 197 | 198 | /*Account*/ 199 | .account { 200 | flex: 1; 201 | /* background-color: skyblue; */ 202 | display: flex; 203 | justify-content: center; 204 | align-items: center; 205 | flex-direction: column; 206 | margin-bottom: 10rem; 207 | } 208 | 209 | .account__heading { 210 | font-size: 3rem; 211 | margin-bottom: 3rem; 212 | } 213 | 214 | .account__details { 215 | display: flex; 216 | align-items: center; 217 | margin-bottom: 4rem; 218 | } 219 | 220 | .account__details-avatar { 221 | border-radius: 50%; 222 | } 223 | 224 | .account__details-username { 225 | font-size: 2rem; 226 | font-weight: 500; 227 | } 228 | 229 | .account__delete-btn { 230 | padding: 1.5rem 5rem; 231 | background-color: var(--color-btn); 232 | color: var(--color-btn-text); 233 | border: none; 234 | border-radius: 5px; 235 | font-size: 2rem; 236 | font-weight: 600; 237 | cursor: pointer; 238 | } 239 | 240 | /*Home*/ 241 | .header { 242 | flex: 1; 243 | /* background-color: skyblue; */ 244 | display: flex; 245 | padding: 3rem; 246 | line-height: 1.4; 247 | } 248 | 249 | .header__text-box { 250 | flex: 1; 251 | /* background-color: skyblue; */ 252 | display: flex; 253 | justify-content: center; 254 | /* align-items: center; */ 255 | flex-direction: column; 256 | padding-left: 7rem; 257 | } 258 | 259 | .header__img { 260 | flex: 1; 261 | display: flex; 262 | align-items: center; 263 | justify-content: center; 264 | /* background-color: pink; */ 265 | } 266 | 267 | .heading-primary { 268 | /* text-align: center; */ 269 | margin-bottom: 5rem; 270 | } 271 | 272 | .heading-primary--main { 273 | font-size: 3.2rem; 274 | display: inline-block; 275 | margin-bottom: 1.5rem; 276 | font-weight: 600; 277 | color: rgba(24, 24, 24, 0.6); 278 | } 279 | 280 | .heading-primary--sub { 281 | display: block; 282 | font-size: 5rem; 283 | } 284 | 285 | .header__btns { 286 | /* background-color: pink; */ 287 | width: 90%; 288 | display: flex; 289 | justify-content: center; 290 | margin-bottom: 5rem; 291 | } 292 | 293 | .btn-main, 294 | .btn-video { 295 | flex: 1; 296 | display: inline-flex; 297 | align-items: center; 298 | justify-content: center; 299 | border: none; 300 | padding: 1.5rem 0rem; 301 | font-size: 2rem; 302 | font-weight: 600; 303 | border-radius: 5px; 304 | cursor: pointer; 305 | /* border: 2px solid #fff; */ 306 | transition: all 0.2s; 307 | letter-spacing: 0.5px; 308 | } 309 | 310 | .btn-main { 311 | text-decoration: none; 312 | background-color: var(--color-btn); 313 | color: var(--color-white); 314 | } 315 | 316 | .btn-video { 317 | margin-left: 4rem; 318 | background-color: transparent; 319 | } 320 | 321 | .maker { 322 | font-size: 2rem; 323 | font-weight: 400; 324 | } 325 | 326 | .maker__connect { 327 | color: var(--color-text); 328 | font-weight: 600; 329 | } 330 | 331 | /*SideNav*/ 332 | .sidenav { 333 | display: flex; 334 | background-color: var(--color-nav-bg); 335 | position: fixed; 336 | right: 0; 337 | bottom: 0; 338 | left: 0; 339 | border-top: 4px solid var(--color-card); 340 | overflow: auto; 341 | white-space: nowrap; 342 | z-index: 10; 343 | } 344 | 345 | .sidenav__list { 346 | /* height: 100%; 347 | display: flex; */ 348 | flex: 1; 349 | display: flex; 350 | flex-wrap: nowrap; 351 | overflow: auto; 352 | -webkit-overflow-scrolling: touch; 353 | -ms-overflow-style: -ms-autohiding-scrollbar; 354 | } 355 | 356 | .sidenav__item { 357 | /* width: 20%; */ 358 | /* text-align: center; */ 359 | flex: 1; 360 | margin-top: 0.5rem; 361 | padding: 0.8rem 3rem; 362 | display: inline-block; 363 | } 364 | 365 | .sidenav__link { 366 | display: flex; 367 | flex-direction: column; 368 | align-items: center; 369 | justify-content: center; 370 | /* background-color: skyblue; */ 371 | text-decoration: none; 372 | color: inherit; 373 | font-size: 1.9rem; 374 | font-weight: 600; 375 | opacity: 0.4; 376 | } 377 | 378 | .sidenav__link.active { 379 | opacity: 1; 380 | } 381 | 382 | /*Add*/ 383 | .add { 384 | width: 40%; 385 | flex: 1; 386 | display: flex; 387 | flex-direction: column; 388 | padding: 3rem; 389 | margin: 0 auto; 390 | } 391 | 392 | .add__heading { 393 | align-self: flex-start; 394 | font-size: 3rem; 395 | margin-bottom: 4rem; 396 | } 397 | 398 | .add__form { 399 | /* background-color: skyblue; */ 400 | width: 100%; 401 | } 402 | 403 | .add__label { 404 | display: inline-block; 405 | margin-bottom: 1rem; 406 | font-size: 2rem; 407 | font-weight: 600; 408 | } 409 | 410 | .add__input, 411 | .add__select { 412 | width: 100%; 413 | color: var(--color-text); 414 | display: block; 415 | margin-bottom: 3rem; 416 | font-size: 2rem; 417 | padding: 2rem; 418 | border: 2px solid var(--color-border); 419 | border-radius: 5px; 420 | outline: none; 421 | /* background-color: transparent; */ 422 | background-color: var(--color-card); 423 | transition: all 0.2s; 424 | } 425 | 426 | .add__input:focus, 427 | .add__select:focus { 428 | border-color: var(--color-btn); 429 | } 430 | 431 | .add__select { 432 | -webkit-appearance: none; 433 | appearance: none; 434 | background: url("./../public/img/chevron-down.svg") no-repeat right 435 | var(--color-card); 436 | background-position-x: calc(100% - 15px); 437 | } 438 | 439 | .add__btn { 440 | width: 100%; 441 | height: 5rem; 442 | font-size: 2rem; 443 | font-weight: 600; 444 | padding: 1.5rem 0; 445 | background-color: var(--color-btn); 446 | color: var(--color-btn-text); 447 | border: none; 448 | border-radius: 5px; 449 | cursor: pointer; 450 | display: flex; 451 | align-items: center; 452 | justify-content: center; 453 | } 454 | 455 | .add__feed { 456 | background-color: var(--color-card); 457 | border: 2px solid var(--color-border); 458 | padding: 1.5rem 3rem; 459 | border-radius: 5px; 460 | word-break: break-all; 461 | } 462 | 463 | .add__feed-icon { 464 | border-radius: 100%; 465 | margin-bottom: 1rem; 466 | } 467 | 468 | .add__feed-image { 469 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", 470 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 471 | /* flex: 1; */ 472 | background-color: var(--color-text); 473 | color: var(--color-card); 474 | border-radius: 50%; 475 | display: flex; 476 | align-items: center; 477 | justify-content: center; 478 | font-size: 1.7rem; 479 | font-weight: 700; 480 | } 481 | 482 | /*Image Placeholder*/ 483 | .add__default-img { 484 | width: 25px; 485 | height: 25px; 486 | margin-bottom: 1rem; 487 | } 488 | 489 | .category__default-img { 490 | /*TODO:*/ 491 | min-width: 30px; 492 | min-height: 30px; 493 | } 494 | 495 | .add__feed-title { 496 | font-size: 2.2rem; 497 | margin-bottom: 1rem; 498 | } 499 | 500 | .add__feed-url { 501 | font-size: 2rem; 502 | margin-bottom: 3rem; 503 | } 504 | 505 | /*Category*/ 506 | .category-feed { 507 | /* background-color: pink; */ 508 | width: 50%; 509 | margin: 0 auto 10rem; 510 | padding: 3rem 2rem; 511 | } 512 | 513 | .category__empty { 514 | flex: 1; 515 | /* background-color: pink; */ 516 | display: flex; 517 | flex-direction: column; 518 | justify-content: center; 519 | align-items: center; 520 | font-weight: 600; 521 | padding: 0 2rem; 522 | text-align: center; 523 | margin-bottom: 10rem; 524 | } 525 | 526 | .category__empty-heading { 527 | font-size: 3rem; 528 | margin-bottom: 2.5rem; 529 | opacity: 0.5; 530 | } 531 | 532 | .category__empty-text { 533 | font-size: 2.5rem; 534 | opacity: 0.5; 535 | margin-bottom: 2rem; 536 | } 537 | 538 | .category__empty-link { 539 | display: flex; 540 | align-items: center; 541 | text-decoration: none; 542 | background-color: var(--color-btn); 543 | color: var(--color-btn-text); 544 | font-size: 2rem; 545 | padding: 1.2rem 3rem; 546 | border-radius: 5px; 547 | } 548 | 549 | .category-feed__item { 550 | display: flex; 551 | align-items: center; 552 | background-color: var(--color-card); 553 | margin-bottom: 3rem; 554 | border-radius: 8px; 555 | text-decoration: none; 556 | color: inherit; 557 | font-weight: 600; 558 | font-size: 2.2rem; 559 | } 560 | 561 | .category-feed__item-link { 562 | flex: 1; 563 | /* background-color: pink; */ 564 | display: flex; 565 | align-items: center; 566 | text-decoration: none; 567 | padding: 3rem 2rem; 568 | color: inherit; 569 | } 570 | 571 | .category-feed__image { 572 | border-radius: 100%; 573 | } 574 | 575 | .category-feed__icon { 576 | /* flex: 1; */ 577 | /* background-color: skyblue; */ 578 | cursor: pointer; 579 | padding: 1rem 2rem; 580 | } 581 | 582 | .trash-icon, 583 | .icon-chevron { 584 | /* width: 100%; */ 585 | background-color: transparent; 586 | border: none; 587 | cursor: pointer; 588 | color: inherit; 589 | /* padding: 1rem 2rem; */ 590 | } 591 | 592 | /*List*/ 593 | .add, 594 | .category-feed { 595 | margin-bottom: 10rem; 596 | } 597 | 598 | .list { 599 | width: 50%; 600 | margin: 0 auto 10rem; 601 | /* background-color: pink; */ 602 | padding: 3rem 2rem; 603 | line-height: 1.6; 604 | } 605 | 606 | .list__heading { 607 | font-size: 3.5rem; 608 | margin-bottom: 2rem; 609 | } 610 | 611 | .list__item { 612 | /* width: 100%; */ 613 | display: flex; 614 | /* align-items: center; */ 615 | flex-direction: column; 616 | background-color: var(--color-card); 617 | text-decoration: none; 618 | color: inherit; 619 | padding: 2rem; 620 | /* border: 20px solid #fff; */ 621 | margin-bottom: 3rem; 622 | border-radius: 8px; 623 | font-weight: 600; 624 | /* background-color: pink; */ 625 | } 626 | 627 | .list__item-external { 628 | padding: 0 2rem; 629 | } 630 | 631 | .list__item-image { 632 | border-radius: 8px; 633 | margin-top: 2rem; 634 | } 635 | 636 | .list__item-header { 637 | width: 100%; 638 | display: flex; 639 | align-items: center; 640 | /* background-color: skyblue; */ 641 | } 642 | 643 | .list__item-main { 644 | /* background-color: pink; */ 645 | flex: 1; 646 | display: flex; 647 | flex-direction: column; 648 | } 649 | 650 | /* .list__item-main.yt { 651 | align-items: center; 652 | } */ 653 | 654 | .list__item-title { 655 | /* background-color: pink; */ 656 | font-size: 2.3rem; 657 | margin-right: 1rem; 658 | margin-bottom: 2rem; 659 | } 660 | 661 | .list__item-content { 662 | display: -webkit-box; 663 | -webkit-line-clamp: 2; 664 | -webkit-box-orient: vertical; 665 | overflow: hidden; 666 | text-overflow: ellipsis; 667 | font-weight: 400; 668 | font-size: 2rem; 669 | opacity: 0.6; 670 | margin-bottom: 2rem; 671 | word-break: break-word; 672 | } 673 | 674 | .list__item-info { 675 | /* background-color: pink; */ 676 | display: flex; 677 | } 678 | 679 | .list__item-date, 680 | .list__twitter-date, 681 | .list__item-play { 682 | /* align-self: flex-start; */ 683 | font-weight: 600; 684 | font-size: 1.9rem; 685 | padding: 0.5rem 1rem; 686 | border-radius: 5px; 687 | background-color: var(--color-label); 688 | /* border-color: transparent; */ 689 | } 690 | 691 | .list__item-play { 692 | padding: 0.5rem 2rem; 693 | } 694 | 695 | .list__item-date { 696 | margin-right: 2rem; 697 | } 698 | 699 | .list__twitter-date { 700 | align-self: flex-start; 701 | } 702 | 703 | .list__item-play { 704 | border: none; 705 | display: inline-flex; 706 | align-items: center; 707 | /* justify-content: center; */ 708 | background-color: var(--color-btn); 709 | color: var(--color-btn-text); 710 | text-decoration: none; 711 | cursor: pointer; 712 | } 713 | 714 | /* .list__item-ytlink { 715 | margin-left: 2rem; 716 | text-decoration: none; 717 | } */ 718 | 719 | .list__podcast { 720 | /* background-color: pink; */ 721 | background-color: var(--color-btn); 722 | color: var(--color-btn-text); 723 | margin-top: 2.5rem; 724 | padding: 3rem; 725 | border-radius: 5px; 726 | margin-bottom: 2.5rem; 727 | box-shadow: 5px 5px 20px rgba(0, 0, 0, 0.4); 728 | position: sticky; 729 | top: 78px; 730 | z-index: 2; 731 | } 732 | 733 | .list__podcast-heading { 734 | /* background-color: pink; */ 735 | font-size: 4rem; 736 | margin-bottom: 1rem; 737 | display: flex; 738 | align-items: center; 739 | } 740 | 741 | .list__podcast-title { 742 | /* background-color: skyblue; */ 743 | display: -webkit-box; 744 | -webkit-line-clamp: 2; 745 | -webkit-box-orient: vertical; 746 | overflow: hidden; 747 | text-overflow: ellipsis; 748 | font-weight: 500; 749 | font-size: 2rem; 750 | margin-bottom: 2.5rem; 751 | word-break: break-word; 752 | } 753 | 754 | .list__podcast > audio { 755 | background-color: lightgoldenrodyellow; 756 | width: 100%; 757 | } 758 | 759 | .list__link { 760 | background-color: var(--color-btn); 761 | text-decoration: none; 762 | color: var(--color-btn-text); 763 | font-size: 2.1rem; 764 | font-weight: 600; 765 | display: flex; 766 | justify-content: center; 767 | padding: 1.5rem; 768 | border-radius: 5px; 769 | } 770 | 771 | /*List Twitter*/ 772 | .list__twitter { 773 | text-decoration: none; 774 | color: inherit; 775 | /* font-weight: 600; */ 776 | display: flex; 777 | flex-direction: column; 778 | background-color: var(--color-card); 779 | padding: 2rem; 780 | font-weight: 400; 781 | margin-bottom: 3rem; 782 | border-radius: 8px; 783 | /* border: 1px solid #181818; */ 784 | } 785 | 786 | .list__twitter-date { 787 | margin-bottom: 3rem; 788 | } 789 | 790 | .list__twitter-text { 791 | font-size: 2.2rem; 792 | } 793 | 794 | .list__twitter-media { 795 | margin-top: 4rem; 796 | align-self: center; 797 | border-radius: 8px; 798 | } 799 | 800 | /*404*/ 801 | 802 | .error-page { 803 | flex: 1; 804 | /* background-color: pink; */ 805 | display: flex; 806 | flex-direction: column; 807 | justify-content: center; 808 | align-items: center; 809 | padding: 0 2rem; 810 | } 811 | 812 | .error-page__text { 813 | font-weight: 600; 814 | text-align: center; 815 | margin-bottom: 4rem; 816 | } 817 | 818 | .error-page__text--main, 819 | .error-page__text--sub { 820 | display: block; 821 | } 822 | 823 | .error-page__text--main { 824 | font-size: 2.8rem; 825 | margin-bottom: 2rem; 826 | } 827 | 828 | .error-page__text--sub { 829 | opacity: 0.6; 830 | font-size: 2rem; 831 | } 832 | 833 | .error-page__btn { 834 | text-decoration: none; 835 | background-color: var(--color-btn); 836 | color: var(--color-btn-text); 837 | font-size: 2rem; 838 | padding: 1rem 2rem; 839 | border-radius: 5px; 840 | } 841 | 842 | /*Hover*/ 843 | .icon-chevron { 844 | transition: all 0.2s; 845 | } 846 | 847 | @media (hover: hover) { 848 | .btn-main:hover > .icon-chevron, 849 | .category-feed__item:hover .icon-chevron { 850 | transform: translateX(10px); 851 | } 852 | } 853 | 854 | /*Utils*/ 855 | .u-mt1 { 856 | margin-top: 1rem; 857 | } 858 | 859 | .u-ml1 { 860 | margin-left: 1rem; 861 | } 862 | 863 | .u-ml1-5 { 864 | margin-left: 1.5rem; 865 | } 866 | 867 | .u-ml2 { 868 | margin-left: 2rem; 869 | } 870 | 871 | .u-ml3 { 872 | margin-left: 3rem; 873 | } 874 | 875 | .u-mr1 { 876 | margin-right: 1rem; 877 | } 878 | 879 | .u-mr2 { 880 | margin-right: 2rem; 881 | } 882 | 883 | .u-mr3 { 884 | margin-right: 3rem; 885 | } 886 | 887 | .u-mr4 { 888 | margin-right: 4rem; 889 | } 890 | 891 | /*Modal*/ 892 | .confirm__modal-heading { 893 | font-size: 2.8rem; 894 | } 895 | 896 | .confirm__modal-text { 897 | font-size: 2.1rem; 898 | margin-bottom: 3rem; 899 | } 900 | 901 | .confirm__modal-btn { 902 | border: none; 903 | padding: 1rem 2rem; 904 | font-size: 2rem; 905 | font-weight: 600; 906 | border-radius: 4px; 907 | cursor: pointer; 908 | } 909 | 910 | .confirm__modal-btn:nth-of-type(1) { 911 | background-color: transparent; 912 | color: var(--color-white); 913 | } 914 | 915 | .confirm__modal-btn:nth-of-type(2) { 916 | background-color: #c61111; 917 | margin-left: 1.5rem; 918 | color: var(--color-white); 919 | } 920 | 921 | /*PF-Loader*/ 922 | .pf-loader { 923 | /* background-color: pink; */ 924 | flex: 1; 925 | display: flex; 926 | align-items: center; 927 | justify-content: center; 928 | margin-bottom: 10rem; 929 | } 930 | 931 | .pf-loader__text { 932 | font-size: 5rem; 933 | font-weight: 700; 934 | animation: fadeInOut 1s cubic-bezier(0.645, 0.045, 0.355, 1) infinite 935 | alternate; 936 | } 937 | 938 | @keyframes fadeInOut { 939 | from { 940 | opacity: 0; 941 | } 942 | to { 943 | opacity: 1; 944 | } 945 | } 946 | 947 | /*Spinners*/ 948 | 949 | .spinner { 950 | display: inline-block; 951 | border: 4px solid transparent; 952 | border-left-color: var(--color-btn-text); 953 | border-radius: 50%; 954 | width: 2.2rem; 955 | height: 2.2rem; 956 | animation: spin 0.5s linear infinite; 957 | } 958 | 959 | .spinner.nav { 960 | border-left-color: var(--color-primary); 961 | } 962 | 963 | @keyframes spin { 964 | to { 965 | transform: rotate(1turn); 966 | } 967 | } 968 | 969 | /* Custom Scroll Bar*/ 970 | ::-webkit-scrollbar { 971 | width: 18px; 972 | } 973 | 974 | ::-webkit-scrollbar-track { 975 | background-color: transparent; 976 | } 977 | 978 | ::-webkit-scrollbar-thumb { 979 | background-color: #d7d5d3; 980 | border-radius: 20px; 981 | border: 6px solid transparent; 982 | -webkit-background-clip: content-box; 983 | background-clip: content-box; 984 | } 985 | 986 | ::-webkit-scrollbar-thumb:hover { 987 | background-color: #c4c2c1; 988 | } 989 | 990 | /*Media Queries*/ 991 | @media (max-width: 56.25em) { 992 | html { 993 | font-size: 58.25%; 994 | } 995 | 996 | .header__text-box { 997 | padding-left: 0; 998 | } 999 | 1000 | .header__img { 1001 | display: none; 1002 | } 1003 | 1004 | .heading-primary { 1005 | text-align: start; 1006 | } 1007 | 1008 | .header__btns { 1009 | width: 100%; 1010 | flex-direction: column; 1011 | } 1012 | 1013 | .btn-main { 1014 | margin-bottom: 2rem; 1015 | } 1016 | 1017 | .btn-video { 1018 | margin-left: 0; 1019 | } 1020 | 1021 | .maker { 1022 | text-align: center; 1023 | } 1024 | 1025 | .sidenav__item { 1026 | /* width: 20%; */ 1027 | overflow: hidden; 1028 | padding: 0.5rem 3rem; 1029 | } 1030 | 1031 | .sidenav__link { 1032 | font-size: 1.7rem; 1033 | } 1034 | 1035 | .add, 1036 | .category-feed, 1037 | .list { 1038 | width: 100%; 1039 | } 1040 | 1041 | .list__podcast { 1042 | top: -8rem; 1043 | } 1044 | 1045 | .list__podcast-title { 1046 | margin-bottom: 1.5rem; 1047 | } 1048 | 1049 | .list__podcast-heading { 1050 | /* display: none; */ 1051 | font-size: 3.5rem; 1052 | } 1053 | 1054 | .category-feed__item, 1055 | .list__item-title, 1056 | .list__item-content, 1057 | .list__item-date { 1058 | font-size: 1.8rem; 1059 | } 1060 | } 1061 | 1062 | /*iPad Port*/ 1063 | @media only screen and (min-device-width: 48em) and (max-device-width: 64em) and (orientation: portrait) { 1064 | html { 1065 | font-size: 75%; /*1rem = 12px*/ 1066 | } 1067 | 1068 | .list__podcast { 1069 | top: 78px; 1070 | } 1071 | } 1072 | 1073 | /*iPad Pro Portrait */ 1074 | @media only screen and (min-device-width: 64em) and (max-device-width: 85.375em) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait) { 1075 | html { 1076 | font-size: 75%; /*1rem = 12px*/ 1077 | } 1078 | 1079 | .header__img { 1080 | display: none; 1081 | } 1082 | 1083 | .header__text-box { 1084 | text-align: center; 1085 | align-items: center; 1086 | padding-left: 0; 1087 | } 1088 | 1089 | .header__btns { 1090 | width: 60%; 1091 | display: flex; 1092 | justify-content: center; 1093 | } 1094 | 1095 | .add, 1096 | .category-feed, 1097 | .list { 1098 | width: 100%; 1099 | } 1100 | } 1101 | 1102 | /* iPad Landscape */ 1103 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 1) { 1104 | html { 1105 | font-size: 75%; /*1rem = 12px*/ 1106 | } 1107 | 1108 | .header__img { 1109 | display: none; 1110 | } 1111 | 1112 | .header__text-box { 1113 | text-align: center; 1114 | align-items: center; 1115 | padding-left: 0; 1116 | } 1117 | 1118 | .header__btns { 1119 | width: 60%; 1120 | display: flex; 1121 | justify-content: center; 1122 | } 1123 | 1124 | .add, 1125 | .category-feed, 1126 | .list { 1127 | width: 80%; 1128 | } 1129 | } 1130 | 1131 | /*Small Sized Phones*/ 1132 | @media (max-width: 20em) { 1133 | html { 1134 | font-size: 48%; 1135 | } 1136 | 1137 | .list__podcast { 1138 | top: -6rem; 1139 | } 1140 | } 1141 | 1142 | /*Big Desktop*/ 1143 | @media (min-width: 112.5em) { 1144 | html { 1145 | font-size: 87.5%; /*1rem = 14px*/ 1146 | } 1147 | } 1148 | 1149 | /*Error boundary*/ 1150 | .error-boundary { 1151 | padding: 2rem; 1152 | } 1153 | 1154 | .error-boundary__heading { 1155 | font-size: 3rem; 1156 | margin-bottom: 1rem; 1157 | } 1158 | 1159 | .error-boundary__btn { 1160 | font-size: 1.8rem; 1161 | font-weight: 600; 1162 | padding: 1rem; 1163 | background-color: #121212; 1164 | color: #fff; 1165 | border-radius: 5px; 1166 | border: none; 1167 | cursor: pointer; 1168 | } 1169 | -------------------------------------------------------------------------------- /utils/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI; 4 | 5 | if (!MONGODB_URI) { 6 | throw new Error( 7 | "Please define the MONGODB_URI environment variable inside .env.local" 8 | ); 9 | } 10 | 11 | let cached = global.mongoose; 12 | 13 | if (!cached) { 14 | cached = global.mongoose = { conn: null, promise: null }; 15 | } 16 | 17 | async function dbConnect() { 18 | if (cached.conn) { 19 | return cached.conn; 20 | } 21 | 22 | if (!cached.promise) { 23 | const opts = { 24 | useNewUrlParser: true, 25 | useUnifiedTopology: true, 26 | bufferCommands: false, 27 | bufferMaxEntries: 0, 28 | useFindAndModify: false, 29 | useCreateIndex: true, 30 | }; 31 | 32 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { 33 | return mongoose; 34 | }); 35 | } 36 | cached.conn = await cached.promise; 37 | return cached.conn; 38 | } 39 | 40 | export default dbConnect; 41 | -------------------------------------------------------------------------------- /utils/getYTChannelAvatar.js: -------------------------------------------------------------------------------- 1 | export async function getYTChannelAvatar(channelId) { 2 | try { 3 | const ytChannelResponse = await fetch( 4 | `https://www.googleapis.com/youtube/v3/channels?part=snippet&id=${channelId}&key=${process.env.YT_API_KEY}`, 5 | { 6 | referrer: 7 | process.env.NODE_ENV === "production" 8 | ? "https://pocket-feed.vercel.app" 9 | : "http://localhost:3000", 10 | } 11 | ); 12 | 13 | const ytChannelData = await ytChannelResponse.json(); 14 | const ytChannelAvatarUrl = 15 | ytChannelData.items[0].snippet.thumbnails.default.url; 16 | console.log(ytChannelAvatarUrl); 17 | return ytChannelAvatarUrl; 18 | } catch (err) { 19 | return null; 20 | } 21 | } 22 | --------------------------------------------------------------------------------