Skip to content

Instantly share code, notes, and snippets.

@jimdoescode
Last active January 10, 2020 05:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimdoescode/4c974cfae29d6a117b2a to your computer and use it in GitHub Desktop.
Save jimdoescode/4c974cfae29d6a117b2a to your computer and use it in GitHub Desktop.
An early version of CodeMana
var React = require("react");
var Router = require('react-router');
var GistForm = require("./GistForm.js");
var AppHeader = require("./AppHeader.js");
var AppFooter = require("./AppFooter.js");
var Gist = require("./Gist.js");
var Config = require("./Config.js");
var DefaultRoute = Router.DefaultRoute;
var NotFoundRoute = Router.NotFoundRoute;
var Route = Router.Route;
var RouteHandler = Router.RouteHandler;
var App = React.createClass({
render: function() {
return (
<div className="app">
<AppHeader origin={Config.origin}/>
<RouteHandler/>
<AppFooter/>
</div>
);
}
});
var GistRoute = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
render: function() {
return (
<Gist id={this.context.router.getCurrentParams().gistId}/>
);
}
});
var HomeRoute = React.createClass({
render: function() {
return (
<div className="container main">
<section className="hero">
<h1>
<div className="fa fa-flask fa-3x"/>
<p>Up your Gist magic <i className="fa fa-magic"/></p>
</h1>
<p>
CodeMana lets you comment in line on your Gists, simply click the line you want to talk about.
It works entirely in your browser, only calling GitHub to post comments and retrieve Gists.
</p>
<p>
If you're curious <a href="https://github.com/jimdoescode/codemana">peruse the code</a>. It's comprised mostly of React components.
</p>
<GistForm className="pure-form" showButton="true"/>
</section>
</div>
);
}
});
var FourOhFourRoute = React.createClass({
render: function() {
return (
<div className="container main">
NOT FOUND!!
</div>
);
}
});
var routes = (
<Route name="app" path={Config.root} handler={App}>
<Route name="gist" path=":gistId" handler={GistRoute}/>
<DefaultRoute handler={HomeRoute}/>
<NotFoundRoute handler={FourOhFourRoute}/>
</Route>
);
Router.run(routes, Router.HistoryLocation, function (Handler) {
React.render(<Handler/>, document.getElementById("mount-point"));
});
var React = require("react");
module.exports = React.createClass({
render: function() {
return (
<footer>
<p className="container">&copy; Jim Saunders</p>
</footer>
);
}
});
var React = require("react");
var GistForm = require("./GistForm.js");
module.exports = React.createClass({
getDefaultProps: function() {
return {origin: window.location.origin};
},
updateStyle: function(event) {
event.preventDefault();
document.getElementById('highlight-style').href = event.target.value;
},
shouldComponentUpdate: function(newProps, newState) {
return false; //No need to update this thing, it's static
},
render: function() {
return (
<header className="app-header">
<nav className="pure-menu pure-menu-horizontal pure-menu-fixed">
<div className="container">
<a className="pure-menu-heading pull-left logo" href={this.props.origin}>
<span>CODE</span><i className="fa fa-flask"/><span>MANA</span>
</a>
<GistForm className="pure-form pull-left pure-u-2-3"/>
<ul className="pure-menu-list pull-right">
<li className="pure-menu-item">
<select onChange={this.updateStyle}>
<option value="css/default.css">Default Highlighting</option>
<option value="css/funky.css">Funky Highlighting</option>
<option value="css/okaidia.css">Okaidia Highlighting</option>
<option value="css/dark.css">Dark Highlighting</option>
</select>
</li>
</ul>
</div>
</nav>
</header>
);
}
});
module.exports = {
root: '/',
gistApi: 'https://api.github.com',
origin: window.location.origin
};
var React = require("react");
module.exports = React.createClass({
getDefaultProps: function() {
return {
name: '',
lines: [],
comments: [],
onCommentFormOpen: function() {},
onCommentFormCancel: function() {},
onCommentFormSubmit: function() {}
};
},
render: function() {
var rows = [];
var lineCount = this.props.lines.length;
for (var i=0; i < lineCount; i++) {
var num = i + 1;
if (this.props.comments[num] && this.props.comments[num].length > 0) {
rows.push(<Line key={this.props.name + num}
onClick={this.props.onCommentFormOpen}
file={this.props.name}
number={num}
content={this.props.lines[i]}
toggle={LineComments.generateNodeId(this.props.name, num)}/>);
rows.push(<LineComments key={this.props.name + num + 'comments'}
onEditOrReply={this.props.onCommentFormOpen}
onCancel={this.props.onCommentFormCancel}
onSubmit={this.props.onCommentFormSubmit}
file={this.props.name}
number={num}
comments={this.props.comments[num]}/>);
} else {
rows.push(<Line key={this.props.name + num}
onClick={this.props.onCommentFormOpen}
file={this.props.name}
number={num}
content={this.props.lines[i]}/>);
}
}
return (
<section className="code-file-container">
<table id={this.props.name} className="code-file">
<tbody>
<tr className="spacer line">
<td className="line-marker"/>
<td className="line-num"/>
<td className="line-content"/>
</tr>
{rows}
<tr className="spacer line">
<td className="line-marker"/>
<td className="line-num"/>
<td className="line-content"/>
</tr>
</tbody>
</table>
</section>
);
}
});
var Line = React.createClass({
getDefaultProps: function() {
return {
number: 0,
content: '',
file: '',
toggle: false,
onClick: function() {}
}
},
//Lines only need to rerender when a new file is set.
shouldComponentUpdate: function(newProps, newState) {
return this.props.content !== newProps.content ||
this.props.file !== newProps.file ||
this.props.toggle !== newProps.toggle;
},
render: function() {
var toggleCol = this.props.toggle ? <td className="line-marker"><CommentToggle toggle={this.props.toggle}/></td> : <td className="line-marker"/>
return (
<tr id={this.props.file+"-L"+this.props.number} className="line">
{toggleCol}
<td className="line-num">{this.props.number}</td>
<td className="line-content" onClick={this.props.onClick.bind(null, this.props.file, this.props.number, 0)}>
<pre>
<code dangerouslySetInnerHTML={{__html: this.props.content}}/>
</pre>
</td>
</tr>
);
}
});
var LineComments = React.createClass({
statics: {
generateNodeId: function(filename, number) {
return filename + "-C" + number;
}
},
getDefaultProps: function() {
return {
number: 0,
file: '',
comments: [],
onEditOrReply: function() {},
onCancel: function() {},
onSubmit: function() {}
}
},
render: function() {
var comments = this.props.comments.map(function(comment) {
return comment.showForm ?
<CommentForm user={comment.user} text={comment.body} key="comment-form" onCancel={this.props.onCancel} onSubmit={this.props.onSubmit}/> :
<Comment user={comment.user} text={comment.body} key={comment.id} onEditOrReply={this.props.onEditOrReply} id={this.props.file+"-L"+this.props.number+"-C"+comment.id}/>;
}, this);
return (
<tr id={LineComments.generateNodeId(this.props.file, this.props.number)} className="line comment-row">
<td className="line-marker"/>
<td className="line-num"/>
<td className="line-comments">{comments}</td>
</tr>
);
}
});
var Comment = React.createClass({
getDefaultProps: function() {
return {
id: '',
text: '',
user: null,
onEditOrReply: function() {}
};
},
//Comments only need to rerender when new text is set.
shouldComponentUpdate: function(newProps, newState) {
return this.props.text !== newProps.text;
},
render: function() {
return (
<div className="line-comment">
<a className="avatar pull-left" href={this.props.user.html_url}><img src={this.props.user.avatar_url} alt=""/></a>
<div className="pull-left content">
<header className="comment-header">
<a href={this.props.user.html_url}>{this.props.user.login}</a>
</header>
<p className="comment-body">{this.props.text}</p>
</div>
</div>
);
}
});
var CommentForm = React.createClass({
getDefaultProps: function() {
return {
text: '',
user: null,
onSubmit: function() {},
onCancel: function() {}
}
},
render: function() {
return (
<div className="line-comment">
<a className="avatar pull-left" href={this.props.user.html_url}><img src={this.props.user.avatar_url} alt=""/></a>
<div className="pull-left content">
<header className="comment-header">
<a href={this.props.user.html_url}>{this.props.user.login}</a>
</header>
<form action="#" onSubmit={this.props.onSubmit} className="comment-body">
<textarea name="text" placeholder="Enter your comment..." defaultValue={this.props.text}/>
<button type="submit" className="pure-button button-primary">
<i className="fa fa-comment"/> Comment
</button>
<button type="button" className="pure-button button-error" onClick={this.props.onCancel}>
<i className="fa fa-times-circle"/> Cancel
</button>
</form>
</div>
</div>
);
}
});
var CommentToggle = React.createClass({
getInitialState: function() {
return {
display: 'none',
symbolClass: this.props.closeIcon
};
},
getDefaultProps: function() {
return {
toggle: '',
closeIcon: 'fa-comment-o fa-flip-horizontal',
openIcon: 'fa-comment fa-flip-horizontal'
};
},
handleClick: function(event) {
var elm = document.getElementById(this.props.toggle);
var display = elm.style.display;
event.preventDefault();
elm.style.display = this.state.display;
this.setState({
symbolClass: display === 'none' ? this.props.closeIcon : this.props.openIcon,
display: display
});
},
render: function() {
return (
<a href='#' onClick={this.handleClick}>
<i className={"fa " + this.state.symbolClass + " fa-fw"}/>
</a>
);
}
});
var React = require("react");
var Qwest = require("qwest");
var Modal = require("react-modal");
var File = require("./File.js");
var Utils = require("./Utils.js");
var Spinner = require("./Spinner.js");
var Config = require("./Config.js");
Modal.setAppElement(document.getElementById("mount-point"));
Modal.injectCSS();
Qwest.base = Config.gistApi;
module.exports = React.createClass({
getHeaders: function() {
var headers = {};
if (this.state.user !== null)
headers["Authorization"] = 'Basic ' + btoa(this.state.user.login + ':' + this.state.user.password);
return headers;
},
getInitialState: function() {
return {
files: [],
comments: [],
openComment: null,
showLoginModal: false,
user: Utils.getUserFromStorage(sessionStorage),
processing: true
};
},
fetchGist: function(gistId) {
var self = this;
var options = {
headers: this.getHeaders(),
responseType: 'json'
};
Qwest.get('/gists/'+gistId, null, options).then(function(xhr, gist) {
var files = [];
for (var name in gist.files)
files.push(Utils.parseFile(gist.files[name]));
if (self.isMounted())
self.setState({
files: files,
processing: false
});
}).catch(function(xhr, response, e) {
console.log(xhr, response, e);
alert('There was a problem fetching and or parsing this Gist.');
self.setState({processing: false});
});
Qwest.get('/gists/'+gistId+'/comments', null, options).then(function(xhr, comments) {
var parsedComments = {};
var commentCount = comments.length;
for (var i=0; i < commentCount; i++) {
var parsed = Utils.parseComment(comments[i]);
if (parsedComments[parsed.filename] === undefined)
parsedComments[parsed.filename] = [];
if (parsedComments[parsed.filename][parsed.line] === undefined)
parsedComments[parsed.filename][parsed.line] = [];
parsedComments[parsed.filename][parsed.line].push(parsed);
}
if (self.isMounted()) {
self.setState({
comments: parsedComments
});
}
}).catch(function(xhr, response, e) {
console.log(xhr, response, e);
alert('There was a problem fetching the comments for this Gist.');
});
},
componentDidMount: function() {
this.fetchGist(this.props.id);
},
componentWillReceiveProps: function(newProps) {
this.setState({processing: true});
this.fetchGist(newProps.id);
},
componentDidUpdate: function(prevProps, prevState) {
//If there is a hash specified then attempt to scroll there.
if (window.location.hash) {
var elm = document.getElementById(window.location.hash.substring(1));
if (elm)
window.scrollTo(0, elm.offsetTop);
}
},
postGistComment: function(event) {
var text = event.target.children.namedItem("text").value.trim();
var comments = this.state.comments;
var open = this.state.openComment;
var options = {
headers: this.getHeaders(),
dataType: 'json',
responseType: 'json'
};
event.preventDefault();
if (text !== "" && open !== null) {
open.body = text;
open.showForm = false;
comments[open.filename][open.line].splice(open.replyTo, 1, open);
//Send the comment to GitHub. We only need to handle the case where it doesn't make it
Qwest.post('/gists/'+this.props.id+'/comments', {body: Utils.createCommentLink(this.props.id, open.filename, open.line) + ' ' + text}, options);
this.setState({
comments: comments,
openComment: null
});
}
},
insertCommentForm: function(filename, line, replyTo, event) {
var comments = this.state.comments;
var open = this.state.openComment;
var newOpen = Utils.createComment(this.props.id, 0, filename, line, '', this.state.user, replyTo, true);
if (this.state.user === null) {
this.setState({showLoginModal: true});
return;
}
event.preventDefault();
if (open !== null)
comments[open.filename][open.line].splice(open.replyTo, 1);
if (!comments[filename])
comments[filename] = [];
if (!comments[filename][line])
comments[filename][line] = [];
if (open === null || open.filename !== newOpen.filename || open.line !== newOpen.line || open.replyTo !== newOpen.replyTo) {
newOpen.id = comments[filename][line].length;
comments[filename][line].splice(replyTo, 0, newOpen);
} else {
newOpen = null;
}
this.setState({
comments: comments,
openComment: newOpen
});
},
removeCommentForm: function(event) {
var comments = this.state.comments;
var open = this.state.openComment;
event.preventDefault();
if (open !== null) {
comments[open.filename][open.line].splice(open.replyTo, 1);
this.setState({
comments: comments,
openComment: null
});
}
},
handleLogin: function(user) {
this.setState({
user: user,
showLoginModal: false
});
},
closeModal: function(event) {
event.preventDefault();
this.setState({showLoginModal: false});
},
render: function() {
var body = this.state.processing ?
<Spinner/> : this.state.files.map(function(file) {
return <File onCommentFormOpen={this.insertCommentForm}
onCommentFormCancel={this.removeCommentForm}
onCommentFormSubmit={this.postGistComment}
key={file.name}
name={file.name}
lines={file.parsedLines}
comments={this.state.comments[file.name]}/>
}, this);
return (
<div className="container main">
<LoginModal show={this.state.showLoginModal} onSuccess={this.handleLogin} onClose={this.closeModal}/>
{body}
</div>
);
}
});
var LoginModal = React.createClass({
getDefaultProps: function() {
return {
show: false,
onSuccess: function(user) {},
onClose: function() {}
};
},
getInitialState: function() {
return {
processing: false
};
},
attemptLogin: function(event) {
var username = event.target.elements.namedItem("username").value.trim();
var password = event.target.elements.namedItem("password").value;
var store = event.target.elements.namedItem("store").checked;
var self = this;
event.preventDefault();
if (username && password) {
var options = {
headers: {
Authorization: 'Basic ' + btoa(username + ':' + password)
},
responseType: 'json'
};
this.setState({processing: true});
Qwest.get('/user', null, options).then(function(xhr, user) {
user.password = password;
if (store)
Utils.saveUserToStorage(user, sessionStorage);
self.props.onSuccess(user);
}).complete(function(xhr, user) {
self.setState({processing: false});
});
}
},
render: function() {
var form = (
<form className="pure-form pure-form-stacked" onSubmit={this.attemptLogin}>
<fieldset>
<input name="username" className="pure-input-1" type="text" placeholder="GitHub User Name..." required="true"/>
<input name="password" className="pure-input-1" type="password" placeholder="GitHub Password or Token..." required="true"/>
<label><input name="store" type="checkbox"/> Store in Memory</label>
</fieldset>
<fieldset>
<button type="submit" className="pure-button button-primary"><i className="fa fa-save"/> Save</button>
<button className="pure-button button-error" onClick={this.props.onClose}><i className="fa fa-times-circle"/> Cancel</button>
</fieldset>
</form>
);
return (
<Modal isOpen={this.props.show} onRequestClose={this.props.onClose} className="react-modal-content" overlayClassName="react-modal-overlay">
<h2><i className="fa fa-github"/> GitHub Access</h2>
<p>To leave a comment you need to enter your GitHub user name and GitHub password. This is <strong>only</strong> used to post Gist comments to GitHub.</p>
<p>If you prefer not to enter your password you can use a <a target="_blank" href="https://github.com/settings/tokens/new">personal access token</a>. Make sure it has Gist access.</p>
<hr/>
{ this.state.processing ? <Spinner className="fa-github-alt"/> : form }
</Modal>
);
}
});
var React = require("react");
var Router = require('react-router');
module.exports = React.createClass({
mixins: [Router.Navigation],
getDefaultProps: function() {
return {
className: "",
showButton: false
};
},
handleSubmit: function(event) {
event.preventDefault();
var gistId = event.target.elements.namedItem("gistId").value.trim();
if (gistId.length > 0)
this.transitionTo('gist', {gistId: gistId});
},
render: function() {
var body = (
this.props.showButton ?
<fieldset>
<input name="gistId" type="text" className="pure-input-1-3" placeholder="Enter a Gist ID..." required="true"/>&nbsp;
<button type="submit" className="pure-button button-primary">Go <i className="fa fa-sign-in"/></button>
</fieldset>
:
<input className="pure-input-1-3" name="gistId" type="text" placeholder="Enter a Gist ID..." required="true"/>
);
return (
<form className={this.props.className} onSubmit={this.handleSubmit} action="#">{body}</form>
);
}
});
var React = require("react");
module.exports = React.createClass({
getDefaultProps: function() {
return {
className: 'fa-spinner'
};
},
render: function() {
var classes = 'fa ' + this.props.className + ' fa-spin fa-5x';
return (
<p className="spinner"><i className={classes}/></p>
);
}
});
var Prism = require("./Prism.js");
module.exports = {
tokenizeNewLines: function(str) {
var tokens = [];
var strlen = str.length;
var lineCount = 0;
for (var i=0; i < strlen; i++)
{
if (tokens[lineCount])
tokens[lineCount] += str[i];
else
tokens[lineCount] = str[i];
if (str[i] === '\n')
lineCount++;
}
return tokens;
},
tokenize: function(code, lang) {
var processed = [];
var tokens = Prism.tokenize(code, lang);
var token = tokens.shift();
while (token)
{
var isObj = typeof token === 'object';
var lines = this.tokenizeNewLines(isObj ? token.content : token);
var count = lines.length;
for (var i=0; i < count; i++)
{
if (isObj)
processed.push(new Prism.Token(token.type, Prism.util.encode(lines[i]), token.alias));
else
processed.push(Prism.util.encode(lines[i]));
}
token = tokens.shift();
}
return processed;
},
syntaxHighlight: function(code, lang) {
var lineCount = 0;
var lines = [''];
var prismLang = this.getPrismCodeLanguage(lang);
var tokens = this.tokenize(code, prismLang);
var token = tokens.shift();
while (token)
{
code = (typeof token === 'object') ? Prism.Token.stringify(token, prismLang) : token;
lines[lineCount] += code.replace(/\n/g, '');
if (code.indexOf('\n') !== -1)
lines[++lineCount] = '';
token = tokens.shift();
}
return lines;
},
getPrismCodeLanguage: function(gistLang) {
var lang = gistLang.toLowerCase().replace(/#/, 'sharp').replace(/\+/g, 'p');
if (Prism.languages[lang]) {
return Prism.languages[lang];
}
console.log("CodeMana Error - Prism doesn't support the language: "+gistLang+". Help them and us out by adding it http://prismjs.com/");
throw ({msg: "CodeMana Error - Prism doesn't support the language: " + gistLang});
},
getUserFromStorage: function(store) {
return JSON.parse(store.getItem('user'));
},
saveUserToStorage: function(user, store) {
store.setItem('user', JSON.stringify(user));
},
createComment: function(gistId, commentId, filename, lineNumber, commentBody, commentUser, replyTo, showForm) {
return {
gistId: gistId,
id: commentId,
filename: filename,
line: parseInt(lineNumber, 10),
body: commentBody,
user: commentUser,
replyTo: replyTo,
showForm: showForm
};
},
createFile: function(name, parsedLines) {
return {
name: name,
parsedLines: parsedLines
};
},
parseComment: function(comment) {
//Annoyingly I couldn't get a single regex to separate everything out...
var split = comment.body.match(/(\S+)\s(.*)/);
var data = split[1].match(/http:\/\/codemana\.com\/(.*)#(.+)-L(\d+)/);
return data !== null ? this.createComment(data[1], comment.id, data[2], parseInt(data[3], 10), split[2], comment.user, 0, false) : null;
},
parseFile: function(file) {
var lines = this.syntaxHighlight(file.content, file.language);
return this.createFile(file.filename, lines)
},
createCommentLink: function(id, filename, lineNumber) {
return 'http://codemana.com/'+id+'#'+filename+'-L'+lineNumber;
}
};
@jimdoescode
Copy link
Author

jimdoescode commented Sep 5, 2015

@jimdoescode
Copy link
Author

jimdoescode commented Sep 5, 2015

https://codemana.com/4c974cfae29d6a117b2a#Utils.js-L116 Is there some way to make this configurable so that you don't have to hardcode the domain?

@jimdoescode
Copy link
Author

jimdoescode commented Sep 5, 2015

https://codemana.com/4c974cfae29d6a117b2a#AppFooter.js-L6 You should include links to the github repo in the footer.

@jimdoescode
Copy link
Author

jimdoescode commented Feb 15, 2017

@jimdoescode
Copy link
Author

jimdoescode commented Feb 15, 2017

https://codemana.com/4c974cfae29d6a117b2a#App.js-L9 Whoops I wanted to say something different

@jimdoescode
Copy link
Author

@jimdoescode
Copy link
Author

@jimdoescode
Copy link
Author

@jimdoescode
Copy link
Author

jimdoescode commented Nov 27, 2018

http://localhost:3000/4c974cfae29d6a117b2a#App.js-L17 Testing the new comment form edit

testing it now

@jimdoescode
Copy link
Author

@jimdoescode
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment