├── .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 | [](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs)
9 | ## Screenshots
10 |
11 | 
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 |
13 |
14 |
15 |
16 |
17 |
{name}
18 |
19 |
24 | {mediaType}
25 |
26 |
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 | }
50 | variant="gradient"
51 | gradient={{ from: "indigo", to: "cyan", deg: 45 }}
52 | onClick={handleConnectClick}
53 | >
54 | Connect with Notion
55 |
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 |
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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {title}
19 |
20 |
21 |
22 | {genres.map((genre) => (
23 |
24 | {genre.name}
25 |
26 | ))}
27 |
28 |
29 | }
31 | variant="light"
32 | style={{ padding: 0, height: "32px" }}
33 | color="blue"
34 | onClick={() => {
35 | window.location.href = `https://www.notion.so/${id.replace(/-/g, "")}`;
36 | }}
37 | >
38 | View
39 |
40 | }
45 | onClick={() => {
46 | handlePageDeleteConfirm(id);
47 | }}
48 | >
49 | Delete
50 |
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 |
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 | {
103 | setPageToDelete("");
104 | setOpenedDeletePopup(false);
105 | }}
106 | leftIcon={ }
107 | >
108 | Cancel
109 |
110 | }>
111 | Delete
112 |
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 |
41 |
42 |
router.push("/")}>
43 |
47 |
48 |
49 | Watchlister
50 |
51 |
52 |
58 |
59 |
67 |
68 |
69 | {matches && (
70 |
71 |
72 | {notionUserCredentials?.owner?.user?.name}
73 |
74 |
75 | {notionUserCredentials?.owner?.user?.person?.email}
76 |
77 |
78 | )}
79 |
80 |
81 | }
82 | >
83 | }
85 | onClick={() => router.push("/settings")}
86 | >
87 | Settings
88 |
89 | } onClick={handleLogoutClick}>
90 | Logout
91 |
92 |
93 |
94 |
95 |
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 |
}
49 | size="lg"
50 | onClick={() => {
51 | router.push("/dashboard");
52 | }}
53 | >
54 | Back
55 |
56 |
Search Results
57 | {contentLoading &&
58 | Array.from(Array(9)).map((e, i) => (
59 |
60 | ))}
61 | {searchData.map((result) => (
62 |
{
66 | router.push(`/content?id=${result.id}&type=${result.media_type}`);
67 | }}
68 | >
69 |
75 |
76 |
{result.name || result.title}
77 |
{truncateString(result.overview, 100)}
78 |
79 | {result.media_type == "tv" ? "tv" : "movie"}
80 |
81 |
82 |
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 | }
175 | onClick={handleMissingPropertiesAdd}
176 | >
177 | Ok, Add them
178 |
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 |
184 |
185 | {searchPagesData.length > 0 && (
186 |
187 |
188 | Select the Page to link
189 |
190 | {searchPagesData.map((page) => (
191 |
{
195 | setPageToSelect(page.id);
196 | setOpenedConfirmPopup(true);
197 | }}
198 | >
199 |
200 | {page.icon.type === "emoji" ? (
201 | page.icon.emoji
202 | ) : page.icon.type === "file" ? (
203 |
204 | ) : page.icon.type === "external" ? (
205 |
206 | ) : (
207 | <>>
208 | )}
209 |
210 |
211 | {page?.title[0].text.content}
212 |
213 |
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 | {
238 | setPageToSelect("");
239 | setOpenedConfirmPopup(false);
240 | }}
241 | leftIcon={ }
242 | >
243 | Cancel
244 |
245 | }
249 | >
250 | Yes I'm sure
251 |
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 | }
280 | onClick={handleMissingPropertiesAdd}
281 | >
282 | Ok, Add them
283 |
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 |
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 | }
210 | onClick={handleAddToNotionClick}
211 | >
212 | Add to Notion
213 |
214 | }
217 | onClick={() => router.push("/dashboard")}
218 | >
219 | Back to Dashboard
220 |
221 |
222 |
223 |
224 |
225 | {whereToWatchSetting && watchProviders?.flatrate && (
226 |
227 |
228 | Watch On
229 |
230 |
231 | {watchProviders?.flatrate.map((provider) => (
232 |
233 |
234 |
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 |
--------------------------------------------------------------------------------