├── .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 | 
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 |
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 |
56 |
57 | );
58 |
59 | const DropDown = () => {
60 | return (
61 |
62 |
63 |

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 |
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 |

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 |
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 |
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 |
--------------------------------------------------------------------------------