Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 77 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,74 @@ Display Github permalinks as codeblocks.

Display Github issue links.

Display browser compatibility tables from caniuse.com.

![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)
![screenshot of the tool in action - light mode ](./screenshot-issuelink-light.png)
![screenshot of the tool in action - dark mode - inline ](./screenshot-issuelink-inline-dark.png)
![screenshot of the tool in action - light mode - inline ](./screenshot-issuelink-inline-light.png)

## CaniuseLink Component

The CaniuseLink component displays live browser compatibility tables for web features using data from caniuse.com. It shows support across different browsers and versions with color-coded indicators.

### Basic Usage

```jsx
import { CaniuseLink } from 'react-github-permalink';
import "react-github-permalink/dist/github-permalink.css";

export function MyApp() {
return <CaniuseLink feature="flexbox" />
}
```

### Inline Usage

```jsx
<p>This layout uses <CaniuseLink feature="flexbox" variant="inline" /> for positioning.</p>
```

### With Custom Data

```jsx
import { CaniuseLinkBase } from 'react-github-permalink';

const customData = {
title: "CSS Flexible Box Layout Module",
description: "Method of positioning elements...",
stats: {
chrome: { "89": "y", "90": "y" },
firefox: { "84": "y", "85": "y" },
safari: { "14.0": "y", "14.1": "y" },
edge: { "89": "y", "90": "y" },
ie: { "8": "n", "9": "n", "10": "a x", "11": "a" }
},
status: "ok"
};

export function MyApp() {
return <CaniuseLinkBase feature="flexbox" data={customData} />
}
```

### RSC Usage

```jsx
import { CaniuseLinkRsc } from 'react-github-permalink/dist/rsc';

export function MyApp() {
return <CaniuseLinkRsc feature="flexbox" />
}
```

The component fetches live data from the caniuse GitHub repository, ensuring up-to-date compatibility information. Support levels are color-coded:
- **Green**: Fully supported
- **Yellow**: Partial support
- **Red**: Not supported

![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)
Expand Down Expand Up @@ -126,6 +194,7 @@ The global configuration object has this signature
type BaseConfiguration = {
getDataFn: (permalink: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise<GithubPermalinkDataResponse>;
getIssueFn: (issueLink: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise<GithubIssueLinkDataResponse>;
getCaniuseFn: (feature: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise<CaniuseLinkDataResponse>;
githubToken: string | undefined;
onError: ((e: unknown) => void) | undefined;
}
Expand All @@ -136,28 +205,32 @@ type BaseConfiguration = {
Client components are configured via context provider:

```tsx
import { GithubPermalink, GithubIssueLink GithubPermalinkProvider, } from 'react-github-permalink';
import { GithubPermalink, GithubIssueLink, CaniuseLink, GithubPermalinkProvider } from 'react-github-permalink';
import "react-github-permalink/dist/github-permalink.css";

export function MyApp() {
return <GithubPermalinkProvider
getDataFn ={(permalink: string) => {
getDataFn={(permalink: string) => {
// Your implementation to retrieve permalinks here
}}
getIssueFn={(issueLink: string) => {
// Your implementation to retrieve issue links here
}}
getCaniuseFn={(feature: string) => {
// Your implementation to retrieve caniuse data here
}}

// Don't put a put a github token into the context provider in production! It will visible for all the world to see!
// Don't put a github token into the context provider in production! It will be visible for all the world to see!
// Instead you will need to expose a data fetching function on the backend to do it for you
githubToken={process.env.NODE_ENV='development' && process.env.MY_GITHUB_TOKEN}
githubToken={process.env.NODE_ENV === 'development' && process.env.MY_GITHUB_TOKEN}

onError={(err) => {
Sentry.captureException(err);
}}
>
<GithubPermalink permalink="https://github.com/dwjohnston/react-github-permalink/blob/5b15aa07e60af4e317086f391b28cadf9aae8e1b/sample_files/sample1.go#L1-L5"/>
<GithubIssueLink issueLink='https://github.com/dwjohnston/react-github-permalink/issues/2' />
<CaniuseLink feature="flexbox" />
</GithubPermalinkProvider>
}
```
Expand Down
45 changes: 41 additions & 4 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@

import { CaniuseLink } from "../library/CaniuseLink/CaniuseLink";
import { CaniuseLinkBase } from "../library/CaniuseLink/CaniuseLinkBase";
import { GithubPermalinkProvider } from "../library/config/GithubPermalinkContext";
import "../library/GithubPermalink/github-permalink.css";

// Mock data for demonstration
const mockFlexboxData = {
title: "CSS Flexible Box Layout Module",
description: "Method of positioning elements in horizontal or vertical stacks. Support includes all properties prefixed with `flex`, as well as `display: flex`, `display: inline-flex`, `align-content`, `align-items`, `align-self`, `justify-content` and `order`.",
stats: {
chrome: { "89": "y", "90": "y", "91": "y", "92": "y", "93": "y" },
firefox: { "84": "y", "85": "y", "86": "y", "87": "y", "88": "y" },
safari: { "14.0": "y", "14.1": "y", "15.0": "y", "15.1": "y", "15.2": "y" },
edge: { "89": "y", "90": "y", "91": "y", "92": "y", "93": "y" },
ie: { "8": "n", "9": "n", "10": "a x", "11": "a" }
},
status: "ok" as const
};

export default function Home() {
return (
<main>
<div >
<div>
<h1>React Github Permalink</h1>


See the <a href="/storybook-static/index.html">Storybook</a>
<p>See the <a href="/storybook-static/index.html">Storybook</a></p>

<h2>CaniuseLink Demo</h2>
<GithubPermalinkProvider>
<div style={{ margin: "20px 0" }}>
<h3>Live API Example</h3>
<CaniuseLink feature="flexbox" />
</div>

<div style={{ margin: "20px 0" }}>
<h3>Mock Data Example</h3>
<CaniuseLinkBase feature="flexbox" data={mockFlexboxData} />
</div>

<div style={{ margin: "20px 0" }}>
<h3>Inline Usage</h3>
<p>
This layout uses <CaniuseLink feature="flexbox" variant="inline" /> for positioning elements.
</p>
</div>
</GithubPermalinkProvider>
</div>
</main>
);
Expand Down
42 changes: 42 additions & 0 deletions src/library/CaniuseLink/CaniuseLink.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CaniuseLink } from "./CaniuseLink";
import {
GithubPermalinkProvider,
} from "../config/GithubPermalinkContext";
import "../GithubPermalink/github-permalink.css";

const meta: Meta<typeof CaniuseLink> = {
component: CaniuseLink,
};

export default meta;

type Story = StoryObj<typeof CaniuseLink>;

export const BlockSuccess: Story = {
render: () => (
<GithubPermalinkProvider>
<CaniuseLink feature="flexbox" />
</GithubPermalinkProvider>
),
};

export const InlineSuccess: Story = {
render: () => (
<GithubPermalinkProvider>
<p>
This example uses{" "}
<CaniuseLink feature="flexbox" variant="inline" />
{" "}for layout.
</p>
</GithubPermalinkProvider>
),
};

export const BlockError: Story = {
render: () => (
<GithubPermalinkProvider>
<CaniuseLink feature="non-existent-feature" />
</GithubPermalinkProvider>
),
};
71 changes: 71 additions & 0 deletions src/library/CaniuseLink/CaniuseLink.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect, test, describe, vi } from 'vitest'
import { defaultGetCaniuseFn } from '../config/defaultFunctions'

// Mock fetch for testing
global.fetch = vi.fn()

describe('defaultGetCaniuseFn', () => {
test('should handle successful response', async () => {
const mockResponse = {
title: 'CSS Flexible Box Layout Module',
description: 'Method of positioning elements in horizontal or vertical stacks.',
stats: {
chrome: {
'89': 'y',
'90': 'y'
},
firefox: {
'84': 'y',
'85': 'y'
}
}
}

vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
} as Response)

const result = await defaultGetCaniuseFn('flexbox')

expect(result).toEqual({
title: 'CSS Flexible Box Layout Module',
description: 'Method of positioning elements in horizontal or vertical stacks.',
stats: {
chrome: {
'89': 'y',
'90': 'y'
},
firefox: {
'84': 'y',
'85': 'y'
}
},
status: 'ok'
})
})

test('should handle 404 response', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 404,
headers: new Headers(),
} as Response)

const result = await defaultGetCaniuseFn('non-existent-feature')

expect(result).toEqual({
status: '404'
})
})

test('should handle network error', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))

const result = await defaultGetCaniuseFn('flexbox')

expect(result).toEqual({
status: 'other-error'
})
})
})
30 changes: 30 additions & 0 deletions src/library/CaniuseLink/CaniuseLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client"

import { useContext, useEffect, useState } from "react";
import { GithubPermalinkContext, CaniuseLinkDataResponse } from "../config/GithubPermalinkContext";
import { CaniuseLinkBase, CaniuseLinkBaseProps } from "./CaniuseLinkBase";

type CaniuseLinkProps = Omit<CaniuseLinkBaseProps, "data">;

export function CaniuseLink(props: CaniuseLinkProps) {
const { feature } = props;
const [data, setData] = useState(null as null | CaniuseLinkDataResponse);
const { getCaniuseFn, githubToken, onError } = useContext(GithubPermalinkContext);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
getCaniuseFn(feature, githubToken, onError).then((v) => {
setIsLoading(false);
setData(v);
});
}, [getCaniuseFn, githubToken, feature, onError]);

if (isLoading) {
return null;
}
if (!data) {
throw new Error("Loading is complete, but no data was returned.");
}

return <CaniuseLinkBase {...props} data={data} />;
}
Loading