diff --git a/components/PostLayout.tsx b/components/PostLayout.tsx index 73d7b1c..9460b3d 100644 --- a/components/PostLayout.tsx +++ b/components/PostLayout.tsx @@ -6,7 +6,7 @@ import { NavMenu } from "./common"; import TitleHead from "./TitleHead"; import Logo from "./common/Logo"; -const PostLayout = ({ children, title, permalink, thumbnail, token }: Props) => ( +const PostLayout = ({ children, title, permalink, thumbnail }: Props) => (
@@ -30,7 +30,7 @@ const PostLayout = ({ children, title, permalink, thumbnail, token }: Props) =>
- + {children} diff --git a/components/Subpage.tsx b/components/Subpage.tsx index 3f22816..e42dbfc 100644 --- a/components/Subpage.tsx +++ b/components/Subpage.tsx @@ -9,7 +9,6 @@ const Subpage = ({ children, title, subreddit, - token, backgroundColor = "white" }: Props) => (
@@ -28,7 +27,7 @@ const Subpage = ({
- + {children} diff --git a/components/common/Header.tsx b/components/common/Header.tsx index cb2dda6..424200f 100644 --- a/components/common/Header.tsx +++ b/components/common/Header.tsx @@ -8,7 +8,7 @@ interface HeaderProps { className?: string; } -const Header: React.FC = ({ token, className = '' }) => { +const Header: React.FC = ({ className = '' }) => { return (
- + ); diff --git a/components/common/index.tsx b/components/common/index.tsx index 4bffe51..5b1aca4 100644 --- a/components/common/index.tsx +++ b/components/common/index.tsx @@ -1,12 +1,13 @@ import _ from 'lodash'; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState, useCallback } from "react"; import Image from 'next/image'; import Link from 'next/link'; import { getIntFromString, getTime, limitText } from "../../functions/common"; import { DESC_MAX } from "../../functions/constants"; import { DropdownProps, Props } from "../../interfaces"; -import { useConfig } from '../../lib/ConfigContext'; -import LoginButton from './LoginButton'; // Import the new LoginButton component +import { useConfig } from '../../functions/useConfig'; +import LoginButton from './LoginButton'; +import { useAuth } from '../../contexts/AuthContext'; // Add this import export const MidContainer = ({ children }: Props) => (
{children}
@@ -14,44 +15,61 @@ export const MidContainer = ({ children }: Props) => ( const ProfileOptions = () => { const [showDropdown, setShowDropdown] = useState(false); - const dropdown = useRef(null); + const dropdownRef = useRef(null); + const avatarRef = useRef(null); + + console.log("ProfileOptions - showDropdown:", showDropdown); + + const toggleDropdown = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setShowDropdown(prev => !prev); + }, []); useEffect(() => { - if (!showDropdown) return; - function handleClick(e: any) { - if (dropdown.current && !dropdown.current.contains(e.target)) { + const handleOutsideClick = (e: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(e.target as Node) && + avatarRef.current && + !avatarRef.current.contains(e.target as Node) + ) { setShowDropdown(false); } + }; + + if (showDropdown) { + document.addEventListener('mousedown', handleOutsideClick); } - window.addEventListener("click", handleClick); - return () => window.removeEventListener("click", handleClick); - }); + + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [showDropdown]); return (
setShowDropdown(!showDropdown)} + onClick={toggleDropdown} >
- {showDropdown ? ( + {showDropdown && (
- + View Profile - + Sign out
- ) : ( -
)}
); @@ -130,10 +148,11 @@ export const PostMetadata = ({ ); -export const NavMenu = ({ token = "" }: any) => { +export const NavMenu = () => { const [showSearch, setShowSearch] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const config = useConfig(); + const { config } = useConfig(); // Destructure to get the config object + const { token } = useAuth(); const newSearch = () => (window.location.href = `/search/?q=${searchTerm}`); @@ -189,7 +208,7 @@ export const NavMenu = ({ token = "" }: any) => { )} - {token != "" ? ( + {token ? ( ) : ( diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx new file mode 100644 index 0000000..aa0c3f1 --- /dev/null +++ b/contexts/AuthContext.tsx @@ -0,0 +1,43 @@ +import React, { createContext, useState, useContext, useEffect } from 'react'; +import Cookies from 'js-cookie'; + +interface AuthContextType { + token: string | null; + setToken: (token: string | null) => void; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [token, setToken] = useState(null); + + useEffect(() => { + const storedToken = Cookies.get('token'); + if (storedToken) { + setToken(storedToken); + } + }, []); + + const updateToken = (newToken: string | null) => { + setToken(newToken); + if (newToken) { + Cookies.set('token', newToken, { expires: 7 }); + } else { + Cookies.remove('token'); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/lib/ConfigContext.tsx b/contexts/ConfigContext.tsx similarity index 100% rename from lib/ConfigContext.tsx rename to contexts/ConfigContext.tsx diff --git a/next.config.js b/next.config.js index 1ddba17..f3ce6ef 100644 --- a/next.config.js +++ b/next.config.js @@ -8,6 +8,7 @@ const nextConfig = { publicRuntimeConfig: { REDDIUM_DISABLE_ABOUT: true, REDDIUM_DISABLE_KOFI_LINK: true, + REDDIUM_DISABLE_GITHUB_LINK: true, REDDIUM_DISABLE_LOGIN: true, REDDIUM_DOMAIN: 'http://localhost:3000', ...Object.fromEntries( diff --git a/package-lock.json b/package-lock.json index ac946e6..b6df022 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,10 +20,12 @@ "cookies-js": "^1.2.3", "glob": "^11.0.0", "highlight.run": "^9.5.0", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "micromark": "^4.0.0", "micromark-extension-gfm": "^3.0.0", "next": "^14.0.0", + "nookies": "^2.5.2", "postcss": "^8.4.47", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -35,6 +37,7 @@ }, "devDependencies": { "@eslint/object-schema": "^2.0.0", + "@types/js-cookie": "^3.0.6", "@types/node": "^18.11.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", @@ -1759,6 +1762,13 @@ "integrity": "sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw==", "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2898,6 +2908,15 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookies": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", @@ -5411,6 +5430,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6670,6 +6698,16 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "license": "MIT" }, + "node_modules/nookies": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/nookies/-/nookies-2.5.2.tgz", + "integrity": "sha512-x0TRSaosAEonNKyCrShoUaJ5rrT5KHRNZ5DwPCuizjgrnkpE5DRf3VL7AyyQin4htict92X1EQ7ejDbaHDVdYA==", + "license": "MIT", + "dependencies": { + "cookie": "^0.4.1", + "set-cookie-parser": "^2.4.6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8426,6 +8464,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", + "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 089cd14..23fcfe9 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,12 @@ "cookies-js": "^1.2.3", "glob": "^11.0.0", "highlight.run": "^9.5.0", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "micromark": "^4.0.0", "micromark-extension-gfm": "^3.0.0", "next": "^14.0.0", + "nookies": "^2.5.2", "postcss": "^8.4.47", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -36,6 +38,7 @@ }, "devDependencies": { "@eslint/object-schema": "^2.0.0", + "@types/js-cookie": "^3.0.6", "@types/node": "^18.11.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", diff --git a/pages/_app.tsx b/pages/_app.tsx index edce820..4ab81fc 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,8 +2,9 @@ import React, { useEffect } from "react"; import { AppProps } from "next/app"; import "../styles/styles.css"; import { H } from "highlight.run"; -import { ConfigProvider } from '../lib/ConfigContext' +import { ConfigProvider } from '../contexts/ConfigContext' import { useConfig } from '../functions/useConfig'; +import { AuthProvider } from '../contexts/AuthContext'; if (typeof window !== "undefined") { H.init("5ldw65eo"); @@ -20,9 +21,11 @@ const App = ({ Component, pageProps }: AppProps) => { return ( -
- -
+ +
+ +
+
); }; diff --git a/pages/login/index.tsx b/pages/login/index.tsx index d5e654c..7a61135 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -1,48 +1,49 @@ import { GetServerSideProps } from "next"; import React from "react"; -import Cookies from "cookies"; +import { setCookie } from 'nookies'; import getConfig from 'next/config'; -import { REDIRECT_URI } from "../../functions/constants"; -const { publicRuntimeConfig, serverRuntimeConfig } = getConfig() || {}; +export const getServerSideProps: GetServerSideProps = async (context) => { + const { query } = context; + const config = getConfig().publicRuntimeConfig; + + if (query.code) { + try { + const response = await fetch('https://www.reddit.com/api/v1/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from(`${config.REDDIUM_CLIENT_ID}:${config.REDDIUM_CLIENT_SECRET}`).toString('base64')}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: query.code as string, + redirect_uri: `${config.REDDIUM_DOMAIN}/login`, + }), + }); + + const data = await response.json(); + if (data.access_token) { + setCookie(context, 'token', data.access_token, { + maxAge: 30 * 24 * 60 * 60, // 30 days + path: '/', + }); + } + } catch (error) { + console.error("Error fetching access token:", error); + } + } -export const getServerSideProps: GetServerSideProps = async ({ - req, - res, - query, -}) => { - const cookies = new Cookies(req, res); - if (query.hasOwnProperty("code")) { - const requestOptions = { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Basic ${Buffer.from( - `${publicRuntimeConfig.REDDIUM_CLIENT_ID}:${publicRuntimeConfig.REDDIUM_CLIENT_SECRET}` - ).toString("base64")}`, - }, - body: new URLSearchParams({ - grant_type: "authorization_code", - code: query.code ? query.code.toString() : "", - redirect_uri: REDIRECT_URI, - }), - }; - const resp = await ( - await fetch("https://www.reddit.com/api/v1/access_token", requestOptions) - ).json(); - cookies.set("token", resp.access_token, { maxAge: 600000 }); - delete query.code; - } else { - console.log("No code received: ", query); // Debug output - } - res.statusCode = 302; - res.setHeader("Location", `/`); - res.end(); return { - props: {}, + redirect: { + destination: '/', + permanent: false, + }, }; }; -const LoginPage = () =>
; +const LoginPage = () => { + return
Processing login...
; +}; export default LoginPage; diff --git a/pages/logout/index.tsx b/pages/logout/index.tsx index 3608089..8d48629 100644 --- a/pages/logout/index.tsx +++ b/pages/logout/index.tsx @@ -1,18 +1,24 @@ -import { GetServerSideProps } from "next"; -import React from "react"; -import Cookies from "cookies"; +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { destroyCookie } from 'nookies'; +import { useAuth } from '../../contexts/AuthContext'; -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const cookies = new Cookies(req, res); - cookies.set("token", ""); - res.statusCode = 302; - res.setHeader("Location", `/`); - res.end(); - return { - props: {} - }; -}; +const LogoutPage = () => { + const router = useRouter(); + const { setToken } = useAuth(); + + useEffect(() => { + const logout = async () => { + destroyCookie(null, 'token', { path: '/' }); + setToken(null); + await new Promise(resolve => setTimeout(resolve, 100)); + router.push('/'); + }; -const LogoutPage = () =>
; + logout(); + }, [router, setToken]); + + return
Logging out...
; +}; -export default LogoutPage; +export default LogoutPage; \ No newline at end of file diff --git a/pages/me/index.tsx b/pages/me/index.tsx index c63f44a..a0c9408 100644 --- a/pages/me/index.tsx +++ b/pages/me/index.tsx @@ -8,13 +8,12 @@ import { import React, { useState } from "react"; import Image from 'next/image'; import TitleHead from "../../components/TitleHead"; -import { NavMenu } from "../../components/common"; +import Header from "../../components/common/Header"; // Import the Header component import UserPost from "../../components/user-page/UserPost"; import UserComment from "../../components/user-page/UserComment"; import { getIntFromString } from "../../functions/common"; import { DOMAIN } from "../../functions/constants"; import Cookies from "cookies"; -import Logo from '../../components/common/Logo'; // Import the Logo component export const getServerSideProps: GetServerSideProps = async ({ req, @@ -51,10 +50,7 @@ export const getServerSideProps: GetServerSideProps = async ({ const MePage = ({ postData, userInfo, params }: any) => { const [{ posts, after }, setPostData] = useState(postData); - // const [selectedParams, setSelectedParams] = useState({ - // ...zipObject(POPULAR_PARAM_KEY, POPULAR_PARAM_DEFAULT), - // ...params - // }); + const fetchMorePosts = async () => { const next = await getUserPostsClient({ ...params, @@ -62,6 +58,7 @@ const MePage = ({ postData, userInfo, params }: any) => { }); setPostData({ posts: [...posts, ...next.posts], after: next.after }); }; + return (
@@ -73,16 +70,9 @@ const MePage = ({ postData, userInfo, params }: any) => { /> -
-
- - -
- -
-
-
-
+ +
+
-
- - -
- -
-
-