From 27a4b5b2815a165c05e5d18f929e5c92e931321e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Jun 2025 07:16:32 +0000 Subject: [PATCH 1/5] Initial plan From b330e432f92a2b780ca770190d60081431ce9cb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Jun 2025 07:28:14 +0000 Subject: [PATCH 2/5] Add Stack Overflow link components and tests Co-authored-by: dwjohnston <2467377+dwjohnston@users.noreply.github.com> --- .../GithubPermalink/github-permalink.css | 91 +++++++++++++++++++ .../StackOverflowLink/StackOverflowLink.tsx | 33 +++++++ .../StackOverflowLinkBase.stories.tsx | 77 ++++++++++++++++ .../StackOverflowLinkBase.tsx | 91 +++++++++++++++++++ .../StackOverflowLinkRsc.tsx | 12 +++ .../StackOverflowSvg/StackOverflowSvg.tsx | 10 ++ src/library/config/BaseConfiguration.ts | 5 +- src/library/config/GithubPermalinkContext.tsx | 16 +++- .../config/GithubPermalinkRscConfig.ts | 7 +- src/library/config/defaultFunctions.ts | 38 +++++++- src/library/export.ts | 2 + src/library/rsc.ts | 3 +- src/library/utils/urlParsers.test.ts | 30 +++++- src/library/utils/urlParsers.ts | 12 +++ 14 files changed, 419 insertions(+), 8 deletions(-) create mode 100644 src/library/StackOverflowLink/StackOverflowLink.tsx create mode 100644 src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx create mode 100644 src/library/StackOverflowLink/StackOverflowLinkBase.tsx create mode 100644 src/library/StackOverflowLink/StackOverflowLinkRsc.tsx create mode 100644 src/library/StackOverflowSvg/StackOverflowSvg.tsx diff --git a/src/library/GithubPermalink/github-permalink.css b/src/library/GithubPermalink/github-permalink.css index 5229982..d5b4cb0 100644 --- a/src/library/GithubPermalink/github-permalink.css +++ b/src/library/GithubPermalink/github-permalink.css @@ -400,3 +400,94 @@ svg.github-logo{ margin: 0 2px; } } + +/* Stack Overflow link styles */ +.react-stackoverflow-link { + border: 1px solid var(--rgp-color-border); + border-radius: 8px; + background-color: var(--rgp-color-bg-stark); + color: var(--rgp-color-text-stark); + font-size: 14px; + font-family: "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; +} + +.react-stackoverflow-link a { + text-decoration: none; + color: inherit; +} + +.react-stackoverflow-link .header { + background-color: var(--rgp-color-bg-frame); + padding: 8px; +} + +.react-stackoverflow-link-header { + display: flex; + align-items: center; + gap: 8px; +} + +.react-stackoverflow-link-header p { + margin: 0; + font-weight: 600; + color: var(--rgp-color-text-frame); +} + +.react-stackoverflow-link-body { + padding: 8px; +} + +.react-stackoverflow-link-title { + font-weight: 600; + color: var(--rgp-color-text-stark); + display: block; + margin-bottom: 8px; +} + +.react-stackoverflow-link-stats { + display: flex; + gap: 16px; + margin-bottom: 8px; + font-size: 12px; +} + +.react-stackoverflow-link-stats .stat-label { + color: var(--rgp-color-text-frame); +} + +.react-stackoverflow-link-stats .stat-value { + font-weight: 600; + color: var(--rgp-color-text-stark); +} + +.react-stackoverflow-link-stats .answered-indicator { + color: #2f7d32; + font-weight: bold; +} + +.react-stackoverflow-link-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.react-stackoverflow-link-tags .tag { + background-color: #e8f4fd; + color: #39739d; + font-size: 11px; + padding: 2px 6px; + border-radius: 3px; + border: 1px solid #e8f4fd; +} + +@media (prefers-color-scheme: dark) { + .react-stackoverflow-link-tags .tag { + background-color: #2d4a5a; + color: #9cc3d5; + border-color: #2d4a5a; + } +} + +.react-stackoverflow-link-inline { + color: var(--rgp-color-reaction-foreground); +} diff --git a/src/library/StackOverflowLink/StackOverflowLink.tsx b/src/library/StackOverflowLink/StackOverflowLink.tsx new file mode 100644 index 0000000..872c4da --- /dev/null +++ b/src/library/StackOverflowLink/StackOverflowLink.tsx @@ -0,0 +1,33 @@ +"use client" + +import { useContext, useEffect, useState } from "react"; + +import { GithubPermalinkContext, StackOverflowLinkDataResponse } from "../config/GithubPermalinkContext"; +import { StackOverflowLinkBase, StackOverflowLinkBaseProps } from "./StackOverflowLinkBase"; + +type StackOverflowLinkProps = Omit; + +export function StackOverflowLink(props: StackOverflowLinkProps) { + + const { questionLink } = props; + const [data, setData] = useState(null as null | StackOverflowLinkDataResponse) + const { getStackOverflowFn, onError} = useContext(GithubPermalinkContext); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getStackOverflowFn(questionLink, onError).then((v) => { + setIsLoading(false); + setData(v); + }) + }, [getStackOverflowFn, questionLink, onError]) + + if (isLoading) { + return null; + } + if (!data) { + throw new Error("Loading is complete, but no data was returned.") + } + + return + +} diff --git a/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx b/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx new file mode 100644 index 0000000..0aac780 --- /dev/null +++ b/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx @@ -0,0 +1,77 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { StackOverflowLinkBase } from './StackOverflowLinkBase'; + +const meta: Meta = { + title: 'Example/StackOverflowLinkBase', + component: StackOverflowLinkBase, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['block', 'inline'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const mockSuccessData = { + questionTitle: 'How to create a React component?', + questionId: '123456', + isAnswered: true, + answerCount: 12, + score: 45, + viewCount: 1250, + tags: ['reactjs', 'javascript', 'components'], + creationDate: 1640995200, + status: 'ok' as const, +}; + +const mockErrorData = { + status: '404' as const, +}; + +export const Default: Story = { + args: { + questionLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', + data: mockSuccessData, + variant: 'block', + }, +}; + +export const Inline: Story = { + args: { + questionLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', + data: mockSuccessData, + variant: 'inline', + }, +}; + +export const NotFound: Story = { + args: { + questionLink: 'https://stackoverflow.com/questions/999999/non-existent-question', + data: mockErrorData, + variant: 'block', + }, +}; + +export const Unanswered: Story = { + args: { + questionLink: 'https://stackoverflow.com/questions/789012/unanswered-question', + data: { + ...mockSuccessData, + questionTitle: 'How to solve this complex problem?', + questionId: '789012', + isAnswered: false, + answerCount: 0, + score: 2, + viewCount: 34, + tags: ['algorithm', 'optimization'], + }, + variant: 'block', + }, +}; \ No newline at end of file diff --git a/src/library/StackOverflowLink/StackOverflowLinkBase.tsx b/src/library/StackOverflowLink/StackOverflowLinkBase.tsx new file mode 100644 index 0000000..f3f206c --- /dev/null +++ b/src/library/StackOverflowLink/StackOverflowLinkBase.tsx @@ -0,0 +1,91 @@ +import { PropsWithChildren } from "react"; +import { StackOverflowSvg } from "../StackOverflowSvg/StackOverflowSvg"; +import { StackOverflowLinkDataResponse } from "../config/GithubPermalinkContext"; +import { ErrorMessages } from "../ErrorMessages/ErrorMessages"; +import { Inline } from "../common/Inline/Inline"; + + +export type StackOverflowLinkBaseProps = { + className?: string; + questionLink: string; + data: StackOverflowLinkDataResponse; + variant?: "inline" | "block" +} + + + +export function StackOverflowLinkBase(props: StackOverflowLinkBaseProps) { + const { data, variant ="block", questionLink} = props; + + if (variant === "inline"){ + if(data.status === "ok"){ + return + } + else { + return + } + } + + if (data.status === "ok") { + return +
+ +

+ Stack Overflow +

+
+ +
+

{data.questionTitle}

+ +
+
+ Score: {data.score} +
+
+ Answers: {data.answerCount} + {data.isAnswered && } +
+
+ Views: {data.viewCount.toLocaleString()} +
+
+ + {data.tags && data.tags.length > 0 && ( +
+ {data.tags.map(tag => ( + {tag} + ))} +
+ )} +
+ }> +
+ + } + + return + + + +} + + +function StackOverflowLinkInner(props: PropsWithChildren<{ + header?: React.ReactNode +} & { + questionLink: string; + className?: string; +}>) { + + const {questionLink, className =''} = props; + + return
+ + + {props.children} + +
+} \ No newline at end of file diff --git a/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx b/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx new file mode 100644 index 0000000..0d744ef --- /dev/null +++ b/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx @@ -0,0 +1,12 @@ +import { githubPermalinkRscConfig } from "../config/GithubPermalinkRscConfig" +import { StackOverflowLinkBase, StackOverflowLinkBaseProps } from "./StackOverflowLinkBase"; + +export type StackOverflowLinkRscProps = Omit; + +export async function StackOverflowLinkRsc(props: StackOverflowLinkRscProps) { + const dataFn = githubPermalinkRscConfig.getStackOverflowFn(); + const onError = githubPermalinkRscConfig.getOnError(); + + const data = await dataFn(props.questionLink, onError); + return +} \ No newline at end of file diff --git a/src/library/StackOverflowSvg/StackOverflowSvg.tsx b/src/library/StackOverflowSvg/StackOverflowSvg.tsx new file mode 100644 index 0000000..d76a703 --- /dev/null +++ b/src/library/StackOverflowSvg/StackOverflowSvg.tsx @@ -0,0 +1,10 @@ +export function StackOverflowSvg(props: { + size?: number +}) { + + const size = props.size ?? 16; + + return +} \ No newline at end of file diff --git a/src/library/config/BaseConfiguration.ts b/src/library/config/BaseConfiguration.ts index f75651f..ba1d42d 100644 --- a/src/library/config/BaseConfiguration.ts +++ b/src/library/config/BaseConfiguration.ts @@ -1,5 +1,5 @@ import { defaultGetPermalinkFn } from "./defaultFunctions"; -import { defaultGetIssueFn } from "./defaultFunctions"; +import { defaultGetIssueFn, defaultGetStackOverflowFn } from "./defaultFunctions"; export type BaseConfiguration = { @@ -11,6 +11,9 @@ export type BaseConfiguration = { /** Function to provide issue data payload */ getIssueFn: typeof defaultGetIssueFn; + /** Function to provide Stack Overflow question data payload */ + getStackOverflowFn: typeof defaultGetStackOverflowFn; + /** * A github personal access token - will be passed to the data fetching functions */ diff --git a/src/library/config/GithubPermalinkContext.tsx b/src/library/config/GithubPermalinkContext.tsx index c9b7f57..41497ab 100644 --- a/src/library/config/GithubPermalinkContext.tsx +++ b/src/library/config/GithubPermalinkContext.tsx @@ -1,7 +1,7 @@ "use client" import { PropsWithChildren, createContext } from "react"; import { BaseConfiguration } from "./BaseConfiguration"; -import { defaultGetIssueFn } from "./defaultFunctions"; +import { defaultGetIssueFn, defaultGetStackOverflowFn } from "./defaultFunctions"; import { defaultGetPermalinkFn } from "./defaultFunctions"; // Thanks ChatGPT @@ -56,6 +56,18 @@ export type GithubIssueLinkDataResponse = { } } | ErrorResponses; +export type StackOverflowLinkDataResponse = { + questionTitle: string; + questionId: string; + isAnswered: boolean; + answerCount: number; + score: number; + viewCount: number; + tags: string[]; + creationDate: number; + status: "ok" +} | ErrorResponses; + @@ -63,12 +75,14 @@ export type GithubIssueLinkDataResponse = { export const GithubPermalinkContext = createContext({ getDataFn: defaultGetPermalinkFn, getIssueFn: defaultGetIssueFn, + getStackOverflowFn: defaultGetStackOverflowFn, }); export function GithubPermalinkProvider(props: PropsWithChildren>) { return diff --git a/src/library/config/GithubPermalinkRscConfig.ts b/src/library/config/GithubPermalinkRscConfig.ts index 0e66ded..377f8b4 100644 --- a/src/library/config/GithubPermalinkRscConfig.ts +++ b/src/library/config/GithubPermalinkRscConfig.ts @@ -1,9 +1,10 @@ import { BaseConfiguration } from "./BaseConfiguration"; -import { defaultGetIssueFn, defaultGetPermalinkFn } from "./defaultFunctions"; +import { defaultGetIssueFn, defaultGetPermalinkFn, defaultGetStackOverflowFn } from "./defaultFunctions"; const defaultConfiguration = { getDataFn: defaultGetPermalinkFn, getIssueFn: defaultGetIssueFn, + getStackOverflowFn: defaultGetStackOverflowFn, }; class GithubPermalinkRscConfig { @@ -23,6 +24,10 @@ class GithubPermalinkRscConfig { return this.baseConfiguration.getIssueFn; } + public getStackOverflowFn() { + return this.baseConfiguration.getStackOverflowFn; + } + public getGithubToken() { return this.baseConfiguration.githubToken; } diff --git a/src/library/config/defaultFunctions.ts b/src/library/config/defaultFunctions.ts index 63f54ad..441430c 100644 --- a/src/library/config/defaultFunctions.ts +++ b/src/library/config/defaultFunctions.ts @@ -1,5 +1,5 @@ -import { GithubIssueLinkDataResponse } from "./GithubPermalinkContext"; -import { parseGithubIssueLink, parseGithubPermalinkUrl } from "../utils/urlParsers"; +import { GithubIssueLinkDataResponse, StackOverflowLinkDataResponse } from "./GithubPermalinkContext"; +import { parseGithubIssueLink, parseGithubPermalinkUrl, parseStackOverflowLink } from "../utils/urlParsers"; import { GithubPermalinkDataResponse } from "./GithubPermalinkContext"; import { ErrorResponses } from "./GithubPermalinkContext"; @@ -97,3 +97,37 @@ export function handleResponse(response: Response): ErrorResponses { }; } +export async function defaultGetStackOverflowFn(questionLink: string, onError?: (err: unknown) => void): Promise { + const config = parseStackOverflowLink(questionLink); + + // Stack Exchange API doesn't require authentication but has rate limits + const apiUrl = `https://api.stackexchange.com/2.3/questions/${config.questionId}?site=stackoverflow&filter=withbody`; + + const questionResult = await fetch(apiUrl); + + if (!questionResult.ok) { + onError?.(questionResult); + return handleResponse(questionResult); + } + + const questionJson = await questionResult.json(); + + if (!questionJson.items || questionJson.items.length === 0) { + return { status: "404" }; + } + + const question = questionJson.items[0]; + + return { + questionTitle: question.title, + questionId: config.questionId, + isAnswered: question.is_answered, + answerCount: question.answer_count, + score: question.score, + viewCount: question.view_count, + tags: question.tags, + creationDate: question.creation_date, + status: "ok" + }; +} + diff --git a/src/library/export.ts b/src/library/export.ts index 31db22b..9d2c310 100644 --- a/src/library/export.ts +++ b/src/library/export.ts @@ -3,4 +3,6 @@ export * from "./GithubPermalink/GithubPermalink"; export * from "./GithubPermalink/GithubPermalinkBase"; export * from "./GithubIssueLink/GithubIssueLink"; export * from "./GithubIssueLink/GithubIssueLinkBase" +export * from "./StackOverflowLink/StackOverflowLink"; +export * from "./StackOverflowLink/StackOverflowLinkBase" export * from "./config/GithubPermalinkContext"; diff --git a/src/library/rsc.ts b/src/library/rsc.ts index ce35b3d..f0b5e91 100644 --- a/src/library/rsc.ts +++ b/src/library/rsc.ts @@ -1,3 +1,4 @@ export * from "./GithubPermalink/GithubPermalinkRsc"; export * from "./config/GithubPermalinkRscConfig"; -export * from "./GithubIssueLink/GithubIssueLinkRsc"; \ No newline at end of file +export * from "./GithubIssueLink/GithubIssueLinkRsc"; +export * from "./StackOverflowLink/StackOverflowLinkRsc"; \ No newline at end of file diff --git a/src/library/utils/urlParsers.test.ts b/src/library/utils/urlParsers.test.ts index b8376ba..50ae917 100644 --- a/src/library/utils/urlParsers.test.ts +++ b/src/library/utils/urlParsers.test.ts @@ -1,5 +1,5 @@ -import { expect, test, it, describe } from 'vitest' -import { parseGithubPermalinkUrl } from "./urlParsers"; +import { expect, it, describe } from 'vitest' +import { parseGithubPermalinkUrl, parseStackOverflowLink } from "./urlParsers"; describe(parseGithubPermalinkUrl, () => { it("behaves correctly for correct urls", () => { @@ -32,3 +32,29 @@ describe(parseGithubPermalinkUrl, () => { }) }) }); + +describe(parseStackOverflowLink, () => { + it("behaves correctly for basic stackoverflow urls", () => { + expect( + parseStackOverflowLink("https://stackoverflow.com/questions/123456/some-question-title") + ).toEqual({ + "questionId": "123456", + }); + }); + + it("behaves correctly for stackoverflow urls without title", () => { + expect( + parseStackOverflowLink("https://stackoverflow.com/questions/123456") + ).toEqual({ + "questionId": "123456", + }); + }); + + it("behaves correctly for stackoverflow urls with query parameters", () => { + expect( + parseStackOverflowLink("https://stackoverflow.com/questions/123456/some-title?noredirect=1&lq=1") + ).toEqual({ + "questionId": "123456", + }); + }); +}); diff --git a/src/library/utils/urlParsers.ts b/src/library/utils/urlParsers.ts index 34f203b..dee1a9e 100644 --- a/src/library/utils/urlParsers.ts +++ b/src/library/utils/urlParsers.ts @@ -36,3 +36,15 @@ export function parseGithubIssueLink(url: string): { owner: string, repo: string throw new Error("Invalid issue link URL"); } } + +export function parseStackOverflowLink(url: string): { questionId: string } { + const regex = /^https?:\/\/stackoverflow\.com\/questions\/(\d+)(?:\/[^?]*)?(?:\?.*)?$/; + const match = url.match(regex); + + if (match) { + const [, questionId] = match; + return { questionId }; + } else { + throw new Error("Invalid Stack Overflow question URL"); + } +} From af68ae1e725674f88fef37a80ad08f83c52911e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Jun 2025 07:32:25 +0000 Subject: [PATCH 3/5] Complete Stack Overflow link implementation with documentation Co-authored-by: dwjohnston <2467377+dwjohnston@users.noreply.github.com> --- README.md | 29 +++++++++++++++++-- .../StackOverflowLinkBase.tsx | 13 +++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fb1b2b5..37de3ab 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Display Github permalinks as codeblocks. Display Github issue links. +Display Stack Overflow question links. + ![screenshot of the tool in action - dark mode ](./screenshot-permalink-dark.png) ![screenshot of the tool in action - light mode ](./screenshot-permalink-light.png) ![screenshot of the tool in action - dark mode ](./screenshot-issuelink-dark.png) @@ -27,9 +29,9 @@ This package is compatible with Next 13+ and the components can be used as RSCs Three variants of each component are exported - - GithubPermalink/GithubIssueLink - Client component - It fetches the data as on the client in a useEffect. ie. Data won't be retrieved until application has loaded in user's browser. - - GithubPermalinkBase/GithubIssueLinkBase - this is the base component - it does no data fetching on its own. - - GithubPermalinkRsc/GithubIssueLinkRsc - This is an RSC. + - GithubPermalink/GithubIssueLink/StackOverflowLink - Client component - It fetches the data as on the client in a useEffect. ie. Data won't be retrieved until application has loaded in user's browser. + - GithubPermalinkBase/GithubIssueLinkBase/StackOverflowLinkBase - this is the base component - it does no data fetching on its own. + - GithubPermalinkRsc/GithubIssueLinkRsc/StackOverflowLinkRsc - This is an RSC. @@ -112,6 +114,26 @@ export function MyApp() { } ``` +## Stack Overflow Links + +### Usage +```jsx +import { StackOverflowLink } from 'react-github-permalink'; +import "react-github-permalink/dist/github-permalink.css"; // Or provide your own styles + +export function MyApp() { + return +} +``` + +StackOverflowLink also has an inline variant: + +```jsx +export function MyApp() { + return +} +``` + ## Rate Limits and Authentication By default the components make unauthenticated requests against Github's API. The rate limit for such requests is 60/hour and only publicly visible repositories are available. @@ -126,6 +148,7 @@ The global configuration object has this signature type BaseConfiguration = { getDataFn: (permalink: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise; getIssueFn: (issueLink: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise; + getStackOverflowFn: (questionLink: string, onError?: ((err: unknown) => void) | undefined) => Promise; githubToken: string | undefined; onError: ((e: unknown) => void) | undefined; } diff --git a/src/library/StackOverflowLink/StackOverflowLinkBase.tsx b/src/library/StackOverflowLink/StackOverflowLinkBase.tsx index f3f206c..7411035 100644 --- a/src/library/StackOverflowLink/StackOverflowLinkBase.tsx +++ b/src/library/StackOverflowLink/StackOverflowLinkBase.tsx @@ -2,7 +2,6 @@ import { PropsWithChildren } from "react"; import { StackOverflowSvg } from "../StackOverflowSvg/StackOverflowSvg"; import { StackOverflowLinkDataResponse } from "../config/GithubPermalinkContext"; import { ErrorMessages } from "../ErrorMessages/ErrorMessages"; -import { Inline } from "../common/Inline/Inline"; export type StackOverflowLinkBaseProps = { @@ -12,17 +11,25 @@ export type StackOverflowLinkBaseProps = { variant?: "inline" | "block" } +function StackOverflowInline(props: { + href: string; + text: string; +}) { + const {href, text} = props; +return + {text} +} export function StackOverflowLinkBase(props: StackOverflowLinkBaseProps) { const { data, variant ="block", questionLink} = props; if (variant === "inline"){ if(data.status === "ok"){ - return + return } else { - return + return } } From 23a0947a13d913fc75289d350255435a04905778 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 27 Jun 2025 16:24:42 +1000 Subject: [PATCH 4/5] Tidy up stackoverflow link pr --- .../GithubIssueLink/GithubIssueLinkBase.tsx | 92 +++-- .../GithubPermalink/GithubPermalinkBase.tsx | 16 +- .../GithubPermalink/github-permalink.css | 381 +++++++++--------- .../StackOverflowLink/StackOverflowLink.tsx | 10 +- .../StackOverflowLinkBase.stories.tsx | 27 +- .../StackOverflowLinkBase.tsx | 139 ++++--- .../StackOverflowLinkRsc.tsx | 4 +- .../StackOverflowSvg/StackOverflowSvg.tsx | 10 - src/library/common/Inline/Inline.tsx | 12 +- .../{ => images}/GithubSvg/GithubSvg.tsx | 0 .../StackOverflowSvg/StackOverflowSvg.tsx | 14 + 11 files changed, 385 insertions(+), 320 deletions(-) delete mode 100644 src/library/StackOverflowSvg/StackOverflowSvg.tsx rename src/library/{ => images}/GithubSvg/GithubSvg.tsx (100%) create mode 100644 src/library/images/StackOverflowSvg/StackOverflowSvg.tsx diff --git a/src/library/GithubIssueLink/GithubIssueLinkBase.tsx b/src/library/GithubIssueLink/GithubIssueLinkBase.tsx index a6480b6..3cb5eda 100644 --- a/src/library/GithubIssueLink/GithubIssueLinkBase.tsx +++ b/src/library/GithubIssueLink/GithubIssueLinkBase.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from "react"; -import { GithubSvg } from "../GithubSvg/GithubSvg"; +import { GithubSvg } from "../images/GithubSvg/GithubSvg"; import { GithubIssueLinkDataResponse } from "../config/GithubPermalinkContext"; import { ErrorMessages } from "../ErrorMessages/ErrorMessages"; import { Reactions } from "../common/Reactions/Reactions"; @@ -17,60 +17,66 @@ export type GithubIssueLinkBaseProps = { export function GithubIssueLinkBase(props: GithubIssueLinkBaseProps) { - const { data, variant ="block", issueLink} = props; - - if (variant === "inline"){ - if(data.status === "ok"){ - return - } - else { - return - } - } + const { data, variant = "block", issueLink } = props; + if (variant === "inline") { if (data.status === "ok") { - return -
- -

- {data.owner}/{data.repo} -

-
- -
-

{data.issueTitle} #{data.issueNumber}

- - {data.issueState === "open" ?
Open
:
Closed
} - - -
-
- {data.reactions && } -
- }> -
- - } - - return - - + return + } + else { + return + } + } + + if (data.status === "ok") { + return +
+ +

+ {data.owner}/{data.repo} +

+
+ +
+

{data.issueTitle} #{data.issueNumber}

+ + {data.issueState === "open" ?
Open
:
Closed
} + + +
+
+ {data.reactions && } +
+ }> +
+ + } + + + return + + } -function GithubIssueLinkInner(props: PropsWithChildren<{ +/** + * This thing is displayed regardless of whether it has errored or not. + * @param props + * @returns + */ +function GithubIssueLinkFrame(props: PropsWithChildren<{ header?: React.ReactNode } & { issueLink: string; className?: string; }>) { - const {issueLink, className =''} = props; + const { issueLink, className = '' } = props; return
diff --git a/src/library/GithubPermalink/GithubPermalinkBase.tsx b/src/library/GithubPermalink/GithubPermalinkBase.tsx index 2529e0a..0243fb3 100644 --- a/src/library/GithubPermalink/GithubPermalinkBase.tsx +++ b/src/library/GithubPermalink/GithubPermalinkBase.tsx @@ -1,6 +1,6 @@ import { GithubPermalinkDataResponse, } from "../config/GithubPermalinkContext"; import { ErrorMessages } from "../ErrorMessages/ErrorMessages"; -import { GithubSvg } from "../GithubSvg/GithubSvg"; +import { GithubSvg } from "../images/GithubSvg/GithubSvg"; import { PropsWithChildren } from "react"; import { SyntaxHighlight } from "../SyntaxHighlight/SyntaxHighlight"; import { formatForLineExclusions } from "./formatLineExclusions"; @@ -34,32 +34,32 @@ export function GithubPermalinkBase(props: GithubPermalinkBaseProps) { return acc + "\n" + cur.lines.join("\n"); }, '') - return + return {`${data.owner}/${data.repo}/${data.path}`}

{data.lineFrom === data.lineTo ? <>Line {data.lineFrom} : <>Lines {data.lineFrom} to {data.lineTo}} in {data.commit.slice(0, 7)}

}> {formatedLineExclusions.map((v) => { if (v.isExclude) { - return + return } - return + return })} - + } - return + return - + } -function GithubPermalinkInner(props: PropsWithChildren<{ +function GithubPermalinkFrame(props: PropsWithChildren<{ header?: React.ReactNode clipboard?: string; } & GithubPermalinkBaseProps>) { diff --git a/src/library/GithubPermalink/github-permalink.css b/src/library/GithubPermalink/github-permalink.css index d5b4cb0..6dce3d6 100644 --- a/src/library/GithubPermalink/github-permalink.css +++ b/src/library/GithubPermalink/github-permalink.css @@ -1,25 +1,23 @@ - - .rgp-base { - --rgp-color-bg-stark: white; + --rgp-color-bg-stark: white; --rgp-color-text-stark: rgb(31, 35, 40); - + --rgp-color-bg-frame: rgb(246, 248, 250); --rgp-color-text-frame: rgb(101, 109, 118); - - --rgp-color-border: rgb(208, 215, 222); + + --rgp-color-border: rgb(208, 215, 222); --rgp-color-issuenumber: rgb(101, 109, 118); - --rgp-color-file-link: rgb(9, 105, 218); + --rgp-color-file-link: rgb(9, 105, 218); --rgp-color-commit-link: rgb(31, 35, 40); - --rgp-color-status-foreground: white; + --rgp-color-status-foreground: white; --rgp-color-status-open: rgb(31, 136, 61); --rgp-color-status-closed: rgb(130, 80, 223); --rgp-color-status-error: red; - --rgp-color-reaction-foreground: #0969da; + --rgp-color-reaction-foreground: #0969da; --rgp-color-reaction-background: #ddf4ff; --rgp-color-button-text: #59636e; @@ -28,15 +26,18 @@ --rgp-color-button-background-hover: #eff2f5; --rgp-color-button-border: #d1d9e0; --rgp-color-tooltip-bg: #3d444d; - ; - - - color: var(--rgp-color-text-stark); + --rgp-color-stackoverflow-blue: #2b75d0; + --rgp-color-stackoverflow-answer-green: rgb(24, 134, 75); + --rgp-color-stackoverflow-orange: #e7700d; + --rgp-color-stackoverflow-bg: #ffffff; + --rgp-color-stackoverflow-border: #d5d8db; + --rgp-color-stackoverflow-tag-bg: #f0f1f2; + --rgp-color-stackoverflow-tag-text: #3b4045; } @@ -44,43 +45,48 @@ .rgp-base { - --rgp-color-bg-stark: rgb(29, 31, 33); - --rgp-color-text-stark: rgb(230, 237, 243); + --rgp-color-bg-stark: rgb(29, 31, 33); + --rgp-color-text-stark: rgb(230, 237, 243); - - --rgp-color-bg-frame: rgb(22, 27, 34); - --rgp-color-text-frame: rgb(141, 150, 160); - - --rgp-color-border: rgb(48, 54, 61); - - --rgp-color-issuenumber: rgb(101, 109, 118); - --rgp-color-file-link: rgb(68, 147, 248); - --rgp-color-commit-link: rgb(230, 237, 243); - --rgp-color-status-foreground: white; - --rgp-color-status-open: #238636; - --rgp-color-status-closed: rgb(130, 80, 223); - --rgp-color-status-error: red; + --rgp-color-bg-frame: rgb(22, 27, 34); + --rgp-color-text-frame: rgb(141, 150, 160); + + --rgp-color-border: rgb(48, 54, 61); + + --rgp-color-issuenumber: rgb(101, 109, 118); + --rgp-color-file-link: rgb(68, 147, 248); + --rgp-color-commit-link: rgb(230, 237, 243); + + --rgp-color-status-foreground: white; + --rgp-color-status-open: #238636; + --rgp-color-status-closed: rgb(130, 80, 223); + --rgp-color-status-error: red; - --rgp-color-reaction-foreground: #4493f8; - --rgp-color-reaction-background: #388bfd33; + --rgp-color-reaction-foreground: #4493f8; + --rgp-color-reaction-background: #388bfd33; - - --rgp-color-button-text: #9198a1; - --rgp-color-button-background: #212830; - --rgp-color-button-background-hover: #262c36; - --rgp-color-button-border: #3d444d; + --rgp-color-button-text: #9198a1; + --rgp-color-button-background: #212830; + --rgp-color-button-background-hover: #262c36; - + --rgp-color-button-border: #3d444d; + --rgp-color-stackoverflow-blue: #89bbec; + + + --rgp-color-stackoverflow-border: #484c4f; + --rgp-color-stackoverflow-bg: #242526; + --rgp-color-stackoverflow-tag-bg: #333638; + --rgp-color-stackoverflow-tag-text: #dbdddf; } } .react-github-issue-link-inline { - color: var(--rgp-color-reaction-forergound); - border: solid 1px var(--rgp-color-text-frame); + color: var(--rgp-color-reaction-forergound); + border: solid 1px var(--rgp-color-text-frame); border-radius: 2px; - background-color: var(--rgp-color-bg-stark); + background-color: var(--rgp-color-bg-stark); font-family: monospace; padding: 0 0.25em; margin: 0 0.25em; @@ -88,28 +94,28 @@ .github-logo { - position: relative; + position: relative; top: 0.1em; - width: 0.9em; - height: 0.9em; + width: 0.9em; + height: 0.9em; } } .rgp-base { - pre + .hide-line-numbers { + pre+.hide-line-numbers { margin-top: -1em; } - .hide-line-numbers + pre { + .hide-line-numbers+pre { margin-top: 0em; } .hide-line-numbers { - margin:0; + margin: 0; } - + .hide-line-numbers .linenumber { visibility: hidden; } @@ -119,19 +125,20 @@ -svg.github-logo{ +svg.github-logo { fill: var(--rgp-color-commit-link); } .react-github-issuelink .react-github-issuelink-body { font-size: 16px; color: var(--rgp-color-text-stark); - display: flex; - flex-flow: row nowrap; + display: flex; + flex-flow: row nowrap; justify-content: space-between; align-items: center; text-decoration: none; } + .react-github-issuelink-body p { margin: 0; } @@ -143,7 +150,7 @@ svg.github-logo{ } .react-github-issuelink a:hover .react-github-issuelink-title, - .react-github-issuelink a:hover .react-github-issuelink-number { +.react-github-issuelink a:hover .react-github-issuelink-number { text-decoration: underline; } @@ -153,7 +160,7 @@ svg.github-logo{ } .react-github-issuelink-repo { - font-size: 12px; + font-size: 12px; font-weight: bold; display: flex; flex-flow: row nowrap; @@ -162,18 +169,18 @@ svg.github-logo{ text-decoration: underline; } -.react-github-issuelink-status { - color: var(--rgp-color-status-foreground); - fill: var(--rgp-color-status-foreground); +.react-github-issuelink-status { + color: var(--rgp-color-status-foreground); + fill: var(--rgp-color-status-foreground); font-size: 0.75em; - padding: 0 10px; - border-radius: 2em; + padding: 0 10px; + border-radius: 2em; font-weight: 500; - line-height: 24px; + line-height: 24px; - display: flex; - flex-flow: row nowrap; - align-items: center; + display: flex; + flex-flow: row nowrap; + align-items: center; justify-content: space-between; gap: 0.6em; @@ -191,26 +198,28 @@ svg.github-logo{ padding: 0.5em; } -.react-github-permalink, .react-github-issuelink { +.react-github-permalink, +.react-github-issuelink { border: solid 1px var(--rgp-color-border); margin: 0.5em; font-size: 12px; - border-radius: 4px; + border-radius: 4px; text-align: left; background-color: var(--rgp-color-bg-stark); } -.react-github-permalink, .react-github-permalink .header { +.react-github-permalink, +.react-github-permalink .header { border-color: var(--rgp-color-border); - border-style: solid; - border-width: 1px; + border-style: solid; + border-width: 1px; } .react-github-permalink .header { - display: flex; + display: flex; flex-flow: row nowrap; gap: 0.75em; align-items: center; @@ -218,113 +227,116 @@ svg.github-logo{ .copy-button-container { - .tooltip-container{ - position: relative; - anchor-name: --tooltip-anchor; + .tooltip-container { + position: relative; + anchor-name: --tooltip-anchor; + } + + .tooltip-content { + + position: absolute; + top: -14px; + left: -26px; + background-color: var(--rgp-color-tooltip-bg); + + transition-property: opacity; + transition-duration: 0.25s; + opacity: 1; + + @starting-style { + opacity: 0; + } + + @supports(position-anchor: --tooltip-anchor) { + position: fixed; + position-anchor: --tooltip-anchor; + top: unset; + left: unset; + bottom: anchor(top); + justify-self: anchor-center; + margin: 3px; + } - .tooltip-content { - position: absolute; - top: -14px; - left: -26px; - background-color: var(--rgp-color-tooltip-bg); + height: 12px; - transition-property: opacity; - transition-duration: 0.25s; - opacity: 1; - @starting-style { - opacity: 0; - } + white-space: nowrap; + border-radius: 2px; - @supports(position-anchor: --tooltip-anchor){ - position: fixed; - position-anchor: --tooltip-anchor; - top:unset; - left: unset; - bottom: anchor(top); - justify-self: anchor-center; - margin: 3px; + color: var(--rgp-color-text-stark); + box-shadow: 2px 2px 2px var(--rgp-color-bg-stark); - } + font-size: 11px; + padding: 0.5em 0.75em; + } + .tooltip-target[popover] { + overlay: none !important; + display: none; + } - height: 12px; - - - white-space: nowrap; - border-radius: 2px; + .tooltip-target[popover]:popover-open+.tooltip-content { + display: block; + } - color: var(--rgp-color-text-stark); - box-shadow: 2px 2px 2px var(--rgp-color-bg-stark); + .tooltip-target[popover]+.tooltip-content { + display: none; + } - font-size: 11px; - padding: 0.5em 0.75em; - } - .tooltip-target[popover] { - overlay: none !important; - display: none; + + button { + all: unset; + /* Remove default styling */ + appearance: none; + /* Remove specific styling across different browsers */ + border: none; + /* Remove any border */ + background: none; + /* Remove background */ + padding: 0; + /* Remove default padding */ + margin: 0; + /* Remove default margin */ + box-shadow: none; + /* Remove box shadow */ + text-align: inherit; + /* Inherit text alignment from parent */ + font: inherit; + /* Use parent's font settings */ + color: inherit; + /* Use parent's text color */ + cursor: pointer; + /* Set pointer cursor for better UX */ + background-color: var(--rgp-color-button-background); + border: solid 1px var(--rgp-color-button-border); + padding: 0.5em; + border-radius: 0.25em; + + + color: var(--rgp-color-button-text); + + &.clicked { + color: var(--rgp-color-button-text-success); } - .tooltip-target[popover]:popover-open + .tooltip-content { - display: block; + svg { + fill: currentColor; } - .tooltip-target[popover] + .tooltip-content { - display: none; + &:hover { + background-color: var(--rgp-color-button-background-hover); } + } +} - button { - all: unset; - /* Remove default styling */ - appearance: none; - /* Remove specific styling across different browsers */ - border: none; - /* Remove any border */ - background: none; - /* Remove background */ - padding: 0; - /* Remove default padding */ - margin: 0; - /* Remove default margin */ - box-shadow: none; - /* Remove box shadow */ - text-align: inherit; - /* Inherit text alignment from parent */ - font: inherit; - /* Use parent's font settings */ - color: inherit; - /* Use parent's text color */ - cursor: pointer; - /* Set pointer cursor for better UX */ - background-color: var(--rgp-color-button-background); - border: solid 1px var(--rgp-color-button-border); - padding: 0.5em; - border-radius: 0.25em; - - - color: var(--rgp-color-button-text); - - &.clicked { - color: var(--rgp-color-button-text-success); - } - svg { - fill: currentColor; - } - &:hover { - background-color: var(--rgp-color-button-background-hover); - } - - } - -} - -.react-github-permalink .header, .react-github-issuelink .header{ - border-width: 0 0 1px 0; +.react-github-permalink .header, +.react-github-issuelink .header { + border-width: 0 0 1px 0; font-family: "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; } @@ -337,7 +349,7 @@ svg.github-logo{ } .react-github-permalink a.commit-link { - color: var(--rgp-color-commit-link); + color: var(--rgp-color-commit-link); text-decoration: underline; } @@ -351,10 +363,10 @@ svg.github-logo{ .react-github-permalink .header { background-color: var(--rgp-color-bg-frame); - padding: 8px; + padding: 8px; .link-wrapper { - min-width: 0; + min-width: 0; overflow: hidden; flex: 1 1 auto; @@ -369,28 +381,29 @@ svg.github-logo{ */ text-overflow: ellipsis; overflow: hidden; - display: block; + display: block; } } } -.react-github-permalink .error, .react-github-issuelink .error { - font-weight: bold; +.react-github-permalink .error, +.react-github-issuelink .error { + font-weight: bold; color: var(--rgp-color-status-error); margin-left: 1em; } .reaction-bar { - font-size: 0.8em; + font-size: 0.8em; margin-top: 0.5em; } .reaction-value { - color: var(--rgp-color-reaction-foreground); - background-color: var(--rgp-color-reaction-background); - border: solid 1px var(--rgp-color-reaction-foreground); + color: var(--rgp-color-reaction-foreground); + background-color: var(--rgp-color-reaction-background); + border: solid 1px var(--rgp-color-reaction-foreground); border-radius: 100px; display: inline-block; margin-right: 0.75em; @@ -409,6 +422,21 @@ svg.github-logo{ color: var(--rgp-color-text-stark); font-size: 14px; font-family: "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; + overflow: hidden; +} + +.react-stackoverflow-inline-vote-count { + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + font-size: 12px; + color: var(--rgp-color-stackoverflow-answer-green); + border: solid 1px var(--rgp-color-stackoverflow-answer-green); + + &.answered { + background-color: var(--rgp-color-stackoverflow-answer-green); + color: white; + } } .react-stackoverflow-link a { @@ -417,7 +445,6 @@ svg.github-logo{ } .react-stackoverflow-link .header { - background-color: var(--rgp-color-bg-frame); padding: 8px; } @@ -439,9 +466,8 @@ svg.github-logo{ .react-stackoverflow-link-title { font-weight: 600; - color: var(--rgp-color-text-stark); + color: var(--rgp-color-stackoverflow-blue); display: block; - margin-bottom: 8px; } .react-stackoverflow-link-stats { @@ -461,7 +487,7 @@ svg.github-logo{ } .react-stackoverflow-link-stats .answered-indicator { - color: #2f7d32; + color: var(--rgp-color-stackoverflow-answer-green); font-weight: bold; } @@ -472,22 +498,17 @@ svg.github-logo{ } .react-stackoverflow-link-tags .tag { - background-color: #e8f4fd; - color: #39739d; + background-color: var(--rgp-color-stackoverflow-tag-bg); + color: var(--rgp-color-stackoverflow-tag-text); font-size: 11px; padding: 2px 6px; border-radius: 3px; - border: 1px solid #e8f4fd; -} - -@media (prefers-color-scheme: dark) { - .react-stackoverflow-link-tags .tag { - background-color: #2d4a5a; - color: #9cc3d5; - border-color: #2d4a5a; - } } .react-stackoverflow-link-inline { - color: var(--rgp-color-reaction-foreground); -} + font-family: "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--rgp-color-stackoverflow-blue); +} \ No newline at end of file diff --git a/src/library/StackOverflowLink/StackOverflowLink.tsx b/src/library/StackOverflowLink/StackOverflowLink.tsx index 872c4da..0cccd5f 100644 --- a/src/library/StackOverflowLink/StackOverflowLink.tsx +++ b/src/library/StackOverflowLink/StackOverflowLink.tsx @@ -1,17 +1,17 @@ "use client" -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; -import { GithubPermalinkContext, StackOverflowLinkDataResponse } from "../config/GithubPermalinkContext"; +import { GithubPermalinkContext, StackOverflowLinkDataResponse } from "../config/GithubPermalinkContext"; import { StackOverflowLinkBase, StackOverflowLinkBaseProps } from "./StackOverflowLinkBase"; type StackOverflowLinkProps = Omit; export function StackOverflowLink(props: StackOverflowLinkProps) { - const { questionLink } = props; + const { stackoverflowLink: questionLink } = props; const [data, setData] = useState(null as null | StackOverflowLinkDataResponse) - const { getStackOverflowFn, onError} = useContext(GithubPermalinkContext); + const { getStackOverflowFn, onError } = useContext(GithubPermalinkContext); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -28,6 +28,6 @@ export function StackOverflowLink(props: StackOverflowLinkProps) { throw new Error("Loading is complete, but no data was returned.") } - return + return } diff --git a/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx b/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx index 0aac780..c89b986 100644 --- a/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx +++ b/src/library/StackOverflowLink/StackOverflowLinkBase.stories.tsx @@ -37,23 +37,42 @@ const mockErrorData = { export const Default: Story = { args: { - questionLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', + stackoverflowLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', data: mockSuccessData, variant: 'block', }, }; +export const WithTags: Story = { + args: { + stackoverflowLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', + data: mockSuccessData, + variant: 'block', + showTags: true + }, +}; + + export const Inline: Story = { args: { - questionLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', + stackoverflowLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', data: mockSuccessData, variant: 'inline', }, }; +export const InlineError: Story = { + args: { + stackoverflowLink: 'https://stackoverflow.com/questions/123456/how-to-create-react-component', + data: mockErrorData, + variant: 'inline', + }, +}; + + export const NotFound: Story = { args: { - questionLink: 'https://stackoverflow.com/questions/999999/non-existent-question', + stackoverflowLink: 'https://stackoverflow.com/questions/999999/non-existent-question', data: mockErrorData, variant: 'block', }, @@ -61,7 +80,7 @@ export const NotFound: Story = { export const Unanswered: Story = { args: { - questionLink: 'https://stackoverflow.com/questions/789012/unanswered-question', + stackoverflowLink: 'https://stackoverflow.com/questions/789012/unanswered-question', data: { ...mockSuccessData, questionTitle: 'How to solve this complex problem?', diff --git a/src/library/StackOverflowLink/StackOverflowLinkBase.tsx b/src/library/StackOverflowLink/StackOverflowLinkBase.tsx index 7411035..fe033f8 100644 --- a/src/library/StackOverflowLink/StackOverflowLinkBase.tsx +++ b/src/library/StackOverflowLink/StackOverflowLinkBase.tsx @@ -1,96 +1,111 @@ -import { PropsWithChildren } from "react"; -import { StackOverflowSvg } from "../StackOverflowSvg/StackOverflowSvg"; +import { PropsWithChildren, ReactNode } from "react"; +import { StackOverflowSvg } from "../images/StackOverflowSvg/StackOverflowSvg"; import { StackOverflowLinkDataResponse } from "../config/GithubPermalinkContext"; import { ErrorMessages } from "../ErrorMessages/ErrorMessages"; export type StackOverflowLinkBaseProps = { className?: string; - questionLink: string; + stackoverflowLink: string; data: StackOverflowLinkDataResponse; - variant?: "inline" | "block" + variant?: "inline" | "block", + showTags?: boolean; } function StackOverflowInline(props: { - href: string; - text: string; + href: string; + text: string; +} | { + href: string; + data: StackOverflowLinkDataResponse & { status: "ok" } }) { + const { href } = props; - const {href, text} = props; -return - {text} + + let textToDisplay = null; + let startAdornment: null | ReactNode = null; + + if ("data" in props) { + textToDisplay = props.data.questionTitle; + startAdornment =
{props.data.score} + } + else { + textToDisplay = props.text; + } + + return + {startAdornment} {textToDisplay} + } export function StackOverflowLinkBase(props: StackOverflowLinkBaseProps) { - const { data, variant ="block", questionLink} = props; - - if (variant === "inline"){ - if(data.status === "ok"){ - return - } - else { - return - } - } + const { data, variant = "block", stackoverflowLink, showTags } = props; + if (variant === "inline") { if (data.status === "ok") { - return -
- -

- Stack Overflow -

+ return + } + else { + return + } + } + + if (data.status === "ok") { + return +
+ + {data.questionTitle} +
+ }> + +
+
+
+ Score: {data.score} +
+
+ Answers: {data.answerCount} + {data.isAnswered && }
- -
-

{data.questionTitle}

- -
-
- Score: {data.score} -
-
- Answers: {data.answerCount} - {data.isAnswered && } -
-
- Views: {data.viewCount.toLocaleString()} -
-
- - {data.tags && data.tags.length > 0 && ( -
- {data.tags.map(tag => ( - {tag} - ))} -
- )} +
+ + {showTags && data.tags && data.tags.length > 0 && ( +
+ {data.tags.map(tag => ( + {tag} + ))}
- }> - - - } - - return - - + )} +
+ + + } + + return + + {props.stackoverflowLink} +
+ }> + +
} -function StackOverflowLinkInner(props: PropsWithChildren<{ +function StackOverflowLinkFrame(props: PropsWithChildren<{ header?: React.ReactNode } & { - questionLink: string; + stackoverflowLink: string; className?: string; }>) { - const {questionLink, className =''} = props; + const { stackoverflowLink, className = '' } = props; return
- + {props.children} diff --git a/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx b/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx index 0d744ef..0bc7f66 100644 --- a/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx +++ b/src/library/StackOverflowLink/StackOverflowLinkRsc.tsx @@ -7,6 +7,6 @@ export async function StackOverflowLinkRsc(props: StackOverflowLinkRscProps) { const dataFn = githubPermalinkRscConfig.getStackOverflowFn(); const onError = githubPermalinkRscConfig.getOnError(); - const data = await dataFn(props.questionLink, onError); - return + const data = await dataFn(props.stackoverflowLink, onError); + return } \ No newline at end of file diff --git a/src/library/StackOverflowSvg/StackOverflowSvg.tsx b/src/library/StackOverflowSvg/StackOverflowSvg.tsx deleted file mode 100644 index d76a703..0000000 --- a/src/library/StackOverflowSvg/StackOverflowSvg.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export function StackOverflowSvg(props: { - size?: number -}) { - - const size = props.size ?? 16; - - return -} \ No newline at end of file diff --git a/src/library/common/Inline/Inline.tsx b/src/library/common/Inline/Inline.tsx index 91eb405..3d95e19 100644 --- a/src/library/common/Inline/Inline.tsx +++ b/src/library/common/Inline/Inline.tsx @@ -1,11 +1,11 @@ -import { GithubSvg } from "../../GithubSvg/GithubSvg"; +import { GithubSvg } from "../../images/GithubSvg/GithubSvg"; export function Inline(props: { - href: string; - text: string; + href: string; + text: string; }) { - const {href, text} = props; -return - {text} + const { href, text } = props; + return + {text} } \ No newline at end of file diff --git a/src/library/GithubSvg/GithubSvg.tsx b/src/library/images/GithubSvg/GithubSvg.tsx similarity index 100% rename from src/library/GithubSvg/GithubSvg.tsx rename to src/library/images/GithubSvg/GithubSvg.tsx diff --git a/src/library/images/StackOverflowSvg/StackOverflowSvg.tsx b/src/library/images/StackOverflowSvg/StackOverflowSvg.tsx new file mode 100644 index 0000000..e1681de --- /dev/null +++ b/src/library/images/StackOverflowSvg/StackOverflowSvg.tsx @@ -0,0 +1,14 @@ +export function StackOverflowSvg(props: { + size?: number +}) { + + const size = props.size ?? 16; + + return + + + + + + +} \ No newline at end of file From 8306d5edccd2d453f676fa12ad29b46b05a40313 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Fri, 27 Jun 2025 16:26:15 +1000 Subject: [PATCH 5/5] Create slimy-plums-drop.md --- .changeset/slimy-plums-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slimy-plums-drop.md diff --git a/.changeset/slimy-plums-drop.md b/.changeset/slimy-plums-drop.md new file mode 100644 index 0000000..f66cb19 --- /dev/null +++ b/.changeset/slimy-plums-drop.md @@ -0,0 +1,5 @@ +--- +"react-github-permalink": minor +--- + +Add Stack Overflow link components