Skip to content
Open
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

Display live GitHub code in an iframe.

> [Demo](https://github-iframe.vercel.app) | [Blog post](https://sanitypress.dev/blog/introducing-github-iframe)
> [Demo](https://github-iframe-kappa.vercel.app) | [Blog post](https://sanitypress.dev/blog/introducing-github-iframe)

```html
<iframe src="https://github-iframe.vercel.app/{owner}/{repo}/{path}"></iframe>
<iframe src="https://github-iframe-kappa.vercel.app/{owner}/{repo}/{path}"></iframe>

<!-- e.g. -->
<iframe
src="https://github-iframe.vercel.app/nuotsu/github-iframe/src/lib/store.ts"
src="https://github-iframe-kappa.vercel.app/nuotsu/github-iframe/src/lib/store.ts"
width="100%"
height="400px"
title="nuotsu/github-iframe/src/lib/store.ts"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 12 additions & 5 deletions src/app/(iframe)/[owner]/[repo]/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getRawContent } from '@/lib/octokit/utils'
import setHighlights from '@/lib/octokit/setHighlights'
import ConvertHashToParam from './ConvertHashToParam'
import Code from './Code'
Expand All @@ -8,6 +7,7 @@ import DisplayPath from './DisplayPath'
import BgColor from './BgColor'
import { cn } from '@/lib/utils'
import type { Display } from '@/lib/store'
import { fetchGithubFileContent } from '@/lib/octokit/github';

export default async function Page({
params,
Expand All @@ -17,20 +17,27 @@ export default async function Page({
owner: string
repo: string
path?: string[]
}>
}>;
searchParams: Promise<{
theme?: string
lang?: string
display?: Display
lineNums?: string
L?: string
scrollTo?: string
}>
token?: string
}>;
}) {
const { owner, repo, path } = await params
const { theme, lang, display, lineNums, L, scrollTo } = await searchParams
const { theme, lang, display, lineNums, L, scrollTo, token} = await searchParams

const raw = await getRawContent({ owner, repo, path })
let raw: string;
try {
raw = await fetchGithubFileContent(owner, repo, path?.join('/') || '', token);
} catch (error) {
console.error('Error fetching content:', error);
raw = error instanceof Error ? error.message : 'Error loading the code...Try again';
}

return (
<>
Expand Down
305 changes: 181 additions & 124 deletions src/app/(site)/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,192 @@
'use client'

import { useState, useEffect } from 'react'
import { store, DISPLAYS, type Theme, type Display } from '@/lib/store'
import Input from '@/ui/Input'
import { debounce } from '@/lib/utils'
import {
VscSettings,
VscFileCode,
VscRepo,
VscSymbolColor,
VscEye,
VscEyeClosed,
VscPaintcan,
VscCode,
VscListSelection,
VscListOrdered,
VscSettings,
VscFileCode,
VscRepo,
VscSymbolColor,
VscEye,
VscEyeClosed,
VscPaintcan,
VscCode,
VscListSelection,
VscListOrdered,
VscLock,
VscUnlock,
} from 'react-icons/vsc'
import { bundledThemes } from 'shiki'
import Checkbox from '@/ui/Checkbox'

export default function Options() {
const {
repo,
path,
theme,
lang,
display,
lineNums,
highlight,
scrollTo,
setRepo,
setPath,
setTheme,
setLang,
setDisplay,
setLineNums,
setHighlight,
setScrollTo,
} = store()

const ext = path?.split('.').at(-1) ?? ''

return (
<fieldset className="border border-neutral-300 p-2">
<legend className="with-icon">
<VscSettings /> Options
</legend>

<div className="grid gap-x-4 gap-y-2 md:grid-cols-2">
<Input
title="Repo owner/repo"
icon={VscRepo}
defaultValue={repo}
onChange={debounce((e) => setRepo(e.target.value))}
pattern=".+/.+"
required
/>

<Input
title="Path to file"
icon={VscFileCode}
defaultValue={path}
onChange={debounce((e) => setPath(e.target.value))}
className="group-[:has(.view-file-source:hover)_input]/root:border-black/30"
required
/>

<Input icon={VscSymbolColor}>
<select
title="Theme"
className="input w-full"
defaultValue={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
>
<option disabled>Select a theme</option>
{Object.entries(bundledThemes).map(([option]) => (
<option key={option}>{option}</option>
))}
</select>
</Input>

<Input
label="Language"
icon={VscCode}
defaultValue={lang}
onChange={debounce((e) => setLang(e.target.value))}
placeholder={ext}
/>

<Input
label="Display"
icon={display === 'none' ? VscEyeClosed : VscEye}
>
<select
className="input w-full"
defaultValue={display}
onChange={(e) => setDisplay(e.target.value as Display)}
>
{DISPLAYS.map((option) => (
<option key={option}>{option}</option>
))}
</select>
</Input>

<Checkbox
label={`${lineNums ? 'Hide' : 'Show'} line numbers`}
onIcon={VscListOrdered}
offIcon={VscListSelection}
defaultChecked={lineNums}
reverseChecked
onChange={(e) => setLineNums(e.target.checked)}
/>

<Input
label="Highlight"
title="Lines to highlight"
icon={VscPaintcan}
defaultValue={highlight}
onChange={debounce((e) => setHighlight(e.target.value))}
pattern="((\d+|\d+-\d+),)*(\d+|\d+-\d+)"
placeholder="lines (e.g. 5,14-19)"
/>

{highlight && (
<Checkbox
className="anim-fade-to-r"
label="Scroll to highlighted line"
defaultChecked={scrollTo}
onChange={(e) => setScrollTo(e.target.checked)}
/>
)}
</div>
</fieldset>
)
}
const {
repo,
path,
theme,
lang,
display,
lineNums,
highlight,
scrollTo,
setRepo,
setPath,
setTheme,
setLang,
setDisplay,
setLineNums,
setHighlight,
setScrollTo,
setToken, // New
} = store()

const ext = path?.split('.').at(-1) ?? ''

const [token, setLocalToken] = useState<string>('')
const [showTokenInput, setShowTokenInput] = useState<boolean>(false)
const [showToken, setShowToken] = useState<boolean>(false)

useEffect(() => {
if (showTokenInput && token) {
setToken(token)
} else {
setToken('') // Clear token in store when hidden
}
}, [token, showTokenInput, setToken])

return (
<fieldset className="border border-neutral-300 p-2">
<legend className="with-icon">
<VscSettings /> Options
</legend>

<div className="grid gap-x-4 gap-y-2 md:grid-cols-2">
<Input
title="Repo owner/repo"
icon={VscRepo}
defaultValue={repo}
onChange={debounce((e) => setRepo(e.target.value))}
pattern=".+/.+"
required
/>

<Input
title="Path to file"
icon={VscFileCode}
defaultValue={path}
onChange={debounce((e) => setPath(e.target.value))}
className="group-[:has(.view-file-source:hover)_input]/root:border-black/30"
required
/>

<div className="col-span-2 space-y-2">
<button
type="button"
onClick={() => setShowTokenInput(!showTokenInput)}
className="w-fit border border-neutral-300 px-3 py-1 rounded-md bg-white text-neutral-700 hover:bg-neutral-50 transition-colors flex items-center gap-2 text-sm"
>
{showTokenInput ? <VscUnlock /> : <VscLock />}
{showTokenInput ? 'Not Private' : 'Private Repo?'}
</button>

{showTokenInput && (
<div className="space-y-2">
<Input
title="Personal Access Token"
icon={showToken ? VscEye : VscEyeClosed}
value={token}
onChange={(e) => setLocalToken(e.target.value)}
type={showToken ? 'text' : 'password'}
placeholder="ghp_xxxxxxxxxxxxxxxx"
/>
<span
onClick={() => setShowToken(!showToken)}
className="text-sm text-blue-500 cursor-pointer hover:underline"
>
{showToken ? 'Hide token' : 'Show token'}
</span>
<p className="text-xs text-neutral-500">
Use a token with <code>repo</code> scope from{' '}
<a
href="https://github.com/settings/tokens"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
GitHub Settings
</a>. Fetches automatically as you type.
</p>
</div>
)}
</div>

<Input icon={VscSymbolColor}>
<select
title="Theme"
className="input w-full"
defaultValue={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
>
<option disabled>Select a theme</option>
{Object.entries(bundledThemes).map(([option]) => (
<option key={option}>{option}</option>
))}
</select>
</Input>

<Input
label="Language"
icon={VscCode}
defaultValue={lang}
onChange={debounce((e) => setLang(e.target.value))}
placeholder={ext}
/>

<Input
label="Display"
icon={display === 'none' ? VscEyeClosed : VscEye}
>
<select
className="input w-full"
defaultValue={display}
onChange={(e) => setDisplay(e.target.value as Display)}
>
{DISPLAYS.map((option) => (
<option key={option}>{option}</option>
))}
</select>
</Input>

<Checkbox
label={`${lineNums ? 'Hide' : 'Show'} line numbers`}
onIcon={VscListOrdered}
offIcon={VscListSelection}
defaultChecked={lineNums}
reverseChecked
onChange={(e) => setLineNums(e.target.checked)}
/>

<Input
label="Highlight"
title="Lines to highlight"
icon={VscPaintcan}
defaultValue={highlight}
onChange={debounce((e) => setHighlight(e.target.value))}
pattern="((\d+|\d+-\d+),)*(\d+|\d+-\d+)"
placeholder="lines (e.g. 5,14-19)"
/>

{highlight && (
<Checkbox
className="anim-fade-to-r"
label="Scroll to highlighted line"
defaultChecked={scrollTo}
onChange={(e) => setScrollTo(e.target.checked)}
/>
)}
</div>
</fieldset>
)
}
Loading