├── .eslintrc.json ├── next.config.js ├── screenshot.png ├── utils └── truncateString.js ├── pages ├── content │ ├── _middleware.js │ └── index.js ├── api │ ├── fetch-database.js │ ├── search-watchlist-page.js │ ├── toggle-archive-page-from-db.js │ ├── update-db-properties.js │ ├── auth │ │ └── notion │ │ │ └── get-access-token.js │ ├── get-pages-from-db.js │ └── add-page-to-db.js ├── auth │ └── notion │ │ └── callback.js ├── index.js ├── _app.js ├── dashboard.js ├── search.js ├── settings.js └── set-watchlist-page.js ├── context ├── NotionCred.js └── Settings.js ├── .gitignore ├── package.json ├── README.md ├── public └── favicon.svg ├── LICENSE ├── components ├── Footer.js ├── SearchSuggestion.js ├── MovieSearchBar.js ├── NotionContentCard.js ├── NotionWatchlist.js └── Navbar.js └── styles └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Royal-lobster/Watchlister/HEAD/screenshot.png -------------------------------------------------------------------------------- /utils/truncateString.js: -------------------------------------------------------------------------------- 1 | export default function truncateString(str, num) { 2 | if (str.length > num) { 3 | return str.slice(0, num) + "..."; 4 | } else { 5 | return str; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pages/content/_middleware.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | export async function middleware(req, res) { 3 | const { nextUrl: url, geo } = req; 4 | url?.searchParams?.set("country", geo.country ?? "US"); 5 | return NextResponse.rewrite(url); 6 | } 7 | -------------------------------------------------------------------------------- /context/NotionCred.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | export const NotionCredContext = React.createContext(); 3 | 4 | export const NotionCredProvider = ({ children }) => { 5 | const [notionUserCredentials, setNotionUserCredentials] = useState({}); 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /.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 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /pages/api/fetch-database.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | if (req.method === "POST" && req.body.token && req.body.page_id) { 3 | fetch(`https://api.notion.com/v1/databases/${req.body.page_id}`, { 4 | method: "PATCH", 5 | headers: { 6 | "Content-Type": "application/json", 7 | "Notion-Version": "2021-08-16", 8 | Authorization: `Bearer ${req.body.token}`, 9 | }, 10 | }) 11 | .then((response) => response.json()) 12 | .then((data) => { 13 | res.status(200).json(data); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-watchlister", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@mantine/core": "^3.1.0", 13 | "@mantine/hooks": "^3.1.0", 14 | "@mantine/notifications": "^3.1.4", 15 | "next": "12.0.1", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2", 18 | "react-icons": "^4.3.1" 19 | }, 20 | "devDependencies": { 21 | "eslint": "7.32.0", 22 | "eslint-config-next": "12.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pages/api/search-watchlist-page.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | if (req.method === "POST" && req.body.token && req.body.search) { 3 | fetch(`https://api.notion.com/v1/search`, { 4 | method: "POST", 5 | headers: { 6 | "Content-Type": "application/json", 7 | Authorization: `Bearer ${req.body.token}`, 8 | "Notion-Version": "2021-08-16", 9 | }, 10 | body: JSON.stringify({ 11 | query: req.body.search, 12 | }), 13 | }) 14 | .then((response) => response.json()) 15 | .then((data) => { 16 | res.status(200).json(data); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/toggle-archive-page-from-db.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | if (req.method === "POST" && req.body.token && req.body.page_id && req.body.archive) { 3 | fetch(`https://api.notion.com/v1/pages/${req.body.page_id}`, { 4 | method: "PATCH", 5 | headers: { 6 | "Content-Type": "application/json", 7 | "Notion-Version": "2021-08-16", 8 | Authorization: `Bearer ${req.body.token}`, 9 | }, 10 | body: JSON.stringify({ 11 | archived: req.body.archive, 12 | }), 13 | }) 14 | .then((response) => response.json()) 15 | .then((data) => { 16 | console.log(data); 17 | res.status(200).json(data); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/update-db-properties.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | console.log(req.body.body_content); 3 | if ( 4 | req.method === "POST" && 5 | req.body.token && 6 | req.body.database_id && 7 | req.body.body_content 8 | ) { 9 | fetch(`https://api.notion.com/v1/databases/${req.body.database_id}`, { 10 | method: "PATCH", 11 | headers: { 12 | "Content-Type": "application/json", 13 | "Notion-Version": "2021-08-16", 14 | Authorization: `Bearer ${req.body.token}`, 15 | }, 16 | body: JSON.stringify(req.body.body_content), 17 | }) 18 | .then((response) => response.json()) 19 | .then((data) => { 20 | console.log(data); 21 | res.status(200).json(data); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pages/api/auth/notion/get-access-token.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | if (req.method === "POST" && req.body.code) { 3 | fetch(`https://api.notion.com/v1/oauth/token`, { 4 | method: "POST", 5 | headers: { 6 | "Content-Type": "application/json", 7 | Authorization: `Basic ${Buffer.from( 8 | `${process.env.NOTION_OAUTH_CLIENT_TOKEN}:${process.env.NOTION_INTEGRATION_SECRET}` 9 | ).toString("base64")}`, 10 | }, 11 | body: JSON.stringify({ 12 | grant_type: "authorization_code", 13 | code: req.body.code, 14 | }), 15 | }) 16 | .then((response) => response.json()) 17 | .then((data) => { 18 | console.log(data); 19 | res.status(200).json(data); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/get-pages-from-db.js: -------------------------------------------------------------------------------- 1 | export default async function handler(req, res) { 2 | if (req.method === "POST" && req.body.token && req.body.database_id) { 3 | try { 4 | const response = await fetch(`https://api.notion.com/v1/databases/${req.body.database_id}/query`, { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json", 8 | "Notion-Version": "2021-08-16", 9 | Authorization: `Bearer ${req.body.token}`, 10 | }, 11 | }); 12 | const data = await response.json(); 13 | res.status(200).json(data); 14 | } catch (error) { 15 | res.status(500).json({ error: "Server error or failed to fetch data." }); 16 | } 17 | } else { 18 | res.status(400).json({ error: "Invalid request or missing parameters." }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Watchlister 3 | 4 | Manage your Notion Watchlist with ease ! Watchlister automatically fills Movie/TV/Anime details to you watchlist so you don't have to do it manually. It uses Notion API and TMDB API. 5 | 6 | 🔗 **Link:** https://watchlister.vercel.app/ 7 | 8 | [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) 9 | ## Screenshots 10 | 11 | ![App Screenshot](https://raw.githubusercontent.com/Royal-lobster/watchlister-nextjs/main/screenshot.png) 12 | 13 | 14 | ## Installation 15 | 16 | ```bash 17 | git clone https://github.com/Royal-lobster/watchlister 18 | 19 | cd watchlister 20 | 21 | npm install 22 | ``` 23 | ## Running the Application 24 | 25 | ```bash 26 | npm run dev 27 | ``` 28 | 29 | ## Authors 30 | 31 | - [@SrujanGurram](https://www.github.com/royal-lobster) 32 | 33 | 34 | ## License 35 | 36 | [MIT](https://choosealicense.com/licenses/mit/) 37 | 38 | 39 | -------------------------------------------------------------------------------- /pages/api/add-page-to-db.js: -------------------------------------------------------------------------------- 1 | export default async function handler(req, res) { 2 | try { 3 | if (req.method !== "POST" || !req.body.token || !req.body.body_content) { 4 | res.status(400).json({ error: "Invalid request or missing parameters." }); 5 | return; 6 | } 7 | 8 | const { token, body_content } = req.body; 9 | 10 | const response = await fetch(`https://api.notion.com/v1/pages`, { 11 | method: "POST", 12 | headers: { 13 | "Content-Type": "application/json", 14 | "Notion-Version": "2021-08-16", 15 | Authorization: `Bearer ${token}`, 16 | }, 17 | body: body_content, 18 | }); 19 | 20 | const data = await response.json(); 21 | 22 | if (data.object === "error") { 23 | res.status(data.status).json({ error: data.message }); 24 | } else { 25 | res.status(200).json(data); 26 | } 27 | } catch (error) { 28 | res.status(500).json({ error: "Server error or failed to fetch data." }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /context/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | export const SettingsContext = React.createContext(); 3 | 4 | export const SettingsProvider = ({ children }) => { 5 | let getInitialState = (storageName) => { 6 | return typeof window !== "undefined" 7 | ? localStorage.getItem(storageName) 8 | ? JSON.parse(localStorage.getItem(storageName)) 9 | : false 10 | : false; 11 | }; 12 | 13 | const [whereToWatchSetting, setWhereToWatchSetting] = useState( 14 | getInitialState("WHERE_TO_WATCH_SETTING") 15 | ); 16 | 17 | useEffect(() => { 18 | console.log("SETTING WHERE TO WATCH SETTING", whereToWatchSetting); 19 | localStorage.setItem( 20 | "WHERE_TO_WATCH_SETTING", 21 | JSON.parse(whereToWatchSetting) 22 | ); 23 | }, [whereToWatchSetting]); 24 | 25 | return ( 26 | 29 | {children} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Srujan Gurram 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 | -------------------------------------------------------------------------------- /pages/auth/notion/callback.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import React, { useEffect } from "react"; 3 | import { Loader } from "@mantine/core"; 4 | 5 | export let getStaticProps = () => { 6 | return { 7 | props: { 8 | APPLICATION_URL: process.env.APPLICATION_URL, 9 | }, 10 | }; 11 | }; 12 | 13 | function NotionCallback({ APPLICATION_URL }) { 14 | let router = useRouter(); 15 | useEffect(() => { 16 | async function fetchNotionAccessToken() { 17 | const response = await fetch(`${APPLICATION_URL}/api/auth/notion/get-access-token`, { 18 | method: "POST", 19 | headers: { 20 | "Content-Type": "application/json", 21 | }, 22 | body: JSON.stringify({ 23 | code: router.query.code, 24 | }), 25 | }); 26 | const resBody = await response.json(); 27 | if (resBody.access_token) { 28 | localStorage.setItem("NOTION_USER_CREDENTIALS", JSON.stringify(resBody)); 29 | router.push("/set-watchlist-page"); 30 | } else { 31 | alert("Something went wrong. Please try again."); 32 | router.push("/"); 33 | } 34 | } 35 | fetchNotionAccessToken(); 36 | }, [router, APPLICATION_URL]); 37 | 38 | return ( 39 |
40 | 41 |
42 | ); 43 | } 44 | 45 | export default NotionCallback; 46 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Footer() { 4 | return ( 5 | <> 6 | 11 | 12 | 51 | 52 | ); 53 | } 54 | 55 | export default Footer; 56 | -------------------------------------------------------------------------------- /components/SearchSuggestion.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Avatar, ThemeIcon } from "@mantine/core"; 3 | import { MdOutlineMovie } from "react-icons/md"; 4 | import { useRouter } from "next/router"; 5 | function SearchSuggestion({ name, mediaType, posterPath, id }) { 6 | let router = useRouter(); 7 | let handleSuggestionClick = () => { 8 | router.push(`/content?id=${id}&type=${mediaType}`); 9 | }; 10 | return ( 11 | <> 12 | 27 | 47 | 48 | ); 49 | } 50 | 51 | export default SearchSuggestion; 52 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Button, Paper, ThemeIcon, Notification } from "@mantine/core"; 3 | import { SiNotion } from "react-icons/si"; 4 | import { useRouter } from "next/router"; 5 | import { MdOutlineMovie } from "react-icons/md"; 6 | 7 | export let getStaticProps = () => { 8 | return { 9 | props: { 10 | NOTION_OAUTH_CLIENT_TOKEN: process.env.NOTION_OAUTH_CLIENT_TOKEN, 11 | APPLICATION_URL: process.env.APPLICATION_URL, 12 | }, 13 | }; 14 | }; 15 | 16 | function Index({ NOTION_OAUTH_CLIENT_TOKEN, APPLICATION_URL }) { 17 | const router = useRouter(); 18 | 19 | useEffect(() => { 20 | if (localStorage.getItem("NOTION_USER_CREDENTIALS")) { 21 | router.push("/dashboard"); 22 | } 23 | }, [router]); 24 | 25 | let handleConnectClick = () => { 26 | window.location.href = `https://api.notion.com/v1/oauth/authorize?owner=user&client_id=${NOTION_OAUTH_CLIENT_TOKEN}&response_type=code`; 27 | }; 28 | 29 | return ( 30 | <> 31 |
32 | 33 |
34 | 35 | 36 | 37 |

Watchlister

38 |
39 | 40 | Duplicate{" "} 41 | this page to your 42 | notion account (skip this if you already have a similar page) 43 | 44 | 45 | Click on the button below. then search for the page from search box you duplicated before and{" "} 46 | only select that one page (not more not less) 47 | 48 | 56 |
57 |
58 | 59 | 75 | 76 | ); 77 | } 78 | 79 | export default Index; 80 | -------------------------------------------------------------------------------- /components/MovieSearchBar.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import SearchSuggestion from "./SearchSuggestion"; 3 | import { useDebouncedValue } from "@mantine/hooks"; 4 | import { Input } from "@mantine/core"; 5 | import { FiSearch } from "react-icons/fi"; 6 | import { useRouter } from "next/router"; 7 | 8 | function MovieSearchBar({ TMDB_API_KEY }) { 9 | let router = useRouter(); 10 | const [searchTerm, setSearchTerm] = useState(""); 11 | const [searchData, setSearchData] = useState([]); 12 | const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 200, { leading: true }); 13 | 14 | useEffect(() => { 15 | if (debouncedSearchTerm.length > 1) { 16 | fetchData(); 17 | } 18 | async function fetchData() { 19 | let url = `https://api.themoviedb.org/3/search/multi?api_key=${TMDB_API_KEY}&query=${debouncedSearchTerm}&page=1`; 20 | let response = await fetch(url); 21 | let { results } = await response.json(); 22 | setSearchData( 23 | results?.filter((result) => result.media_type === "movie" || result.media_type === "tv").slice(0, 5) 24 | ); 25 | } 26 | }, [TMDB_API_KEY, debouncedSearchTerm]); 27 | 28 | return ( 29 | <> 30 |
{ 33 | e.preventDefault(); 34 | if (searchTerm.length > 1) { 35 | router.push(`/search?q=${searchTerm}&p=1`); 36 | } 37 | }} 38 | > 39 | } 42 | placeholder="Search for movie/tv/anime" 43 | value={searchTerm} 44 | onChange={(e) => setSearchTerm(e.target.value)} 45 | /> 46 |
47 | {searchData?.length > 0 && 48 | searchTerm.length > 0 && 49 | searchData.map((data) => ( 50 | 57 | ))} 58 |
59 |
60 | 61 | 82 | 83 | ); 84 | } 85 | 86 | export default MovieSearchBar; 87 | -------------------------------------------------------------------------------- /components/NotionContentCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Avatar, Badge, Button, Card, Group, Image, Text, ThemeIcon } from "@mantine/core"; 3 | import { MdOutlineMovie } from "react-icons/md"; 4 | import { FiTrash2 } from "react-icons/fi"; 5 | import { SiNotion } from "react-icons/si"; 6 | function NotionContentCard({ title, cover, icon, id, genres, handlePageDeleteConfirm }) { 7 | return ( 8 | <> 9 | 10 | 11 | {title 12 | 13 |
14 | 15 | 16 | 17 | 18 | {title} 19 | 20 |
21 |
22 | {genres.map((genre) => ( 23 | 24 | {genre.name} 25 | 26 | ))} 27 |
28 | 29 | 40 | 51 | 52 |
53 | 87 | 88 | ); 89 | } 90 | 91 | export default NotionContentCard; 92 | -------------------------------------------------------------------------------- /components/NotionWatchlist.js: -------------------------------------------------------------------------------- 1 | import { Paper, Skeleton, Text } from "@mantine/core"; 2 | import React from "react"; 3 | import NotionContentCard from "./NotionContentCard"; 4 | 5 | function NotionWatchlist({ 6 | contentData, 7 | contentLoading, 8 | handlePageDeleteConfirm, 9 | }) { 10 | return ( 11 | <> 12 |
13 |
14 |
15 |
16 | {contentLoading && 17 | Array.from(Array(9)).map((_, i) => ( 18 | 19 | ))} 20 | {contentData?.map( 21 | (page) => 22 | page.properties.Name.title.length !== 0 && ( 23 | 33 | ) 34 | )} 35 | {(contentData?.length === 0) && ( 36 | 44 | 50 | No TV Shows, Movies, Anime found 51 | 52 | 56 | You can now add new TV Shows, Movies, Anime to your notion 57 | page by searching from the search bar above. 58 | 59 | 60 | )} 61 |
62 |
63 | 84 | 85 | ); 86 | } 87 | 88 | export default NotionWatchlist; 89 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { MantineProvider, NormalizeCSS, GlobalStyles } from "@mantine/core"; 3 | import { NotionCredProvider } from "../context/NotionCred"; 4 | import { NotificationsProvider } from "@mantine/notifications"; 5 | import Head from "next/head"; 6 | import Footer from "../components/Footer"; 7 | import { SettingsProvider } from "../context/Settings"; 8 | 9 | function MyApp({ Component, pageProps }) { 10 | return ( 11 | <> 12 | 13 | Watchlister 14 | 18 | 19 | 20 | 21 | 22 | {/* */} 23 | 24 | 25 | 26 | 30 | 31 | 32 | {/* */} 33 | 34 | 35 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 | 55 |
56 |
58 |
59 |
60 |
61 |
62 | 63 | 72 | 73 | ); 74 | } 75 | 76 | export default MyApp; 77 | -------------------------------------------------------------------------------- /pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from "react"; 2 | import { Button, Group, Modal, Notification } from "@mantine/core"; 3 | import { FiTrash2, FiX } from "react-icons/fi"; 4 | import Navbar from "../components/Navbar"; 5 | import { NotionCredContext } from "../context/NotionCred"; 6 | import NotionWatchlist from "../components/NotionWatchlist"; 7 | import { useRouter } from "next/router"; 8 | import MovieSearchBar from "../components/MovieSearchBar"; 9 | 10 | export let getStaticProps = () => { 11 | return { 12 | props: { 13 | TMDB_API_KEY: process.env.TMDB_API_KEY, 14 | APPLICATION_URL: process.env.APPLICATION_URL, 15 | }, 16 | }; 17 | }; 18 | 19 | function Dashboard({ TMDB_API_KEY, APPLICATION_URL }) { 20 | let router = useRouter(); 21 | const [contentData, setContentData] = useState([]); 22 | const [contentLoading, setContentLoading] = useState(true); 23 | const [pageDeleteLoading, setPageDeleteLoading] = useState(false); 24 | const [openedDeletePopup, setOpenedDeletePopup] = useState(false); 25 | const [pageToDelete, setPageToDelete] = useState(""); 26 | let [notionUserCredentials] = useContext(NotionCredContext); 27 | 28 | useEffect(() => { 29 | if (localStorage.getItem("NOTION_WATCHLIST_PAGE_ID") == null) { 30 | router.push("/set-watchlist-page"); 31 | } 32 | }, [router]); 33 | 34 | useEffect(() => { 35 | setContentLoading(true); 36 | let databaseID = localStorage.getItem("NOTION_WATCHLIST_PAGE_ID"); 37 | fetch(`${APPLICATION_URL}/api/get-pages-from-db`, { 38 | method: "POST", 39 | headers: { 40 | "Content-Type": "application/json", 41 | }, 42 | body: JSON.stringify({ 43 | token: notionUserCredentials.access_token, 44 | database_id: databaseID, 45 | }), 46 | }).then((response) => { 47 | response.json().then((data) => { 48 | if(data.error){ 49 | setContentLoading(false); 50 | return; 51 | } 52 | if(data.results.length == 0) { 53 | setContentLoading(false); 54 | return; 55 | } 56 | setContentData(data.results); 57 | setContentLoading(false); 58 | }); 59 | }); 60 | }, [notionUserCredentials, APPLICATION_URL]); 61 | 62 | let handlePageDeleteConfirm = (page_id) => { 63 | setPageToDelete(page_id); 64 | setOpenedDeletePopup(true); 65 | }; 66 | 67 | let handlePageDelete = () => { 68 | setPageDeleteLoading(true); 69 | fetch(`${APPLICATION_URL}/api/toggle-archive-page-from-db`, { 70 | method: "POST", 71 | headers: { 72 | "Content-Type": "application/json", 73 | }, 74 | body: JSON.stringify({ 75 | token: notionUserCredentials.access_token, 76 | page_id: pageToDelete, 77 | archive: true, 78 | }), 79 | }).then((response) => { 80 | response.json().then((data) => { 81 | console.log(data); 82 | window.location.reload(); 83 | }); 84 | }); 85 | }; 86 | return ( 87 | <> 88 | 89 |
90 | 91 | 96 | setOpenedDeletePopup(false)} title="Confirm Delete"> 97 | 98 | Are You Sure you want to delete the page ? 99 | 100 | 101 | 110 | 113 | 114 | 115 |
116 | 123 | 124 | ); 125 | } 126 | 127 | export default Dashboard; 128 | -------------------------------------------------------------------------------- /components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from "react"; 2 | import { NotionCredContext } from "../context/NotionCred"; 3 | import { useRouter } from "next/router"; 4 | import { 5 | Avatar, 6 | Text, 7 | Menu, 8 | UnstyledButton, 9 | Group, 10 | ThemeIcon, 11 | } from "@mantine/core"; 12 | import { FiLogOut, FiSettings, FiUser } from "react-icons/fi"; 13 | import { MdOutlineMovie } from "react-icons/md"; 14 | import { useMediaQuery } from "@mantine/hooks"; 15 | 16 | function Navbar() { 17 | let router = useRouter(); 18 | const [notionUserCredentials, setNotionUserCredentials] = 19 | useContext(NotionCredContext); 20 | useEffect(() => { 21 | async function getNotionUserCredentials() { 22 | const storedCredentials = await localStorage.getItem( 23 | "NOTION_USER_CREDENTIALS" 24 | ); 25 | if (storedCredentials) { 26 | setNotionUserCredentials(JSON.parse(storedCredentials)); 27 | } else { 28 | router.push("/"); 29 | } 30 | } 31 | getNotionUserCredentials(); 32 | }, [router, setNotionUserCredentials]); 33 | const matches = useMediaQuery("(min-width: 500px)"); 34 | let handleLogoutClick = () => { 35 | localStorage.clear(); 36 | window.location.replace("/"); 37 | }; 38 | return ( 39 | <> 40 | 96 | 134 | 135 | ); 136 | } 137 | 138 | export default Navbar; 139 | -------------------------------------------------------------------------------- /pages/search.js: -------------------------------------------------------------------------------- 1 | import { Avatar, Badge, Button, Image, Paper, Skeleton, Text, ThemeIcon } from "@mantine/core"; 2 | import { useRouter } from "next/router"; 3 | import React, { useEffect, useState } from "react"; 4 | import { FiArrowLeft } from "react-icons/fi"; 5 | import Navbar from "../components/Navbar"; 6 | import truncateString from "../utils/truncateString"; 7 | import { Pagination } from "@mantine/core"; 8 | 9 | export let getStaticProps = () => { 10 | return { 11 | props: { 12 | TMDB_API_KEY: process.env.TMDB_API_KEY, 13 | APPLICATION_URL: process.env.APPLICATION_URL, 14 | }, 15 | }; 16 | }; 17 | 18 | function Search({ TMDB_API_KEY }) { 19 | let router = useRouter(); 20 | const [searchData, setSearchData] = useState([]); 21 | const [activePage, setPage] = useState(router.query.p); 22 | const [contentLoading, setContentLoading] = useState(true); 23 | 24 | useEffect(() => { 25 | fetchData(); 26 | async function fetchData() { 27 | setContentLoading(true); 28 | let url = `https://api.themoviedb.org/3/search/multi?api_key=${TMDB_API_KEY}&query=${router.query.q}&page=${router.query.p}`; 29 | let response = await fetch(url); 30 | let { results } = await response.json(); 31 | setSearchData(results?.filter((result) => result.media_type === "movie" || result.media_type === "tv")); 32 | setContentLoading(false); 33 | } 34 | }, [TMDB_API_KEY, router.query.p, router.query.q]); 35 | 36 | useEffect(() => { 37 | if (activePage !== router.query.p) { 38 | router.push(`/search?q=${router.query.q}&p=${activePage}`); 39 | } 40 | }, [activePage, router]); 41 | 42 | return ( 43 | <> 44 | 45 |
46 | 56 |

Search Results

57 | {contentLoading && 58 | Array.from(Array(9)).map((e, i) => ( 59 | 60 | ))} 61 | {searchData.map((result) => ( 62 | 83 | ))} 84 | {searchData.length === 0 && ( 85 | 89 | 90 | No Results Found 91 | 92 | 93 | No More results found for your search. please go back and try to search again. 94 | 95 | 96 | )} 97 | 98 |
99 | 139 | 140 | ); 141 | } 142 | 143 | export default Search; 144 | -------------------------------------------------------------------------------- /pages/settings.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import Navbar from "../components/Navbar"; 3 | import { 4 | Switch, 5 | Modal, 6 | Notification, 7 | Group, 8 | Button, 9 | LoadingOverlay, 10 | } from "@mantine/core"; 11 | import { SettingsContext } from "../context/Settings"; 12 | import { useNotifications } from "@mantine/notifications"; 13 | import { NotionCredContext } from "../context/NotionCred"; 14 | import { FiTool } from "react-icons/fi"; 15 | 16 | export let getStaticProps = () => { 17 | return { 18 | props: { 19 | APPLICATION_URL: process.env.APPLICATION_URL, 20 | }, 21 | }; 22 | }; 23 | 24 | function Settings({ APPLICATION_URL }) { 25 | const notifications = useNotifications(); 26 | let [notionUserCredentials] = useContext(NotionCredContext); 27 | const [whereToWatchSetting, setWhereToWatchSetting] = 28 | useContext(SettingsContext); 29 | const [openedAddPropertiesPopup, setOpenedAddPropertiesPopup] = 30 | useState(false); 31 | const [loading, setLoading] = useState(false); 32 | const [presentSettingToChange, setPresentSettingToChange] = useState({ 33 | name: "", 34 | schema: {}, 35 | }); 36 | 37 | let handleMissingPropertiesAdd = async () => { 38 | // SET LOADING TO TRUE FOR UI 39 | setLoading(true); 40 | 41 | // ADD PROPERTIES TO NOTION PAGE 42 | let response = await fetch(`${APPLICATION_URL}/api/update-db-properties`, { 43 | method: "POST", 44 | headers: { 45 | "Content-Type": "application/json", 46 | }, 47 | body: JSON.stringify({ 48 | token: notionUserCredentials.access_token, 49 | database_id: await localStorage.getItem("NOTION_WATCHLIST_PAGE_ID"), 50 | body_content: presentSettingToChange.schema, 51 | }), 52 | }); 53 | let data = await response.json(); 54 | 55 | // SET LOADING TO FALSE FOR UI 56 | setLoading(false); 57 | 58 | // CHECK IF RESPONSE IS OK 59 | if (response.status == 200) { 60 | // SHOW SUCCESS NOTIFICATION AND CLOSE POPUP 61 | notifications.showNotification({ 62 | title: "Successfully Added property", 63 | message: 64 | "The property have been successfully added to the Notion page.", 65 | }); 66 | setOpenedAddPropertiesPopup(false); 67 | 68 | // TURN ON THE SETTING 69 | if (presentSettingToChange.name === "Watch Provider") { 70 | setWhereToWatchSetting(true); 71 | } 72 | } 73 | 74 | // IF RESPONSE IS NOT OK 75 | else { 76 | // SHOW ERROR NOTIFICATION 77 | alert("Something went wrong. Please Try again"); 78 | // CLOSE POPUP 79 | setOpenedAddPropertiesPopup(false); 80 | } 81 | }; 82 | 83 | let handleAddPropertiesToDb = async (propertyName, propertySchema) => { 84 | // SET LOADING TO TRUE FOR UI 85 | setLoading(true); 86 | // STORE PROPERTY SCHEMA TO STATE 87 | setPresentSettingToChange({ name: propertyName, schema: propertySchema }); 88 | 89 | // FETCH THE PAGE FROM NOTION 90 | let response = await fetch(`${APPLICATION_URL}/api/fetch-database`, { 91 | method: "POST", 92 | headers: { 93 | "Content-Type": "application/json", 94 | }, 95 | body: JSON.stringify({ 96 | token: notionUserCredentials.access_token, 97 | page_id: localStorage.getItem("NOTION_WATCHLIST_PAGE_ID"), 98 | }), 99 | }); 100 | let data = await response.json(); 101 | 102 | // SET LOADING TO FALSE FOR UI 103 | setLoading(false); 104 | 105 | // CHECK THE PROPERTIES OF THE PAGE 106 | const presentPageProperties = Object.keys(data.properties); 107 | 108 | console.log(presentPageProperties.includes(propertyName)); 109 | // ADD THE PROPERTIES TO THE PAGE IF NECESSARY (OPEN MODAL) 110 | if (!presentPageProperties.includes(propertyName)) { 111 | setOpenedAddPropertiesPopup(true); 112 | } 113 | 114 | // IF PROPERTY IS ALREADY PRESENT, JUST TURN ON THE SETTING 115 | else { 116 | setWhereToWatchSetting(true); 117 | } 118 | }; 119 | 120 | return ( 121 | <> 122 | 123 |
124 |
125 | 126 |

Settings

127 | { 133 | e.currentTarget.checked 134 | ? handleAddPropertiesToDb("Watch Provider", { 135 | properties: { 136 | ["Watch Provider"]: { 137 | type: "multi_select", 138 | multi_select: {}, 139 | }, 140 | }, 141 | }) 142 | : setWhereToWatchSetting(false); 143 | }} 144 | /> 145 |
146 |
147 | 148 | { 151 | presentSettingToChange.name === "Watch Provider" && 152 | setWhereToWatchSetting(false); 153 | setOpenedAddPropertiesPopup(false); 154 | }} 155 | title="Found Missing Properties" 156 | > 157 | 158 | The property{" "} 159 | 160 | {presentSettingToChange.name} 161 | {" "} 162 | is missing from the page database. Shall I add them ? 163 | 164 | 172 | 179 | 180 | 181 | 212 | 213 | ); 214 | } 215 | 216 | export default Settings; 217 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | a { 7 | display: inline-block; 8 | color: #228be6; 9 | font-weight: bold; 10 | text-decoration: none; 11 | border-bottom: 1px solid #228be6 !important; 12 | } 13 | body { 14 | background-color: #1a1b1e; 15 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%232f3034' fill-opacity='0.4'%3E%3Cpath d='M24.37 16c.2.65.39 1.32.54 2H21.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06A5 5 0 0 1-17.45 28v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H-20a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1L.9 19.22a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0L2.26 23h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM-13.82 27l16.37 4.91L18.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H-13.1z'/%3E%3Cpath id='path6_fill-copy' d='M284.37 16c.2.65.39 1.32.54 2H281.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06a5 5 0 0 1-2.24-8.94v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H240a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM246.18 27l16.37 4.91L278.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H246.9z'/%3E%3Cpath d='M159.5 21.02A9 9 0 0 0 151 15h-42a9 9 0 0 0-8.5 6.02 6 6 0 0 0 .02 11.96A8.99 8.99 0 0 0 109 45h42a9 9 0 0 0 8.48-12.02 6 6 0 0 0 .02-11.96zM151 17h-42a7 7 0 0 0-6.33 4h54.66a7 7 0 0 0-6.33-4zm-9.34 26a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-7a7 7 0 1 1 0-14h42a7 7 0 1 1 0 14h-9.34zM109 27a9 9 0 0 0-7.48 4H101a4 4 0 1 1 0-8h58a4 4 0 0 1 0 8h-.52a9 9 0 0 0-7.48-4h-42z'/%3E%3Cpath d='M39 115a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm6-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0zm-3-29v-2h8v-6H40a4 4 0 0 0-4 4v10H22l-1.33 4-.67 2h2.19L26 130h26l3.81-40H58l-.67-2L56 84H42v-6zm-4-4v10h2V74h8v-2h-8a2 2 0 0 0-2 2zm2 12h14.56l.67 2H22.77l.67-2H40zm13.8 4H24.2l3.62 38h22.36l3.62-38z'/%3E%3Cpath d='M129 92h-6v4h-6v4h-6v14h-3l.24 2 3.76 32h36l3.76-32 .24-2h-3v-14h-6v-4h-6v-4h-8zm18 22v-12h-4v4h3v8h1zm-3 0v-6h-4v6h4zm-6 6v-16h-4v19.17c1.6-.7 2.97-1.8 4-3.17zm-6 3.8V100h-4v23.8a10.04 10.04 0 0 0 4 0zm-6-.63V104h-4v16a10.04 10.04 0 0 0 4 3.17zm-6-9.17v-6h-4v6h4zm-6 0v-8h3v-4h-4v12h1zm27-12v-4h-4v4h3v4h1v-4zm-6 0v-8h-4v4h3v4h1zm-6-4v-4h-4v8h1v-4h3zm-6 4v-4h-4v8h1v-4h3zm7 24a12 12 0 0 0 11.83-10h7.92l-3.53 30h-32.44l-3.53-30h7.92A12 12 0 0 0 130 126z'/%3E%3Cpath d='M212 86v2h-4v-2h4zm4 0h-2v2h2v-2zm-20 0v.1a5 5 0 0 0-.56 9.65l.06.25 1.12 4.48a2 2 0 0 0 1.94 1.52h.01l7.02 24.55a2 2 0 0 0 1.92 1.45h4.98a2 2 0 0 0 1.92-1.45l7.02-24.55a2 2 0 0 0 1.95-1.52L224.5 96l.06-.25a5 5 0 0 0-.56-9.65V86a14 14 0 0 0-28 0zm4 0h6v2h-9a3 3 0 1 0 0 6H223a3 3 0 1 0 0-6H220v-2h2a12 12 0 1 0-24 0h2zm-1.44 14l-1-4h24.88l-1 4h-22.88zm8.95 26l-6.86-24h18.7l-6.86 24h-4.98zM150 242a22 22 0 1 0 0-44 22 22 0 0 0 0 44zm24-22a24 24 0 1 1-48 0 24 24 0 0 1 48 0zm-28.38 17.73l2.04-.87a6 6 0 0 1 4.68 0l2.04.87a2 2 0 0 0 2.5-.82l1.14-1.9a6 6 0 0 1 3.79-2.75l2.15-.5a2 2 0 0 0 1.54-2.12l-.19-2.2a6 6 0 0 1 1.45-4.46l1.45-1.67a2 2 0 0 0 0-2.62l-1.45-1.67a6 6 0 0 1-1.45-4.46l.2-2.2a2 2 0 0 0-1.55-2.13l-2.15-.5a6 6 0 0 1-3.8-2.75l-1.13-1.9a2 2 0 0 0-2.5-.8l-2.04.86a6 6 0 0 1-4.68 0l-2.04-.87a2 2 0 0 0-2.5.82l-1.14 1.9a6 6 0 0 1-3.79 2.75l-2.15.5a2 2 0 0 0-1.54 2.12l.19 2.2a6 6 0 0 1-1.45 4.46l-1.45 1.67a2 2 0 0 0 0 2.62l1.45 1.67a6 6 0 0 1 1.45 4.46l-.2 2.2a2 2 0 0 0 1.55 2.13l2.15.5a6 6 0 0 1 3.8 2.75l1.13 1.9a2 2 0 0 0 2.5.8zm2.82.97a4 4 0 0 1 3.12 0l2.04.87a4 4 0 0 0 4.99-1.62l1.14-1.9a4 4 0 0 1 2.53-1.84l2.15-.5a4 4 0 0 0 3.09-4.24l-.2-2.2a4 4 0 0 1 .97-2.98l1.45-1.67a4 4 0 0 0 0-5.24l-1.45-1.67a4 4 0 0 1-.97-2.97l.2-2.2a4 4 0 0 0-3.09-4.25l-2.15-.5a4 4 0 0 1-2.53-1.84l-1.14-1.9a4 4 0 0 0-5-1.62l-2.03.87a4 4 0 0 1-3.12 0l-2.04-.87a4 4 0 0 0-4.99 1.62l-1.14 1.9a4 4 0 0 1-2.53 1.84l-2.15.5a4 4 0 0 0-3.09 4.24l.2 2.2a4 4 0 0 1-.97 2.98l-1.45 1.67a4 4 0 0 0 0 5.24l1.45 1.67a4 4 0 0 1 .97 2.97l-.2 2.2a4 4 0 0 0 3.09 4.25l2.15.5a4 4 0 0 1 2.53 1.84l1.14 1.9a4 4 0 0 0 5 1.62l2.03-.87zM152 207a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6 2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11 1a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-6 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-8 8a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4 7a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5-2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5 4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-5-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-24 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm16 5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm7-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0zm86-29a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1 246 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM275 214a29 29 0 0 0-57.97 0h57.96zM72.33 198.12c-.21-.32-.34-.7-.34-1.12v-12h-2v12a4.01 4.01 0 0 0 7.09 2.54c.57-.69.91-1.57.91-2.54v-12h-2v12a1.99 1.99 0 0 1-2 2 2 2 0 0 1-1.66-.88zM75 176c.38 0 .74-.04 1.1-.12a4 4 0 0 0 6.19 2.4A13.94 13.94 0 0 1 84 185v24a6 6 0 0 1-6 6h-3v9a5 5 0 1 1-10 0v-9h-3a6 6 0 0 1-6-6v-24a14 14 0 0 1 14-14 5 5 0 0 0 5 5zm-17 15v12a1.99 1.99 0 0 0 1.22 1.84 2 2 0 0 0 2.44-.72c.21-.32.34-.7.34-1.12v-12h2v12a3.98 3.98 0 0 1-5.35 3.77 3.98 3.98 0 0 1-.65-.3V209a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-24c.01-1.53-.23-2.88-.72-4.17-.43.1-.87.16-1.28.17a6 6 0 0 1-5.2-3 7 7 0 0 1-6.47-4.88A12 12 0 0 0 58 185v6zm9 24v9a3 3 0 1 0 6 0v-9h-6z'/%3E%3Cpath d='M-17 191a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2H4zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1-14 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM15 214a29 29 0 0 0-57.97 0h57.96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); 16 | } 17 | -------------------------------------------------------------------------------- /pages/set-watchlist-page.js: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Button, 4 | Input, 5 | LoadingOverlay, 6 | Notification, 7 | Paper, 8 | Text, 9 | Modal, 10 | Group, 11 | } from "@mantine/core"; 12 | import React, { useContext, useEffect, useState } from "react"; 13 | import Navbar from "../components/Navbar"; 14 | import { FiSave, FiSearch, FiTool, FiX } from "react-icons/fi"; 15 | import { NotionCredContext } from "../context/NotionCred"; 16 | import { useNotifications } from "@mantine/notifications"; 17 | import router from "next/router"; 18 | 19 | export let getStaticProps = () => { 20 | return { 21 | props: { 22 | APPLICATION_URL: process.env.APPLICATION_URL, 23 | }, 24 | }; 25 | }; 26 | 27 | function SetWatchlistPage({ APPLICATION_URL }) { 28 | const notifications = useNotifications(); 29 | const [search, setSearch] = useState(""); 30 | const [openedConfirmPopup, setOpenedConfirmPopup] = useState(false); 31 | const [openedAddPropertiesPopup, setOpenedAddPropertiesPopup] = 32 | useState(false); 33 | const [searchLoading, setSearchLoading] = useState(false); 34 | const [propertiesToAdd, setPropertiesToAdd] = useState([]); 35 | let [notionUserCredentials] = useContext(NotionCredContext); 36 | const [searchPagesData, setSearchPagesData] = useState([]); 37 | const [pageToSelect, setPageToSelect] = useState(""); 38 | 39 | useEffect(() => { 40 | if (localStorage.getItem("NOTION_WATCHLIST_PAGE_ID") !== null) { 41 | router.push("/dashboard"); 42 | } 43 | }, []); 44 | 45 | let handleSetWatchlistPageSearchSubmit = async (e) => { 46 | e.preventDefault(); 47 | if (search.trim() === "") { 48 | notifications.showNotification({ 49 | title: "Search Field is Empty", 50 | message: "Please enter a search term.", 51 | color: "red", 52 | autoClose: 3000, 53 | }); 54 | return; 55 | } 56 | setSearchLoading(true); 57 | let response = await fetch(`${APPLICATION_URL}/api/search-watchlist-page`, { 58 | method: "POST", 59 | headers: { 60 | "Content-Type": "application/json", 61 | }, 62 | body: JSON.stringify({ 63 | token: notionUserCredentials.access_token, 64 | search: search, 65 | }), 66 | }); 67 | 68 | let data = await response.json(); 69 | let onlyDatabaseResults = data.results.filter( 70 | (page) => page.title != undefined 71 | ); 72 | await setSearchPagesData(onlyDatabaseResults); 73 | if (data.results.length != 0 && onlyDatabaseResults.length === 0) { 74 | notifications.showNotification({ 75 | title: "The Page is Invalid", 76 | message: 77 | "Watchlist page must be a FULL PAGE DATABASE notion page. If you dont have one, Try logging out this app by pressing your avatar on top left and duplicate the starter page shown at beginning and try linking to notion again", 78 | color: "red", 79 | autoClose: false, 80 | }); 81 | } 82 | if (onlyDatabaseResults.length === 0 && data.results.length === 0) { 83 | notifications.showNotification({ 84 | title: "No Pages Found", 85 | message: 86 | "The page may be non existent or you may have not granted the permission for this application to access the page. if so, click on you profile picture in top right, logout and try linking to notion again", 87 | color: "red", 88 | autoClose: false, 89 | }); 90 | } 91 | setSearchLoading(false); 92 | }; 93 | let handlePageConfirmation = () => { 94 | const selectedPageProperties = Object.keys( 95 | searchPagesData.filter((page) => page.id == pageToSelect)[0].properties 96 | ); 97 | const pagePropertiesToAdd = ["Tags"].filter( 98 | (x) => !selectedPageProperties.includes(x) 99 | ); 100 | setOpenedConfirmPopup(false); 101 | if (pagePropertiesToAdd.length != 0) { 102 | setPropertiesToAdd(pagePropertiesToAdd); 103 | setOpenedAddPropertiesPopup(true); 104 | } else { 105 | localStorage.setItem("NOTION_WATCHLIST_PAGE_ID", pageToSelect); 106 | notifications.showNotification({ 107 | title: "Page selected", 108 | message: 109 | "your watchlist page is saved to your browser so you dont have to do this step again", 110 | }); 111 | window.location.href = `${APPLICATION_URL}/dashboard`; 112 | } 113 | }; 114 | 115 | let handleMissingPropertiesAdd = async () => { 116 | setSearchLoading(true); 117 | let body_content = { 118 | properties: { 119 | Tags: { 120 | type: "multi_select", 121 | multi_select: [], 122 | }, 123 | }, 124 | }; 125 | let response = await fetch(`${APPLICATION_URL}/api/update-db-properties`, { 126 | method: "POST", 127 | headers: { 128 | "Content-Type": "application/json", 129 | }, 130 | body: JSON.stringify({ 131 | token: notionUserCredentials.access_token, 132 | database_id: pageToSelect, 133 | body_content: body_content, 134 | }), 135 | }); 136 | setSearchLoading(false); 137 | let data = await response.json(); 138 | if (response.status == 200) { 139 | localStorage.setItem("NOTION_WATCHLIST_PAGE_ID", pageToSelect); 140 | notifications.showNotification({ 141 | title: "Added Properties and Page selected", 142 | message: 143 | "your watchlist page is saved to your browser so you dont have to do this step again", 144 | }); 145 | window.location.href = `${APPLICATION_URL}/dashboard`; 146 | } else { 147 | alert("Something went wrong. Please Try again"); 148 | router.push("/"); 149 | } 150 | }; 151 | return ( 152 | <> 153 | 154 |
155 | 156 | 157 |
158 | 159 | 🔗 Confirm Watchlist Page 160 | 161 | 162 | Search for the title of the page you duplicated before and allowed 163 | this application to access it. 164 | 165 |
166 | setSearch(e.target.value)} 169 | className="setWatchlistPage__search" 170 | icon={} 171 | placeholder="eg: watchlist" 172 | type="text" 173 | /> 174 | 183 | 184 |
185 | {searchPagesData.length > 0 && ( 186 |
187 | 188 | Select the Page to link 189 | 190 | {searchPagesData.map((page) => ( 191 | 214 | ))} 215 |
216 | )} 217 |
218 |
219 | 220 | setOpenedConfirmPopup(false)} 223 | title="Confirm page link" 224 | > 225 | 226 | Are you sure this is the correct page ? 227 | 228 | 236 | 245 | 252 | 253 | 254 | 255 | setOpenedAddPropertiesPopup(false)} 258 | title="Found Missing Properties" 259 | > 260 | 266 | The properties {propertiesToAdd.join()} are missing from the page 267 | database. Shall I add them ? 268 | 269 | 277 | 284 | 285 | 286 | 330 | 331 | ); 332 | } 333 | 334 | export default SetWatchlistPage; 335 | -------------------------------------------------------------------------------- /pages/content/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Blockquote, 3 | Button, 4 | Image, 5 | Loader, 6 | Skeleton, 7 | } from "@mantine/core"; 8 | import { useRouter } from "next/router"; 9 | import React, { useContext, useEffect, useState } from "react"; 10 | import { FiArrowLeft, FiPlusCircle } from "react-icons/fi"; 11 | import { RiMovie2Line } from "react-icons/ri"; 12 | import Navbar from "../../components/Navbar"; 13 | import { NotionCredContext } from "../../context/NotionCred"; 14 | import { useNotifications } from "@mantine/notifications"; 15 | import { SettingsContext } from "../../context/Settings"; 16 | 17 | export const getServerSideProps = ({ query }) => ({ 18 | props: { 19 | COUNTRY_CODE: query.country ?? "US", 20 | TMDB_API_KEY: process.env.TMDB_API_KEY, 21 | APPLICATION_URL: process.env.APPLICATION_URL, 22 | }, 23 | }); 24 | 25 | function Content({ APPLICATION_URL, TMDB_API_KEY, COUNTRY_CODE }) { 26 | const notifications = useNotifications(); 27 | let router = useRouter(); 28 | let [notionUserCredentials] = useContext(NotionCredContext); 29 | let [whereToWatchSetting] = useContext(SettingsContext); 30 | let [mediaData, setMediaData] = useState({}); 31 | let [watchProviders, setWatchProviders] = useState([]); 32 | 33 | useEffect(() => { 34 | let id = router.query.id; 35 | let type = router.query.type; 36 | async function fetchData() { 37 | // FETCH DATA FROM TMDB 38 | let response = await fetch( 39 | `https://api.themoviedb.org/3/${type}/${id}?api_key=${TMDB_API_KEY}&language=en-US` 40 | ); 41 | let data = await response.json(); 42 | setMediaData(data); 43 | 44 | // IF WHERE TO WATCH SETTING IS TRUE, FETCH PROVIDERS DATA 45 | if (whereToWatchSetting) { 46 | // FETCH PROVIDERS DATA 47 | let providerRes = await fetch( 48 | `https://api.themoviedb.org/3/${type}/${id}/watch/providers?api_key=${TMDB_API_KEY}` 49 | ); 50 | let data = await providerRes.json(); 51 | 52 | // FILTER PROVIDERS DATA BY USER COUNTRY CODE 53 | let providers = data.results[COUNTRY_CODE] 54 | ? data.results[COUNTRY_CODE] 55 | : data.results["US"]; 56 | setWatchProviders(providers); 57 | } 58 | } 59 | fetchData(); 60 | }, [COUNTRY_CODE, TMDB_API_KEY, router.query.id, router.query.type, whereToWatchSetting]); 61 | 62 | const handleAddToNotionClick = async () => { 63 | try { 64 | const databaseID = localStorage.getItem("NOTION_WATCHLIST_PAGE_ID"); 65 | if (!databaseID || !mediaData) { 66 | throw new Error("Required data is missing."); 67 | } 68 | 69 | const { name, title, genres, poster_path, backdrop_path, overview, tagline } = mediaData; 70 | const selectedName = name || title; 71 | 72 | // Construct properties 73 | const properties = { 74 | Name: { 75 | title: [ 76 | { 77 | text: { content: selectedName }, 78 | }, 79 | ], 80 | }, 81 | Tags: { 82 | multi_select: genres.map((genre) => ({ name: genre.name })), 83 | }, 84 | }; 85 | 86 | if (whereToWatchSetting && watchProviders?.flatrate) { 87 | properties["Watch Provider"] = { 88 | multi_select: watchProviders.flatrate.map((provider) => ({ name: provider.provider_name })), 89 | }; 90 | } 91 | 92 | // Construct body content 93 | const body_content = { 94 | parent: { database_id: databaseID }, 95 | properties, 96 | icon: { 97 | type: "external", 98 | external: { url: `https://image.tmdb.org/t/p/original${poster_path}` }, 99 | }, 100 | cover: { 101 | type: "external", 102 | external: { 103 | url: backdrop_path 104 | ? `https://image.tmdb.org/t/p/original${backdrop_path}` 105 | : `https://image.tmdb.org/t/p/original${poster_path}`, 106 | }, 107 | }, 108 | children: [ 109 | { 110 | object: "block", 111 | type: "heading_2", 112 | heading_2: { 113 | text: [{ type: "text", text: { content: "Description" } }], 114 | }, 115 | }, 116 | { 117 | object: "block", 118 | type: "paragraph", 119 | paragraph: { 120 | text: [ 121 | { type: "text", text: { content: overview } }, 122 | ], 123 | }, 124 | }, 125 | { 126 | object: "block", 127 | type: "quote", 128 | quote: { 129 | text: [ 130 | { type: "text", text: { content: tagline } }, 131 | ], 132 | }, 133 | }, 134 | ], 135 | }; 136 | 137 | console.log(body_content); 138 | 139 | const response = await fetch(`${APPLICATION_URL}/api/add-page-to-db`, { 140 | method: "POST", 141 | headers: { 142 | "Content-Type": "application/json", 143 | }, 144 | body: JSON.stringify({ 145 | token: notionUserCredentials.access_token, 146 | body_content: JSON.stringify(body_content), 147 | }), 148 | }); 149 | 150 | const data = await response.json(); 151 | console.log(data); 152 | if(data.object === "error" || data.error){ 153 | console.log(data); 154 | return; 155 | } else { 156 | notifications.showNotification({ 157 | title: "Added To Notion", 158 | message: `Added ${selectedName} to your page`, 159 | }); 160 | } 161 | window.location.replace(`${APPLICATION_URL}/dashboard`); 162 | } catch (error) { 163 | console.error("Error:", error); 164 | notifications.showNotification({ 165 | title: "Error", 166 | message: `An error occurred: ${error.message}`, 167 | }); 168 | } 169 | }; 170 | 171 | if (mediaData.id) { 172 | return ( 173 | <> 174 | 175 |
176 |
177 | {mediaData.poster_path ? ( 178 | {`${mediaData.title} 183 | ) : ( 184 |
185 | )} 186 |
187 |

{mediaData?.name ? mediaData?.name : mediaData?.title}

188 | 189 | {mediaData?.tagline && ( 190 |
194 | {mediaData?.tagline} 195 |
196 | )} 197 |

{mediaData.overview}

198 |
199 | {mediaData.genres?.map((genre) => ( 200 |
201 | {genre.name} 202 |
203 | ))} 204 |
205 |
206 | 214 | 221 |
222 |
223 |
224 |
225 | {whereToWatchSetting && watchProviders?.flatrate && ( 226 |
227 |

228 | Watch On 229 |

230 |
231 | {watchProviders?.flatrate.map((provider) => ( 232 |
233 |
234 | {provider.name} 240 |
241 |
242 |

{provider.provider_name}

243 |
244 |
245 | ))} 246 |
247 |
248 | )} 249 |
250 |
251 | 377 | 378 | ); 379 | } else { 380 | return ( 381 |
384 | 385 |
386 | ); 387 | } 388 | } 389 | 390 | export default Content; 391 | --------------------------------------------------------------------------------