diff --git a/client.ts b/client.ts index e3585d6a1..627133411 100644 --- a/client.ts +++ b/client.ts @@ -51,6 +51,7 @@ params.theme = attributes.theme; params.reactionsEnabled = attributes.reactionsEnabled || '1'; params.repo = attributes.repo; params.repoId = attributes.repoId; +params.category = attributes.category || ''; params.categoryId = attributes.categoryId; params.description = ogDescriptionMeta ? ogDescriptionMeta.content : ''; diff --git a/components/CommentBox.tsx b/components/CommentBox.tsx index d4e32bd3e..531e46f9f 100644 --- a/components/CommentBox.tsx +++ b/components/CommentBox.tsx @@ -1,7 +1,7 @@ import { MarkdownIcon } from '@primer/octicons-react'; import { ChangeEvent, useCallback, useContext, useEffect, useState } from 'react'; import { adaptComment, adaptReply, handleCommentClick, processCommentBody } from '../lib/adapter'; -import { AuthContext, getLoginUrl } from '../lib/context'; +import { AuthContext } from '../lib/context'; import { IComment, IReply, IUser } from '../lib/types/adapter'; import { resizeTextArea } from '../lib/utils'; import { addDiscussionComment } from '../services/github/addDiscussionComment'; @@ -34,7 +34,7 @@ export default function CommentBox({ const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isReplyOpen, setIsReplyOpen] = useState(false); - const { token, origin } = useContext(AuthContext); + const { token, origin, getLoginUrl } = useContext(AuthContext); const loginUrl = getLoginUrl(origin); const isReply = !!replyToId; diff --git a/components/Configuration.tsx b/components/Configuration.tsx index 2264810ed..41cc9961e 100644 --- a/components/Configuration.tsx +++ b/components/Configuration.tsx @@ -1,12 +1,41 @@ import { CheckIcon, ClippyIcon, SyncIcon, XIcon } from '@primer/octicons-react'; -import { useEffect, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { handleClipboardCopy } from '../lib/adapter'; import { useDebounce } from '../lib/hooks'; import { ICategory } from '../lib/types/adapter'; import { themeOptions } from '../lib/variables'; import { getCategories } from '../services/giscus/categories'; -const mappingOptions = [ +interface IDirectConfig { + theme: string; + reactionsEnabled: boolean; +} + +interface IConfigurationProps { + directConfig: IDirectConfig; + onDirectConfigChange: ( + key: keyof IDirectConfig, + value: IDirectConfig[keyof IDirectConfig], + ) => void; +} + +type Mapping = 'pathname' | 'url' | 'title' | 'og:title' | 'specific' | 'number'; + +interface IConfig { + repository: string; + repositoryId: string; + mapping: Mapping; + term: string; + category: string; + categoryId: string; + useCategory: boolean; +} + +const mappingOptions: Array<{ + value: Mapping; + label: ReactNode; + description: ReactNode; +}> = [ { value: 'pathname', label: ( @@ -92,35 +121,28 @@ function ClipboardCopy() { ); } -interface DirectConfig { - theme: string; - reactionsEnabled: boolean; -} - -interface ConfigurationProps { - directConfig: DirectConfig; - onDirectConfigChange: (key: keyof DirectConfig, value: DirectConfig[keyof DirectConfig]) => void; -} - -export default function Configuration({ directConfig, onDirectConfigChange }: ConfigurationProps) { - const [repository, setRepository] = useState(''); - const [repositoryId, setRepositoryId] = useState(''); - const [categoryId, setCategoryId] = useState(''); +export default function Configuration({ directConfig, onDirectConfigChange }: IConfigurationProps) { + const [config, setConfig] = useState({ + repository: '', + repositoryId: '', + mapping: 'pathname', + term: '', + category: '', + categoryId: '', + useCategory: true, + }); const [error, setError] = useState(false); const [categories, setCategories] = useState([]); - const [mapping, setMapping] = useState('pathname'); - const [term, setTerm] = useState(''); - const dRepository = useDebounce(repository); + const dRepository = useDebounce(config.repository); useEffect(() => { setError(false); - setRepositoryId(''); - setCategoryId(''); + setConfig((current) => ({ ...current, repositoryId: '', category: '', categoryId: '' })); setCategories([]); if (dRepository) { getCategories(dRepository) .then(({ repositoryId, categories }) => { - setRepositoryId(repositoryId); + setConfig((current) => ({ ...current, repositoryId })); setCategories(categories); }) .catch(() => { @@ -165,14 +187,16 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co setRepository(event.target.value)} + value={config.repository} + onChange={(event) => + setConfig((current) => ({ ...current, repository: event.target.value })) + } type="text" className="my-2 px-[12px] py-[5px] min-w-[75%] sm:min-w-[50%] form-control border rounded-md placeholder-gray-500" placeholder="owner/repo" /> - {error || (repositoryId && !categories.length) ? ( + {error || (config.repositoryId && !categories.length) ? ( <>

@@ -180,7 +204,7 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co met.

- ) : repositoryId && categories.length ? ( + ) : config.repositoryId && categories.length ? ( <>

@@ -189,7 +213,7 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co ) : ( <> - {!error && !repositoryId && dRepository ? ( + {!error && !config.repositoryId && dRepository ? ( ) : null}

@@ -200,32 +224,6 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co )} -

Discussion Category

-

- Choose the discussion category where new discussions will be created. This is only used for - discussion creation and does not affect how giscus searches for - discussions. -

- -

Page ↔️ Discussions Mapping

Choose the mapping between the embedding page and the embedded discussion.

@@ -237,25 +235,30 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co type="radio" name="mapping" value={value} - checked={mapping === value} - onChange={(event) => { - setTerm(''); - setMapping(event.target.value); - }} + checked={config.mapping === value} + onChange={(event) => + setConfig((current) => ({ + ...current, + term: '', + mapping: event.target.value as Mapping, + })) + } />

{description}

- {['specific', 'number'].includes(mapping) && mapping === value ? ( + {['specific', 'number'].includes(config.mapping) && config.mapping === value ? ( setTerm(event.target.value)} - type={mapping === 'number' ? 'number' : 'text'} + value={config.term} + onChange={(event) => + setConfig((current) => ({ ...current, term: event.target.value })) + } + type={config.mapping === 'number' ? 'number' : 'text'} className="px-[12px] py-[5px] mt-4 form-control border rounded-md placeholder-gray-500 min-w-[75%] sm:min-w-[50%]" placeholder={ - mapping === 'number' ? 'Enter discussion number here' : 'Enter term here' + config.mapping === 'number' ? 'Enter discussion number here' : 'Enter term here' } /> ) : null} @@ -263,6 +266,69 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co ))}
+

Discussion Category

+

+ Choose the discussion category where new discussions will be created.{' '} + {config.mapping === 'number' ? ( + <> + This feature is not supported if you use the specific discussion number{' '} + mapping. + + ) : ( + <> + It is recommended to use a category with the Announcements type so that + new discussions can only be created by maintainers and giscus. + + )} +

+ +
+ + setConfig((current) => ({ ...current, useCategory: event.target.checked })) + } + > + +

+ When searching for a matching discussion, giscus will only search in this category. +

+
+

Features

Choose whether specific features should be enabled.

@@ -320,21 +386,34 @@ export default function Configuration({ directConfig, onDirectConfigChange }: Co https://giscus.app/client.js {'"\n '} data-repo={'"'} - {repository || '[ENTER REPO HERE]'} + {config.repository || '[ENTER REPO HERE]'} {'"\n '} data-repo-id={'"'} - {repositoryId || '[ENTER REPO ID HERE]'} - {'"\n '} - data-category-id={'"'} - {categoryId || '[ENTER CATEGORY ID HERE]'} + {config.repositoryId || '[ENTER REPO ID HERE]'} {'"\n '} + {config.mapping !== 'number' ? ( + <> + {config.useCategory ? ( + <> + data-category={'"'} + {config.category || '[ENTER CATEGORY NAME HERE]'} + {'"\n '} + + ) : null} + data-category-id={'"'} + {config.categoryId || '[ENTER CATEGORY ID HERE]'} + {'"\n '} + + ) : null} data-mapping={'"'} - {mapping} + {config.mapping} {'"\n '} - {['specific', 'number'].includes(mapping) ? ( + {['specific', 'number'].includes(config.mapping) ? ( <> data-term={'"'} - {term || '[ENTER TERM HERE]'} + + {config.term || `[ENTER ${config.mapping === 'number' ? 'NUMBER' : 'TERM'} HERE]`} + {'"\n '} ) : null} diff --git a/components/Giscus.tsx b/components/Giscus.tsx index 1b5c5a0b1..301a2ca9a 100644 --- a/components/Giscus.tsx +++ b/components/Giscus.tsx @@ -1,5 +1,5 @@ import { useCallback, useContext } from 'react'; -import { AuthContext } from '../lib/context'; +import { AuthContext, ConfigContext } from '../lib/context'; import { Reactions, updateDiscussionReaction } from '../lib/reactions'; import { useDiscussions } from '../services/giscus/discussions'; import Comment from './Comment'; @@ -7,24 +7,14 @@ import CommentBox from './CommentBox'; import ReactButtons from './ReactButtons'; interface IGiscusProps { - repo: string; - term?: string; - number?: number; - reactionsEnabled: boolean; onDiscussionCreateRequest?: () => Promise; onError?: (message: string) => void; } -export default function Giscus({ - repo, - term, - number, - reactionsEnabled, - onDiscussionCreateRequest, - onError, -}: IGiscusProps) { +export default function Giscus({ onDiscussionCreateRequest, onError }: IGiscusProps) { const { token } = useContext(AuthContext); - const query = { repo, term, number }; + const { repo, term, number, category, reactionsEnabled } = useContext(ConfigContext); + const query = { repo, term, category, number }; const backComments = useDiscussions(query, token, { last: 15 }); const { diff --git a/components/ReactButtons.tsx b/components/ReactButtons.tsx index 1314e0515..aec0f6ddd 100644 --- a/components/ReactButtons.tsx +++ b/components/ReactButtons.tsx @@ -1,6 +1,6 @@ import { SmileyIcon } from '@primer/octicons-react'; import { useCallback, useContext, useState } from 'react'; -import { AuthContext, getLoginUrl } from '../lib/context'; +import { AuthContext } from '../lib/context'; import { useComponentVisible } from '../lib/hooks'; import { IReactionGroups } from '../lib/types/adapter'; import { Reactions } from '../lib/reactions'; @@ -47,7 +47,7 @@ export default function ReactButtons({ const [current, setCurrent] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [ref, isOpen, setIsOpen] = useComponentVisible(false); - const { token, origin } = useContext(AuthContext); + const { token, origin, getLoginUrl } = useContext(AuthContext); const loginUrl = getLoginUrl(origin); const togglePopover = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]); diff --git a/components/Widget.tsx b/components/Widget.tsx index 96c5aa9e0..3893c0dbc 100644 --- a/components/Widget.tsx +++ b/components/Widget.tsx @@ -1,50 +1,37 @@ -import { NextRouter, useRouter } from 'next/router'; import { useCallback, useState } from 'react'; import Giscus from '../components/Giscus'; -import { AuthContext } from '../lib/context'; -import { useIsMounted } from '../lib/hooks'; +import { AuthContext, ConfigContext, getLoginUrl } from '../lib/context'; import { createDiscussion } from '../services/giscus/createDiscussion'; import { getToken } from '../services/giscus/token'; interface IWidgetProps { + origin: string; + session: string; repo: string; - term?: string; - number?: number; + term: string; + number: number; + category: string; repoId: string; categoryId: string; - description?: string; - reactionsEnabled?: boolean; -} - -function getSession(router: NextRouter) { - const session = router.query.session as string; - if (session) { - const query = { ...router.query }; - delete query.session; - const url = { pathname: router.pathname, query }; - const options = { scroll: false, shallow: true }; - router.replace(url, undefined, options); - } - return session || ''; + description: string; + reactionsEnabled: boolean; } export default function Widget({ + origin, + session, repo, term, number, + category, repoId, categoryId, description, - reactionsEnabled = true, + reactionsEnabled, }: IWidgetProps) { - const router = useRouter(); - const isMounted = useIsMounted(); const [token, setToken] = useState(''); const [isFetchingToken, setIsFetchingToken] = useState(false); - const session = getSession(router); - const origin = (router.query.origin as string) || (isMounted ? location.href : ''); - const handleDiscussionCreateRequest = async () => createDiscussion(repo, { repositoryId: repoId, @@ -70,19 +57,13 @@ export default function Widget({ .catch((err) => handleError(err?.message)); } - const ready = - router.isReady && (!session || token) && !isFetchingToken && repo && (term || number); + const ready = (!session || token) && !isFetchingToken && repo && (term || number); return ready ? ( - - + + + + ) : null; } diff --git a/lib/context.ts b/lib/context.ts index 9b7662405..489e9db85 100644 --- a/lib/context.ts +++ b/lib/context.ts @@ -1,17 +1,42 @@ import { createContext } from 'react'; -export const AuthContext = createContext({ - token: '', - origin: '', -}); +interface IAuthContext { + token: string; + origin: string; + getLoginUrl: (origin: string) => string; +} export function getLoginUrl(origin: string) { return `/api/oauth/authorize?redirect_uri=${encodeURIComponent(origin)}`; } -export const ThemeContext = createContext<{ +export const AuthContext = createContext({ + token: '', + origin: '', + getLoginUrl, +}); + +interface IThemeContext { theme: string; setTheme?: (theme: string) => void; -}>({ +} + +export const ThemeContext = createContext({ theme: '', }); + +interface IConfigContext { + repo: string; + term: string; + number: number; + category: string; + reactionsEnabled: boolean; +} + +export const ConfigContext = createContext({ + repo: '', + term: '', + number: 0, + category: '', + reactionsEnabled: true, +}); diff --git a/lib/types/common.ts b/lib/types/common.ts index 8a3dd750e..f5890520c 100644 --- a/lib/types/common.ts +++ b/lib/types/common.ts @@ -9,4 +9,5 @@ export interface DiscussionQuery { repo: string; term: string; number: number; + category: string; } diff --git a/pages/api/discussions/index.ts b/pages/api/discussions/index.ts index 84bc48cb5..576f0a690 100644 --- a/pages/api/discussions/index.ts +++ b/pages/api/discussions/index.ts @@ -11,6 +11,7 @@ async function get(req: NextApiRequest, res: NextApiResponse { - const { repo, term, number, ...pagination } = params; - const query = `repo:${repo} in:title ${term}`; + const { repo, term, number, category, ...pagination } = params; + const categoryQuery = category ? `category:${JSON.stringify(category)}` : ''; + const query = `repo:${repo} ${categoryQuery} in:title ${term}`; const gql = GET_DISCUSSION_QUERY(number ? 'number' : 'term'); return fetch(GITHUB_GRAPHQL_API_URL, {