generated from githubnext/blocks-template
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
83dfb38
commit 83657f2
Showing
10 changed files
with
645 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,11 @@ | ||
[ | ||
{ | ||
"type": "file", | ||
"id": "file-block", | ||
"title": "Example File Block", | ||
"description": "A basic file block", | ||
"entry": "blocks/example-file-block/index.tsx", | ||
"matches": ["*"], | ||
"example_path": "https://github.com/facebook/react/blob/main/packages/react-dom/index.js" | ||
}, | ||
{ | ||
"type": "folder", | ||
"id": "folder-block", | ||
"title": "Example Folder Block", | ||
"description": "A basic folder block", | ||
"entry": "blocks/example-folder-block.tsx", | ||
"id": "code-stepper", | ||
"title": "Code Stepper", | ||
"description": "Step through versions of code: each version should have the same filename, followed by a number.", | ||
"entry": "blocks/code-stepper/index.tsx", | ||
"matches": ["*"], | ||
"example_path": "https://github.com/githubocto/flat" | ||
"example_path": "https://github.com/cs50/lectures/tree/python/2022/2/src2" | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { useState } from "react"; | ||
import { getTrackBackground, Range } from "react-range"; | ||
import { tw } from "twind"; | ||
import { useInterval } from "./hooks"; | ||
|
||
export const Timeline = ({ | ||
files = [], | ||
activeFileIndex = 0, | ||
setActiveFileIndex, | ||
}: { | ||
files: File[]; | ||
activeFileIndex: number; | ||
setActiveFileIndex: (index: number) => void; | ||
}) => { | ||
const [isPlaying, setIsPlaying] = useState(false); | ||
const activeFile = files[activeFileIndex]; | ||
|
||
useInterval( | ||
() => { | ||
const nextFileIndex = activeFileIndex + 1; | ||
if (nextFileIndex >= files.length - 1) setIsPlaying(false); | ||
setActiveFileIndex(nextFileIndex) | ||
}, | ||
isPlaying ? 500 : null | ||
); | ||
|
||
return ( | ||
<div className={tw`w-full px-3 pt-7 flex items-center gap-3`}> | ||
<button | ||
onClick={() => { | ||
if (activeFileIndex === files.length - 1) { | ||
setActiveFileIndex(0); | ||
} | ||
setIsPlaying(!isPlaying); | ||
}} | ||
> | ||
{isPlaying ? ( | ||
<svg viewBox="0 0 24 24" width="24" height="24" strokeLinecap="round" className={tw`text-[#0969da]`} fill="currentColor"> | ||
<path | ||
d="M 9.5 9.5 L 9.5 14.5" | ||
strokeWidth="2" | ||
stroke="currentColor" | ||
></path> | ||
<path | ||
d="M 14.5 9.5 L 14.5 14.5" | ||
strokeWidth="2" | ||
stroke="currentColor" | ||
></path> | ||
<path | ||
fillRule="evenodd" | ||
d="M12 2.5a9.5 9.5 0 100 19 9.5 9.5 0 000-19zM1 12C1 5.925 5.925 1 12 1s11 4.925 11 11-4.925 11-11 11S1 18.075 1 12z" | ||
></path> | ||
</svg> | ||
) : ( | ||
<svg viewBox="0 0 24 24" width="24" height="24" className={tw`text-[#0969da]`} fill="currentColor"> | ||
<path d="M9.5 15.584V8.416a.5.5 0 01.77-.42l5.576 3.583a.5.5 0 010 .842l-5.576 3.584a.5.5 0 01-.77-.42z"></path> | ||
<path | ||
fillRule="evenodd" | ||
d="M12 2.5a9.5 9.5 0 100 19 9.5 9.5 0 000-19zM1 12C1 5.925 5.925 1 12 1s11 4.925 11 11-4.925 11-11 11S1 18.075 1 12z" | ||
></path> | ||
</svg> | ||
)} | ||
</button> | ||
<div className={tw`flex-1 flex items-center`}> | ||
<Range | ||
step={1} | ||
min={0} | ||
max={files.length - 1} | ||
values={[activeFileIndex]} | ||
onChange={(values) => { | ||
if (isPlaying) { | ||
setIsPlaying(false); | ||
} | ||
setActiveFileIndex(values[0]); | ||
}} | ||
renderMark={({ props, index }) => ( | ||
<div | ||
{...props} | ||
className={tw`w-[12px] h-[12px]`} | ||
style={{ | ||
...props.style, | ||
backgroundColor: index * 1 < activeFileIndex ? "#0969da" : "#d0d7de", | ||
border: "1px solid white", | ||
borderRadius: "100%", | ||
}} | ||
/> | ||
)} | ||
renderTrack={({ props, children }) => ( | ||
<div | ||
{...props} | ||
className={tw( | ||
`bg-gray-200 h-1 w-full`, | ||
|
||
)} | ||
style={{ | ||
...props.style, | ||
background: getTrackBackground({ | ||
values: [activeFileIndex], | ||
colors: ["#0969da", "#d0d7de"], | ||
min: 0, | ||
max: files.length - 1, | ||
}), | ||
}} | ||
> | ||
{children} | ||
</div> | ||
)} | ||
renderThumb={({ props }) => ( | ||
<div | ||
{...props} | ||
className={tw`w-4 h-4 rounded-full bg-[#0969da] flex items-center justify-center relative focus:outline-none`} | ||
style={{ | ||
...props.style, | ||
}} | ||
> | ||
<div | ||
className={tw`absolute -top-3 w-2 h-2 bg-[#0969da] transform rotate-45`} | ||
></div> | ||
<div | ||
className={tw`bg-[#0969da] absolute -top-8 text-white p-1 text-xs`} | ||
> | ||
<span className={tw`font-mono`}> | ||
{(activeFile?.path || "").split("/").pop()} | ||
</span> | ||
</div> | ||
</div> | ||
)} | ||
/> | ||
</div> | ||
</div > | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { useEffect, useRef } from "react"; | ||
|
||
export function useInterval(callback: () => void, delay: number | null) { | ||
const savedCallback = useRef<undefined | (() => void)>(); | ||
|
||
// Remember the latest callback. | ||
useEffect(() => { | ||
savedCallback.current = callback; | ||
}, [callback]); | ||
|
||
// Set up the interval. | ||
useEffect(() => { | ||
function tick() { | ||
savedCallback.current && savedCallback.current(); | ||
} | ||
if (delay !== null) { | ||
let id = setInterval(tick, delay); | ||
return () => clearInterval(id); | ||
} | ||
}, [delay]); | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import { FolderBlockProps, getLanguageFromFilename } from "@githubnext/blocks"; | ||
import { Box, FormControl, Select } from "@primer/react"; | ||
import { tw } from "twind"; | ||
import { useEffect, useState } from "react"; | ||
import { Endpoints } from "@octokit/types"; | ||
import { Timeline } from "./Timeline"; | ||
import { motion } from "framer-motion"; | ||
import SyntaxHighlighter from "react-syntax-highlighter"; | ||
import style from "react-syntax-highlighter/dist/esm/styles/hljs/github-gist"; | ||
import "./index.css" | ||
|
||
export type RawTree = | ||
Endpoints["GET /repos/{owner}/{repo}/git/trees/{tree_sha}"]["response"]["data"]; | ||
export type Tree = RawTree["tree"]; | ||
export type File = RawTree["tree"][0] & { | ||
content: string, | ||
} | ||
export default ({ tree, context, onRequestGitHubData }: FolderBlockProps) => { | ||
const [files, setFiles] = useState<File[]>([]); | ||
const [activeFileIndex, setActiveFileIndex] = useState<number>(0); | ||
const getLessonNameFromLessonPath = (lessonPath: string) => { | ||
const filename = lessonPath.split("/").pop()?.split(".")[0] || ""; | ||
// remove trailing numbers | ||
return filename.replace(/\d+$/, ""); | ||
} | ||
const allLessonNames = files.map(file => file.path | ||
? getLessonNameFromLessonPath(file.path) | ||
: ""); | ||
const uniqueLessonNames = [...new Set(allLessonNames)]; | ||
const [lessonName, setLessonName] = useState<string>(uniqueLessonNames[0]); | ||
const filesInLesson = files.filter(file => file.path?.startsWith(`${context.path}/${lessonName}`)); | ||
|
||
const activeFile = filesInLesson[activeFileIndex]; | ||
const lines = activeFile?.content?.split("\n"); | ||
const language = activeFile?.path ? getLanguageFromFilename(activeFile?.path.split("/").pop() || "") : "" | ||
const languageCommentMarker = languageCommentMarkersMap[language] || "#" | ||
const numberOfCommentLines = lines?.findIndex(line => !line.startsWith(languageCommentMarker)) || 0 | ||
|
||
useEffect(() => { | ||
setActiveFileIndex(0); | ||
}, [lessonName]); | ||
|
||
useEffect(() => { | ||
setLessonName(uniqueLessonNames[0]); | ||
}, [uniqueLessonNames.join(",")]); | ||
|
||
const onFetchFileContents = async (path) => { | ||
const url = `/repos/${context.owner}/${context.repo}/contents/${path}` | ||
console.log(url) | ||
const data = await onRequestGitHubData(url, { | ||
ref: context.sha, | ||
}); | ||
const decodedData = atob(data.content); | ||
return decodedData; | ||
} | ||
const onUpdateFiles = async () => { | ||
const immediateFiles = tree.filter(item => ( | ||
item.type === "blob" | ||
&& item.path?.startsWith(`${context.path}/`) | ||
&& item.path?.slice(context.path.length + 1).split("/").length === 1 | ||
)) | ||
// .slice(0, 3) | ||
console.log(immediateFiles, tree, context) | ||
const files = await Promise.all(immediateFiles.map(async item => { | ||
const data = await onFetchFileContents(item.path); | ||
return { ...item, content: data } | ||
})) | ||
const sortedFiles = files.sort((a, b) => { | ||
if (a.name < b.name) return -1; | ||
if (a.name > b.name) return 1; | ||
return 0; | ||
}) | ||
setFiles(sortedFiles) | ||
} | ||
useEffect(() => { onUpdateFiles() }, [context.path]) | ||
|
||
return ( | ||
<Box p={4}> | ||
<Box | ||
borderColor="border.default" | ||
borderWidth={1} | ||
borderStyle="solid" | ||
borderRadius={6} | ||
overflow="hidden" | ||
> | ||
<Box | ||
display="flex" | ||
background="canvas.subtle" | ||
borderColor="border.default" | ||
borderBottomWidth={1} | ||
borderStyle="solid" | ||
p={3} | ||
> | ||
<FormControl | ||
sx={{ width: "20em" }} | ||
> | ||
<FormControl.Label>Lesson</FormControl.Label> | ||
<Select value={lessonName} onChange={e => setLessonName(e.target.value)} | ||
> | ||
{uniqueLessonNames.map(name => ( | ||
<Select.Option key={name} value={name}> {name} </Select.Option> | ||
))} | ||
</Select> | ||
</FormControl> | ||
{!!activeFile && filesInLesson.length > 1 && ( | ||
<Box px={2} width="100%"> | ||
<Timeline | ||
files={filesInLesson} | ||
activeFileIndex={activeFileIndex} | ||
setActiveFileIndex={setActiveFileIndex} | ||
/> | ||
</Box> | ||
)} | ||
</Box> | ||
|
||
{!!activeFile && ( | ||
<pre className={tw`p-6`}> | ||
{lines.map((line, index) => { | ||
if (numberOfCommentLines > index) return ( | ||
<motion.div | ||
key={line || `line-${index}`} | ||
layout | ||
className={tw( | ||
`flex min-h-[1em] -mx-4 -my-2`, | ||
"bg-[#ddf4ff] px-4 py-3 text-[#0550ae]" | ||
)} | ||
> | ||
<Box className={tw`w-[2em] mr-1 text-[#0550ae]`}> | ||
{index + 1} | ||
</Box> | ||
{line.slice(1).trim()} | ||
</motion.div> | ||
) | ||
const numberOfMatchingPreviousLines = lines.slice(0, index).filter(previousLine => previousLine === line).length; | ||
return ( | ||
<motion.div | ||
key={`line--${line}--${numberOfMatchingPreviousLines}`} | ||
layout | ||
className={tw( | ||
`flex min-h-[1.3em] py-[0.1em]`, | ||
)} | ||
initial={{ x: -30, opacity: 0.5 }} | ||
animate={{ x: 0, opacity: 1 }} | ||
transition={{ duration: 0.3 }} | ||
exit={{ x: -30, opacity: 0.5 }} | ||
> | ||
<Box className={tw`w-[2em] mr-1`} color="fg.subtle"> | ||
<motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }}> | ||
{index + 1} | ||
</motion.span> | ||
</Box> | ||
<SyntaxHighlighter | ||
language={syntaxHighlighterLanguageMap[language] || "javascript"} | ||
lineNumberStyle={{ opacity: 0.45 }} | ||
className={tw(`!p-0`)} | ||
wrapLines | ||
wrapLongLines | ||
style={style} | ||
> | ||
{line} | ||
</SyntaxHighlighter> | ||
</motion.div> | ||
) | ||
})} | ||
</pre> | ||
)} | ||
</Box> | ||
</Box> | ||
); | ||
} | ||
|
||
const syntaxHighlighterLanguageMap = { | ||
JavaScript: "javascript", | ||
TypeScript: "typescript", | ||
} as Record<string, string>; | ||
|
||
const languageCommentMarkersMap = { | ||
JavaScript: "//", | ||
TypeScript: "//", | ||
Python: "#", | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.