Skip to content

Instantly share code, notes, and snippets.

@neisdev
Created April 13, 2023 12:32
Show Gist options
  • Save neisdev/c20e269c738a13d4dfbb7a043baf4e00 to your computer and use it in GitHub Desktop.
Save neisdev/c20e269c738a13d4dfbb7a043baf4e00 to your computer and use it in GitHub Desktop.
React - Gist UI
<div class="container-fluid">
<div id="container"></div>
</div>
const AceEditor = ReactAce.default;
const { Component } = React;
// Gist User
class GistUser extends Component {
constructor(props){
super(props);
this.props = {
gist: {}
};
}
render(){
const gist = this.props.gist;
const file = gist.files && gist.files[Object.keys(gist.files)[0]];
return (
<div className="gist__user d-flex ">
<img src={gist.owner.avatar_url} alt="Gist Owner" className="rounded mr-2" />
<div>
<div>
<a href="#">{gist.owner.login}</a> /
<a href="#" id="toggleBtn"> {file.filename}</a>
</div>
<div className="text-muted">
<small>Created {gist.created_at}</small>
</div>
<div className="gist__description text-muted pt-1">{gist.description}</div>
</div>
</div>
)
}
}
// Gist File
const GistFile = ({file}) => (
<div className="gist__file mt-3">
<div className="gist__filename">
<a href="#{file.filename}"><i className="fa fa-file-code-o"></i> {file.filename}</a>
<a href={file.raw_url} className="btn btn-outline-secondary btn-sm" target="_blank">Raw</a>
</div>
<div className="gist__code">
<pre className="line-numbers"><code className="language-javascript">{file.content}</code></pre>
</div>
</div>
)
// Gist Stats
const GistStats = ({
gist
}) => (
<div className="gist__stats text-muted d-flex hidden-sm-down">
<div className="mr-4 text-nowrap"><i className="fa fa-file-code-o mr-1"></i><span>1</span> file</div>
<div className="mr-4 text-nowrap"><i className="fa fa-code-fork mr-1"></i>0 forks</div>
<div className="mr-4 text-nowrap"><i className="fa fa-comment-o mr-1"></i>0 comments</div>
<div className="mr-2 text-nowrap"><i className="fa fa-star mr-1"></i>0 stars</div>
</div>
)
//GistDetail
const GistDetail = ({
gist,
compact
}) => {
let classNames = 'gist gist--detail m-4';
if (compact) {
classNames += ' gist--compact';
}
let files = [];
for (let key in gist.files) {
files.push(gist.files[key]);
}
return (
<div className={classNames}>
<div className="d-flex justify-content-between">
<GistUser gist={gist}/>
<GistStats gist={gist}/>
</div>
<div className="gist__files">
{ files.map((file) =>(<GistFile key={file.filename} file={file}/>)) }
</div>
</div>
)
}
//GistFileInput
class GistFileInput extends React.Component {
constructor(props) {
super(props);
this.state = {
filename: '',
ext: null,
content: ''
};
this._handleClickDelete = this._handleClickDelete.bind(this);
this._handleChange = this._handleChange.bind(this);
}
_handleClickDelete(e) {
console.log('_handleClickDelete', e, this.props.key);
if (this.props.onDelete) {
this.props.onDelete(this.state.file);
}
}
_handleChange(e) {
this.setState((prevState, props) => ({ content: e}))
//this.props.content = e;
//this.props.onFileChange(e)
console.log('_handleChange', e);
}
render() {
const {filename, content} = this.state;
//const {filename, content} = this.props;
console.log('GistFileInput.render', filename, content);
return (
<div>
<div className="gist__file-toolbar d-flex p-2 justify-content-between">
<div>
<div className="input-group">
<input type="text"
className="form-control"
defaultValue={filename}
placeholder="Filename including extension..." />
<span className="input-group-btn">
<button className="btn btn-outline-danger btn-delete-gist-file"
type="button"
title="Delete File"
onClick={(e) => this._handleClickDelete(e)}>
<i className="fa fa-trash-o"></i>
</button>
</span>
</div>
</div>
<div className="hidden-sm-down">
<div className="gist__file-actions d-flex">
<select className="form-control select-sm js-code-indent-mode">
<optgroup label="Indent mode">
<option value="space">Spaces</option>
<option value="tab">Tabs</option>
</optgroup>
</select>
<select className="form-control select-sm js-code-indent-width">
<optgroup label="Indent size">
<option value="2">2</option>
<option value="4">4</option>
<option value="8">8</option>
</optgroup>
</select>
<select className="form-control select-sm js-code-wrap-mode">
<optgroup label="Line wrap mode">
<option value="off">No wrap</option>
<option value="on">Soft wrap</option>
</optgroup>
</select>
</div>
</div>
</div>
<div className="gist__file-code">
<AceEditor
value={content}
height= '10rem'
width='100%'
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
tabSize: 4,
fontSize: 13,
showGutter: true
}}
mode="javascript"
theme="github"
onChange={this._handleChange}
name="UNIQUE_ID_OF_DIV"
editorProps={{$blockScrolling: true}}
/>
</div>
</div>
)
}
}
//GistForm - Display gist form and submit to api.
class GistForm extends React.Component {
constructor(props) {
super(props);
this.state = {
isToggleOn: true,
files: [
{filename: '', content: ''}
]
};
this._handleClickAdd = this._handleClickAdd.bind(this);
this._handleDelete = this._handleDelete.bind(this);
this._handleSubmit = this._handleSubmit.bind(this);
this._handleFileChange = this._handleFileChange.bind(this);
}
_handleClickAdd(e) {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn,
files: prevState.files.concat({
filename: '',
content: ''
})
}));
}
_handleSubmit(e){
e.preventDefault();
console.log('Submit', this.state);
}
_handleFileChange(e){
e.preventDefault();
console.log('_handleFileChange', this.state);
}
_handleDelete(e) {
console.log('handleDelete', e, this);
let index = this.state.files.indexOf(e);
console.warn('Remove item', index);
}
render() {
return (
<div className="p-3 mb-4">
<form className="gist-form" onSubmit={this._handleSubmit}>
<div className="mb-4">
<input type="text" className="form-control" placeholder="Gist description..." />
</div>
<ul className="gist__files">
{this.state.files &&
this.state.files.map((file, index)=>(
<li>
<GistFileInput
className="gist__file my-3"
key={index}
content={file.content}
filename={file.filename}
onFileChange={this._handleFileChange}
onDelete={this._handleDelete}/>
</li>))
}
</ul>
<div className="form-actions d-flex justify-content-between mt-3">
<button type="button"
onClick={this._handleClickAdd}
className="btn btn-sm btn-secondary gist__add-btn">Add File</button>
<div>
<button className="btn btn-secondary" type="submit">Create public gist</button>
</div>
</div>
</form>
</div>
)
}
}
function getGists(username) {
/* Dont get black listed
return fetch(`https://api.github.com/users/${username}/gists`)
.then(resp => (resp.json().then(json => json)));
*/
return new Promise((resolve, reject) => {
resolve(MOCK_GISTS)
})
}
function getGist(id) {
return fetch(`https://api.github.com/gists/${id}`)
.then(resp => (resp.json().then(json => json)));
}
//App
class App extends React.Component {
constructor(props) {
super(props);
this.defaultProps = {
brand: 'NextGist'
};
this.state = {
username: 'jonniespratley',
gists: []
};
}
componentWillMount() {
getGists('jonniespratley').then(data => this.setState({
gists: data
}))
console.log('componentWillMount', this);
}
render() {
return (
<div>
<header>
<nav className="navbar navbar-toggleable-md navbar-inverse bg-inverse fixed-top">
<button className="navbar-toggler navbar-toggler-right" type="button"
data-toggle="collapse"
data-target="#appNavar"
aria-controls="appNavar"
aria-expanded="false"
aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<a className="navbar-brand" href="/">GitHub <span>Gist</span></a>
<div className="collapse navbar-collapse" id="appNavar">
<input type="search" placeholder="Search..." className="form-control w-50"/>
</div>
</nav>
</header>
<div className="container-fluid gists">
<GistForm/>
{this.state.gists.map(gist => (<GistDetail key={gist.id} gist={gist} compact/>))}
</div>
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.6.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-ace/4.2.1/react-ace.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.10.0/highlight.min.js"></script>
<script src="//codepen.io/jonniespratley/pen/YVPqVR.js"></script>
<script src="//codepen.io/jonniespratley/pen/ZKQoaj.js"></script>
body{
padding-top: 65px;
}
.bg {
&--white {
background: #fff;
}
&--gray {
background: #eee;
}
}
.navbar {
.brand {
display: inline-block;
}
.form-control {
min-width: 300px;
background: #3f4347;
font-size: .9rem;
border: none;
:focus{
color: #fff;
}
}
}
pre[class*=language-] {
padding: 0;
}
.gists {
&__recent {}
}
.gist {
display: block;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
&__label {
font-weight: bold;
display: block;
}
&__description {
font-size: .9rem;
&--small {
font-size: .8rem;
}
}
:not(pre)>code[class*=language-],
pre[class*=language-] {
background: none;
}
//Not campact styles
&:not(--compact) {
// background: red;
.gist__embed {
display: none;
}
}
&__stats {
display: none;
}
&--compact {
.gist__files {
max-height: 180px;
overflow: hidden;
> :not(:first-child){
display: none;
}
}
.gist__tabs {
display: none;
}
.gist__description {
font-size: .8rem;
}
.gist__file {
&:hover {
border-color: blue;
}
}
.gist__actions {
display: none;
}
.gist__filename {
}
.gist__code {
padding: 0;
border-radius: 4px;
cursor: pointer;
max-height: 150px;
overflow: hidden;
pre {
margin: 0;
}
&:hover {
border-color: blue;
}
}
//stats
.gist__stats {
font-size: .8rem;
display: flex;
font-weight: 500;
i {
margin-right: .2rem;
}
}
}
&__user {
img {
max-height: 35px;
}
}
//File
&__file {
border: 1px solid #ddd;
border-radius: 4px;
margin: 1rem 0;
background: #fafbfc;
}
//Filename
&__filename {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important;
padding: .5rem;
font-size: .8rem;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
a {
font-weight: bold;
&:hover {
text-decoration: none;
}
}
}
//Code
&__code {
background: #fff;
font-size: .9rem;
}
&__stats {
display: none;
}
&__files {
list-style-type: none;
margin: 0;
padding: 0;
> li {
margin-bottom: 2rem;
border: 1px solid #ddd;
border-radius: 3px;
.gist__file-toolbar {
border-bottom: 1px solid #ddd;
padding: .5rem;
background: #fafbfc;
input {
background: #fff;
min-width: 300px;
}
select {
margin-right: .5rem;
height: calc(2rem + 1px) !important;
}
}
textarea {
border: none;
}
}
}
.select-sm {
height: 8px;
min-height: 28px;
padding-top: 1px;
padding-bottom: 2px;
font-size: 12px;
}
//File Toolbar
&__file-toolbar{
}
.form-control{
min-height: 34px;
padding: 6px 8px;
font-size: 14px;
line-height: 20px;
color: #24292e;
vertical-align: middle;
background-repeat: no-repeat;
background-position: right 8px center;
border: 1px solid #d1d5da;
border-radius: 3px;
outline: none;
box-shadow: inset 0 1px 2px rgba(27,31,35,0.075);
background-color: #fafbfc;
}
}
/* Prism Line Numbers */
pre.line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em;
/* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid #999;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #999;
display: block;
padding-right: 0.8em;
text-align: right;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.6.0/themes/prism.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment