├── README.md ├── backend ├── call.js ├── daemon.js ├── init.js ├── rustaceans.js └── user.js └── rustaceans.org ├── .babelrc ├── README.md ├── fonts ├── FiraSans-Light.woff ├── FiraSans-Medium.woff └── FiraSans-Regular.woff ├── index.html ├── rustaceans.org.css ├── src ├── api.js ├── home.js ├── index.js ├── search.js └── user.js └── webpack.config.js /README.md: -------------------------------------------------------------------------------- 1 | rustaceans-src 2 | ============== 3 | 4 | This repository is the source code for 5 | [rustaceans.org](http://www.rustaceans.org). To add or modify your data on the 6 | website, you want the [rustaceans.org repo](https://github.com/nrc/rustaceans.org). 7 | 8 | The code is in two parts - `backend` is hosted at www.ncameron.org. The entry 9 | point is `rustaceans.js`. This listens for a GitHub PR hook. It inspects the PR 10 | and merges it then adds the data to an SQLite database. There is also a RESTful 11 | API for getting information about rustaceans from the database. `init.js` is an 12 | alternate stating point which will rebuild the database from the rustaceans.org 13 | repo. This is all node.js stuff. 14 | 15 | The `rustaceans.org` directory contains the frontend; `index.html` is the entry 16 | point. This is a React app and is entirely client-based. It uses the 17 | backend's API to get data. 18 | 19 | ## License 20 | 21 | All files in this repository are copyright 2014-2017 rustaceans.org developers. 22 | 23 | Licensed under the Apache License, Version 2.0 or the MIT license 25 | , at your 26 | option. This file may not be copied, modified, or distributed 27 | except according to those terms. 28 | 29 | -------------------------------------------------------------------------------- /backend/call.js: -------------------------------------------------------------------------------- 1 | // A pathetic wrapper around http API calls. 2 | 3 | var https = require('https'); 4 | var config = require('./config.json'); 5 | 6 | exports.api_call = function(path, f, body, method) { 7 | method = method || 'GET'; 8 | var opts = { 9 | host :"api.github.com", 10 | path : path, 11 | method : method, 12 | body: body, 13 | headers: {'user-agent': config.username, 'Authorization': 'token ' + config.token } 14 | }; 15 | 16 | var request = https.request(opts, function(response) { 17 | let body = ''; 18 | response.on('data',function(chunk) { 19 | body+=chunk; 20 | }); 21 | response.on('end',function() { 22 | //console.log("recevied:") 23 | //console.log(body); 24 | var json = JSON.parse(body); 25 | f(json); 26 | }); 27 | }); 28 | 29 | if (body) { 30 | request.write(JSON.stringify(body)); 31 | } 32 | 33 | request.end(); 34 | } 35 | 36 | exports.graphql_call = function(query, f, variables) { 37 | var opts = { 38 | host :'api.github.com', 39 | path : '/graphql', 40 | method : 'POST', 41 | headers: {'user-agent': config.username, 'Authorization': 'token ' + config.token } 42 | }; 43 | 44 | 45 | var request = https.request(opts, function(response) { 46 | var body = ''; 47 | response.on('data',function(chunk){ 48 | body += chunk; 49 | }); 50 | response.on('end',function(){ 51 | // console.log(body); 52 | if (f) { 53 | try { 54 | var json = JSON.parse(body); 55 | f(json); 56 | } catch (err) { 57 | console.log(err); 58 | console.log(body); 59 | } 60 | } 61 | }); 62 | }); 63 | 64 | var payload = JSON.stringify({ 'query': query, 'variables': variables }); 65 | request.write(payload); 66 | request.end(); 67 | } 68 | 69 | // Leave a comment on an issue or PR. 70 | exports.comment = function(id, comment) { 71 | if (!id) { 72 | return; 73 | } 74 | exports.graphql_call("mutation {\ 75 | addComment(input:{\ 76 | subjectId:\"" + id + "\",\ 77 | body:\"" + comment + "\"\ 78 | }) {\ 79 | subject { id }\ 80 | }\ 81 | }"); 82 | } 83 | 84 | exports.getPrId = function(number, f) { 85 | let query = "query {\ 86 | repository(owner:\"nrc\", name:\"rustaceans.org\") {\ 87 | pullRequest(number:" + number + ") {\ 88 | id\ 89 | }\ 90 | }\ 91 | }"; 92 | exports.graphql_call(query, function(json) { 93 | f(json.data.repository.pullRequest.id); 94 | }); 95 | } 96 | 97 | -------------------------------------------------------------------------------- /backend/daemon.js: -------------------------------------------------------------------------------- 1 | // Code that runs on a regular basis to check for new PRs, check they have come 2 | // from the correct user, merge them. Then update the DB with the contents of 3 | // the PR. 4 | 5 | var call = require('./call.js'); 6 | var user_mod = require("./user.js"); 7 | var config = require('./config.json'); 8 | 9 | exports.process_pr = function(pr) { 10 | if (pr.action != "opened" && pr.action != "synchronize") { 11 | console.log("pr.action: " + pr.action + ", aborting"); 12 | return; 13 | } 14 | pr = pr.pull_request; 15 | var user = pr.user.login; 16 | var pr_number = pr.number; 17 | call.getPrId(pr_number, function(pr_id) { 18 | get_files_changed(pr_id, pr_number, user, function(file) { 19 | if (file.filename == "data/" + user + ".json") { 20 | console.log("Found legit PR for user " + user); 21 | // We pull the file from the PR to make sure it is valid JSON. 22 | call.api_call(config.repo + '/git/blobs/' + file.sha, function(json) { 23 | try { 24 | var buf = new Buffer(json.content, 'base64'); 25 | // We don't use the result, but we want to check the file parses as JSON. 26 | JSON.parse(buf.toString('utf8')); 27 | merge_pr(pr_id, pr_number, user); 28 | } catch (err) { 29 | console.log("error parsing PR: " + pr_number); 30 | call.comment(pr_id, 'There was an error parsing JSON (`' + err + '`), please double check your json file.'); 31 | } 32 | }); 33 | } else { 34 | console.log("PR for user " + user + " changes file " + file.filename); 35 | console.log(" ignored"); 36 | call.comment(pr_id, 'Error trying to merge. This PR modifies more than just your details. Please only add, modify, or remove the file `data/' + user + '.json`.'); 37 | } 38 | }); 39 | }); 40 | }; 41 | 42 | function merge_pr(pr_id, pr_number, user) { 43 | call.api_call(config.repo + '/pulls/' + pr_number + '/merge', function(json) { 44 | if (!json.merged) { 45 | console.log("merging PR " + pr_number + " for " + user + " failed: " + json.message); 46 | call.comment(pr_id, 'Merging this PR failed\n\nping @nrc\n\nreason: '+ json.message); 47 | } else { 48 | console.log("merged PR " + pr_number + " for " + user); 49 | user_mod.process_user(user, pr_id); 50 | } 51 | }, 52 | { 'commit_message': "Merging PR " + pr_number + " from user " + user }, 53 | 'PUT'); 54 | } 55 | 56 | function get_files_changed(pr_id, pr_number, user, do_file) { 57 | call.api_call(config.repo + '/pulls/' + pr_number + '/files', function(json) { 58 | var files = []; 59 | json.forEach(function(f){ 60 | files.push(f); 61 | }); 62 | if (files.length == 1) { 63 | do_file(files[0]); 64 | } else { 65 | console.log(files.length + " changed files"); 66 | call.comment(pr_id, 'Error trying to merge. This PR modifies multiple files. Please only add, modify, or remove the file `' + user + '.json`.'); 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /backend/init.js: -------------------------------------------------------------------------------- 1 | // Initialise the DB - create the tables and do a first update from the repo. 2 | 3 | // To setup environment: 4 | // sudo apt-get install node 5 | // npm install sqlite3 6 | // npm install async 7 | // npm install marked 8 | 9 | 10 | var sqlite = require("sqlite3"); 11 | var call = require('./call.js'); 12 | var user_mod = require("./user.js"); 13 | var config = require('./config.json'); 14 | 15 | 16 | var db = user_mod.openDb(); 17 | console.log("constructing tables"); 18 | db.serialize(function() { 19 | // First empty the DB in case it previously existed. 20 | db.run("DROP TABLE people", err_handler); 21 | db.run("DROP TABLE people_channels", err_handler); 22 | 23 | // Create tables. 24 | db.run("CREATE TABLE people(username STRING PRIMARY KEY,\ 25 | name STRING,\ 26 | irc STRING,\ 27 | show_avatar BOOL,\ 28 | email STRING,\ 29 | discourse STRING,\ 30 | reddit STRING,\ 31 | twitter STRING,\ 32 | blog STRING,\ 33 | website STRING,\ 34 | notes STRING,\ 35 | blob STRING)", err_handler); 36 | db.run("CREATE TABLE people_channels(person STRING, channel STRING)", err_handler); 37 | 38 | // Fill the tables from the repo. 39 | var files = process_repo(); 40 | }); 41 | 42 | function process_repo() { 43 | var queryGetDataList = 'query {\ 44 | repository(owner:"nrc", name:"rustaceans.org") {\ 45 | object(expression: "master:data") {\ 46 | ... on Tree {\ 47 | entries {\ 48 | name,\ 49 | oid\ 50 | }\ 51 | }\ 52 | }\ 53 | }\ 54 | }'; 55 | call.graphql_call(queryGetDataList, function(json) { 56 | let entries = json.data.repository.object.entries; 57 | processEntries(entries); 58 | }); 59 | } 60 | 61 | function processEntries(entries) { 62 | if (entries.length == 0) { 63 | db.close(); 64 | return ; 65 | } 66 | let entry = entries.pop(); 67 | processEntry(entry, entries); 68 | } 69 | 70 | var queryGetUser = 'query GetUser($id: GitObjectID) {\ 71 | repository(owner:"nrc", name:"rustaceans.org") {\ 72 | object(oid: $id) {\ 73 | ... on Blob {\ 74 | text\ 75 | }\ 76 | }\ 77 | }\ 78 | }'; 79 | 80 | function processEntry(entry, entries) { 81 | if (str_endswith(entry.name, '.json') && entry.name != 'template.json') { 82 | call.graphql_call(queryGetUser, function(userJson) { 83 | try { 84 | let text = userJson.data.repository.object.text; 85 | if (!text) { 86 | console.log("no text: " + entry.name); 87 | processEntries(entries); 88 | return; 89 | } 90 | 91 | let user = JSON.parse(text); 92 | let username = entry.name.substring(0, entry.name.length - 5); 93 | user_mod.add_user(user, username, null, db, function() { 94 | processEntries(entries); 95 | }); 96 | } catch(err) { 97 | console.log("error: " + err); 98 | console.log("in " + entry.name); 99 | console.log(userJson); 100 | processEntries(entries); 101 | } 102 | }, { "id": entry.oid }); 103 | } else { 104 | processEntries(entries); 105 | } 106 | } 107 | 108 | function str_endswith(str, suffix) { 109 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 110 | } 111 | 112 | function err_handler(x) { 113 | if (x == null) { 114 | return; 115 | } 116 | 117 | console.log("an error occured: " + x); 118 | } 119 | -------------------------------------------------------------------------------- /backend/rustaceans.js: -------------------------------------------------------------------------------- 1 | // A simple web server providing a RESTful API for Rustaceans data. Allows 2 | // getting a single user or searching for users. 3 | 4 | var sqlite = require('sqlite3'); 5 | var async = require('async'); 6 | 7 | var config = require('./config.json'); 8 | 9 | var daemon = require('./daemon.js'); 10 | 11 | const express = require('express'); 12 | const app = express(); 13 | 14 | app.get('/user', function (req, res) { 15 | get_user(req.query.username, res); 16 | }); 17 | 18 | app.get('/search', function (req, res) { 19 | search(req.query.for , res); 20 | }); 21 | 22 | app.post('/pr', function (req, res) { 23 | // Get payload and parse it 24 | var body = ''; 25 | req.on('data',function(chunk){ 26 | body+=chunk; 27 | }); 28 | req.on('end',function(){ 29 | res.status(200); 30 | res.set("Access-Control-Allow-Origin", "*"); 31 | try { 32 | var json = JSON.parse(body); 33 | daemon.process_pr(json); 34 | res.send("Success?"); 35 | } catch(e) { 36 | res.send("Error: " + e); 37 | } 38 | }); 39 | }); 40 | 41 | app.get('/random', function (req, res) { 42 | // Return a random rustacean. 43 | make_random_user(function (username) { get_user(username, res) } ); 44 | }); 45 | 46 | app.get('*', function (req, res) { 47 | res.status(404); 48 | res.set("Content-Type", "text/plain"); 49 | res.send("404 Not Found\n"); 50 | }); 51 | 52 | app.listen(2345); 53 | 54 | function get_channels(username, res, db, callback) { 55 | db.all('SELECT * FROM people_channels WHERE person=?;', username, function(err, rows) { 56 | if (err) { 57 | console.log("an error occured searching for irc channels for " + username + ": " + err); 58 | make_response(res, [], db); 59 | return; 60 | } 61 | 62 | callback(rows); 63 | }); 64 | } 65 | 66 | function make_random_user(mkusr) { 67 | var db = new sqlite.Database("rustaceans.db"); 68 | db.all('SELECT username FROM people;', function(err, rows) { 69 | if (err) { 70 | console.log("an error occured while looking up usernames: " + err); 71 | make_response(res, [], db); 72 | return; 73 | } 74 | 75 | if (!rows) { 76 | console.log("no resuls while looking up usernames: " + err); 77 | make_response(res, [], db); 78 | return; 79 | } 80 | 81 | var username = rows[Math.floor(Math.random() * rows.length)].username; 82 | mkusr(username); 83 | }); 84 | } 85 | 86 | // Search for users, returns a possibly empty array of user json objects. 87 | function search(search_str, res) { 88 | var db = new sqlite.Database("rustaceans.db"); 89 | db.all("SELECT * FROM people WHERE blob LIKE ?", "%" + search_str + "%", function(err, rows) { 90 | if (err) { 91 | console.log("an error occured while searching for '" + search_str + "': " + err); 92 | make_response(res, [], db); 93 | return; 94 | } 95 | 96 | // This nasty bit of callback hell is for finding each users irc channels. 97 | // For each user we look up the channels from the DB, then create the user object. 98 | // Once we have all the user objects in an array, then we put them in the repsonse. 99 | async.parallel(rows.map(function(row) { 100 | return function(callback) { 101 | get_channels(row.username, res, db, function(rows) { 102 | callback(null, make_user(row, rows)); 103 | }) 104 | }; 105 | }), 106 | function(err, result) { 107 | make_response(res, result, db); 108 | }); 109 | }); 110 | } 111 | 112 | // Make a response of info for a single user. Returns either a single user JSON object or 113 | // an empty JSON object if there is no such user. 114 | function get_user(username, res) { 115 | // Check the db for the user. 116 | var db = new sqlite.Database("rustaceans.db"); 117 | db.get('SELECT * FROM people WHERE username=? COLLATE NOCASE;', username, function(err, row) { 118 | if (err || !row) { 119 | if (err) { 120 | console.log("an error occured searching for user " + username + ": " + err); 121 | } 122 | make_response(res, [], db); 123 | return; 124 | } 125 | 126 | get_channels(username, res, db, function(rows) { 127 | make_response(res, [make_user(row, rows)], db); 128 | }); 129 | }); 130 | } 131 | 132 | // Turn data into JSON and add it to the response 133 | function make_response(res, data, db) { 134 | res.status(200); 135 | res.set("Access-Control-Allow-Origin", "*"); 136 | res.json(data); 137 | db.close(); 138 | } 139 | 140 | // Does not include username. 141 | // Fields that can be directly copied to the user object from the DB. 142 | var fields = [ 143 | "username", 144 | "name", 145 | "irc", 146 | "email", 147 | "discourse", 148 | "reddit", 149 | "twitter", 150 | "blog", 151 | "website", 152 | "notes" 153 | ]; 154 | 155 | 156 | // Take rows from the db and make a JS object representing the user. 157 | function make_user(user_row, channel_rows) { 158 | var user = {}; 159 | 160 | fields.forEach(function(f) { 161 | user[f] = user_row[f]; 162 | }); 163 | 164 | if (user_row.show_avatar) { 165 | user['avatar'] = 'https://avatars.githubusercontent.com/' + user_row['username']; 166 | } 167 | 168 | if (channel_rows) { 169 | user['irc_channels'] = channel_rows.map(function(cr) { return cr.channel; }); 170 | } else { 171 | user['irc_channels'] = []; 172 | } 173 | return user; 174 | } 175 | -------------------------------------------------------------------------------- /backend/user.js: -------------------------------------------------------------------------------- 1 | // Process a user from a file in the repo to an entry in the db. 2 | 3 | var sqlite = require("sqlite3"); 4 | var marked = require("marked"); 5 | marked.options({sanitize: true}); 6 | var call = require('./call.js'); 7 | var config = require('./config.json'); 8 | 9 | exports.process_user = function(username, pr_id) { 10 | let db = exports.openDb(); 11 | call.api_call(config.repo + '/contents/data/' + username + '.json', function(json) { 12 | if (!json || json.type == undefined) { 13 | // Remove the user from the db. 14 | insert_to_db({'username': username}, db, function() { 15 | call.comment(pr_id, 'Success, you have been removed from rustaceans.org (I\'d recommend you check though, and file an issue if there was a problem).'); 16 | db.close(); 17 | }); 18 | 19 | return; 20 | } 21 | if (json.type == 'file' && json.content) { 22 | var buf = new Buffer(json.content, 'base64'); 23 | try { 24 | var user_info = JSON.parse(buf.toString('utf8')); 25 | exports.add_user(user_info, username, pr_id, db, function() { db.close(); }); 26 | } catch (err) { 27 | console.log("error parsing user: " + username + ": " + err); 28 | call.comment(pr_id, 'There was an error parsing JSON (`' + err + '`), please double check your json file and re-submit the PR. If you think it\'s good, ping @nrc.'); 29 | db.close(); 30 | } 31 | } else { 32 | console.log("unexpected contents for " + username + ": " + json.type); 33 | call.comment(pr_id, 'There was an error parsing JSON (unexpected contents), please double check your json file and re-submit the PR. If you think it\'s good, ping @nrc.'); 34 | db.close(); 35 | } 36 | }); 37 | } 38 | 39 | exports.openDb = function() { 40 | return new sqlite.Database("rustaceans.db"); 41 | } 42 | 43 | exports.add_user = function(user, username, pr_id, db, callback) { 44 | user['username'] = username; 45 | insert_to_db(user, db, function() { 46 | console.log("inserted into db: " + username); 47 | call.comment(pr_id, 'Success, the rustaceans db has been updated. You can see your details at http://www.rustaceans.org/' + username + '.'); 48 | callback(); 49 | }); 50 | } 51 | 52 | // Fields to read from the json file and insert into the db 53 | // Does not include irc_channels nor the blob. 54 | var fields = [ 55 | ["username", true], 56 | ["name", true], 57 | ["irc", true], 58 | ["show_avatar", false], 59 | ["email", true], 60 | ["discourse", true], 61 | ["reddit", true], 62 | ["twitter", true], 63 | ["blog", true], 64 | ["website", true], 65 | ["notes", true] 66 | ]; 67 | 68 | function insert_to_db(user_info, db, callback) { 69 | // compute the blob and an input string for each field 70 | var strings = 'INSERT INTO people ('; 71 | var values_str = ') VALUES ('; 72 | var values = [] 73 | var blob = ''; 74 | var first = true; 75 | var field_count = 0; 76 | 77 | fields.forEach(function(f) { 78 | var field = f[0]; 79 | 80 | if (!(field in user_info)) { 81 | return; 82 | } 83 | 84 | var value = user_info[field]; 85 | field_count += 1; 86 | 87 | if (field == 'twitter' && value && value[0] != '@') { 88 | value = '@' + value; 89 | } 90 | if (field == 'notes') { 91 | // Convert notes from markdown to html. 92 | value = marked(value); 93 | } else if (f[1] && value.length > 0) { 94 | blob += value + "\n" 95 | } 96 | 97 | if (first) { 98 | first = false; 99 | } else { 100 | strings += ', '; 101 | values_str += ', '; 102 | } 103 | strings += field; 104 | values_str += "?"; 105 | values.push(value); 106 | }); 107 | 108 | strings += ', blob'; 109 | values_str += ", ?"; 110 | values.push(blob); 111 | strings += values_str + ');'; 112 | 113 | // Write everything to the DB 114 | db.serialize(function() { 115 | db.run('DELETE FROM people WHERE username=?;', user_info['username'], err_handler); 116 | db.run('DELETE FROM people_channels WHERE person=?;', user_info['username'], err_handler); 117 | 118 | if (field_count <= 1) { 119 | callback(); 120 | return; 121 | } 122 | 123 | db.run(strings, values, err_handler); 124 | 125 | // irc channels go into a separate table 126 | var irc_string = 'INSERT INTO people_channels (person, channel) VALUES (?, ?);' 127 | var channels = user_info['irc_channels']; 128 | if (channels) { 129 | channels.forEach(function(ch) { 130 | db.run(irc_string, user_info['username'], ch, err_handler); 131 | }); 132 | } 133 | callback(); 134 | }); 135 | } 136 | 137 | function err_handler(x) { 138 | if (x == null) { 139 | return; 140 | } 141 | 142 | console.log("an error occured: " + x); 143 | } 144 | -------------------------------------------------------------------------------- /rustaceans.org/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["env", { 5 | "targets": { 6 | "browsers": ["last 2 versions", "safari >= 10"] 7 | } 8 | }] 9 | ], 10 | "plugins": ["transform-object-rest-spread"] 11 | } 12 | -------------------------------------------------------------------------------- /rustaceans.org/README.md: -------------------------------------------------------------------------------- 1 | # rustaceans.org frontend 2 | 3 | ## building 4 | 5 | ``` 6 | npm install 7 | 8 | npm install --save react react-dom react-router-dom 9 | npm install --save-dev babel-loader babel-core 10 | npm install --save-dev babel-preset-react 11 | npm install --save-dev babel-preset-es2015 12 | npm install --save-dev babel-preset-env 13 | npm install --save-dev babel-plugin-transform-object-rest-spread 14 | npm install --save-dev webpack webpack-dev-server 15 | 16 | ./node_modules/.bin/webpack --watch 17 | ``` 18 | -------------------------------------------------------------------------------- /rustaceans.org/fonts/FiraSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-unofficial/rustaceans-src/71bccda1c744d67179adbbb01bce40478b3d6636/rustaceans.org/fonts/FiraSans-Light.woff -------------------------------------------------------------------------------- /rustaceans.org/fonts/FiraSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-unofficial/rustaceans-src/71bccda1c744d67179adbbb01bce40478b3d6636/rustaceans.org/fonts/FiraSans-Medium.woff -------------------------------------------------------------------------------- /rustaceans.org/fonts/FiraSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-unofficial/rustaceans-src/71bccda1c744d67179adbbb01bce40478b3d6636/rustaceans.org/fonts/FiraSans-Regular.woff -------------------------------------------------------------------------------- /rustaceans.org/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rustaceans 6 | 7 | 8 | 9 | 10 | 11 |
12 | Rust logo 13 |
14 |

15 | This website was for finding Rustaceans. 16 | I'm sorry for anyone using it, but I no longer have the time to maintain it. 17 | You can find all the backing data in this GitHub repo. 18 |

19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /rustaceans.org/rustaceans.org.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Fira Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Fira Sans Light'), url("https://www.rustaceans.org/fonts/FiraSans-Light.woff") format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Fira Sans'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Fira Sans'), url("https://www.rustaceans.org/fonts/FiraSans-Regular.woff") format('woff'); 12 | } 13 | @font-face { 14 | font-family: 'Fira Sans'; 15 | font-style: normal; 16 | font-weight: 500; 17 | src: local('Fira Sans Medium'), url("https://www.rustaceans.org/fonts/FiraSans-Medium.woff") format('woff'); 18 | } 19 | 20 | img.logo { 21 | margin: 20px; 22 | max-width: 100%; 23 | } 24 | img.avatar { 25 | margin: 0px; 26 | max-width: 100%; 27 | border-radius: 4px; 28 | } 29 | @media (min-width: 500px) { 30 | img.logo { 31 | float: left; 32 | } 33 | img.avatar { 34 | float: right; 35 | } 36 | } 37 | div.clear { 38 | clear: both; 39 | } 40 | div.headsearch { 41 | padding: 20px; 42 | } 43 | .searchnotes { 44 | font-size: 0.85em; 45 | margin-top: -0.3em; 46 | } 47 | .searchbox { 48 | width: 250px; 49 | } 50 | body { 51 | font-family: 'Fira Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 52 | margin-top: 20px; 53 | margin-bottom: 10px; 54 | max-width: 820px; 55 | } 56 | @media (min-width: 992px) { 57 | body { 58 | margin-left: 18%; 59 | } 60 | } 61 | p { 62 | margin-top: 1.2em; 63 | margin-bottom: 1.2em; 64 | } 65 | p.narrow { 66 | max-width: 640px; 67 | } 68 | li { 69 | max-width: 580px; 70 | } 71 | span.name { 72 | font-weight: 500; 73 | font-size: 1.2em; 74 | } 75 | div.name { 76 | margin-top: 0em; 77 | margin-bottom: 1em; 78 | } 79 | div.row { 80 | margin-bottom: 0.3em; 81 | } 82 | span.key { 83 | width: 200px; 84 | display: inline-block; 85 | } 86 | div.searchresult { 87 | border: solid 1px #CCC; 88 | margin-top: 40px; 89 | margin-bottom: 40px; 90 | padding: 20px 10px 20px 10px; 91 | max-width: 600px; 92 | border-radius: 4px; 93 | background-color: #F7F7F7; 94 | } 95 | a { 96 | color: #428bca; 97 | text-decoration: none; 98 | } 99 | a:hover, 100 | a:focus { 101 | color: #2a6496; 102 | text-decoration: underline; 103 | } 104 | a:focus { 105 | outline: thin dotted; 106 | outline-offset: -2px; 107 | } 108 | .pitch b { 109 | font-weight: 400; 110 | } 111 | li { 112 | margin-bottom: .5em; 113 | } 114 | h1 { 115 | font-size: 2em; 116 | margin-top: 1.2em; 117 | margin-bottom: 0.5em; 118 | } 119 | 120 | ul { 121 | padding-left: 30px; 122 | } 123 | 124 | h2 { 125 | font-weight: 500; 126 | font-size: 18.5px; /* gridfit */ 127 | } 128 | h3 { 129 | font-weight: 500; 130 | } 131 | 132 | 133 | 134 | p.pitch { 135 | font-size: 24px; 136 | font-weight: 300; 137 | text-align: center; 138 | } 139 | @media (min-width: 992px) { 140 | p.pitch { 141 | font-size: 24px; 142 | margin-top: 1em; 143 | margin-bottom: 0.5em; 144 | margin-right: 1em; 145 | text-align: left; 146 | } 147 | } 148 | 149 | img { 150 | border: 0; 151 | } 152 | .container { 153 | padding-right: 15px; 154 | padding-left: 15px; 155 | margin-right: auto; 156 | margin-left: auto; 157 | } 158 | @media (min-width: 768px) {.container { 159 | width: 750px; 160 | }} 161 | @media (min-width: 992px) {.container { 162 | width: 970px; 163 | }} 164 | @media (min-width: 1200px) {.container { 165 | width: 1170px; 166 | }} 167 | -------------------------------------------------------------------------------- /rustaceans.org/src/api.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SearchResults } from './search'; 3 | 4 | const API_URL = 'https://www.ncameron.org/rustaceans/'; 5 | 6 | export class FetchSearch extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { results: null }; 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | const thisKey = this.props.getKey(this.props); 14 | const nextKey = this.props.getKey(nextProps); 15 | if (thisKey != nextKey) { 16 | this.setState( { results: null } ); 17 | } 18 | this.fetchData(nextKey); 19 | } 20 | 21 | componentWillMount() { 22 | this.fetchData(this.props.getKey(this.props)); 23 | }; 24 | 25 | fetchData(key) { 26 | const self = this; 27 | let url = API_URL + self.props.endPoint; 28 | if (key) { 29 | url += '=' + key; 30 | } 31 | // console.log("fetching " + url); 32 | fetch(url).then(function(response) { 33 | return response.json(); 34 | }).then(function(results) { 35 | self.setState( { results } ); 36 | }); 37 | } 38 | 39 | render() { 40 | if (!this.state.results) { 41 | return
loading...
; 42 | } else { 43 | return ; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rustaceans.org/src/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { SearchBox } from './search'; 4 | 5 | export const Home = (props) => ( 6 |
7 | Rust logo 8 |
9 |
10 |

11 | This website is for finding Rustaceans. 12 | Wondering who is behind that GitHub username or IRC nick? Here is where to find out (search at the top of the page). 13 |

14 | 15 |

16 | Rustaceans are people who use Rust, contribute to Rust, or are interested in the development of Rust. 17 |

18 |

19 | Rust is a systems programming language that runs blazingly fast, prevents almost all crashes, and eliminates data races. 20 |

21 |

22 | If you want to add or edit your details on rustaceans.org, you can send a pull request to the rustaceans.org repo. It's very simple and there is no new username/password. See the readme in the repo for details. 23 |

24 |

See a random rustacean.

25 | 26 |
27 | Rustaceans communicate via many channels: 28 |
    29 |
  • Discourse (users): for discussing using and learning Rust.
  • 30 |
  • Discourse (internals): for discussion of Rust language design and implementation. And bike-shedding.
  • 31 |
  • Reddit: for general Rust discussion.
  • 32 |
  • Rust Discord: heavily inspired on the IRC, but with support for code highlighting, Rust emojis and voice chat.
  • 33 |
  • IRC on Moznet: 34 |
      35 |
    • #rust is for all things Rust;
    • 36 |
    • #rust-internals is for discussion of other Rust implementation topics;
    • 37 |
    • #rustc is for discussion of the implementation of the Rust compiler;
    • 38 |
    • #rust-lang is for discussion of the design of the Rust language;
    • 39 |
    • #rust-libs is for discussion of the implementation of the Rust standard libraries;
    • 40 |
    • #rust-tools is for discussion of Rust tools;
    • 41 |
    • #rust-gamedev is for people doing game development in Rust;
    • 42 |
    • #rust-crypto is for discussion of cryptography in Rust;
    • 43 |
    • #rust-osdev is for people doing OS development in Rust;
    • 44 |
    • #rust-webdev is for people doing web development in Rust;
    • 45 |
    • #rust-networking is for people doing computer network development and programming in Rust;
    • 46 |
    • #cargo is for discussion of Cargo, Rust's package manager;
    • 47 |
    • #rust-offtopic is for general chit-chat amongst Rustaceans;
    • 48 |
    • #servo is for discussion of Servo, the browser engine written in Rust;
    • 49 |
    • #rust-bots notifcations about Rust from a selection of bots.
    • 50 |
    51 |
  • 52 |
53 |
54 |
55 | ); 56 | -------------------------------------------------------------------------------- /rustaceans.org/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 4 | import { Home } from './home'; 5 | import { DoSearch } from './search'; 6 | import { User, RandomUser } from './user'; 7 | 8 | const App = () => ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export function renderApp() { 20 | ReactDOM.render( 21 | , 22 | document.getElementById('container') 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /rustaceans.org/src/search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, Redirect } from 'react-router-dom'; 3 | import { Card } from './user'; 4 | import { FetchSearch } from './api'; 5 | 6 | export class SearchBox extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { doSearch: null }; 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | this.setState({ doSearch: null }); 14 | } 15 | 16 | render() { 17 | if (this.state.doSearch) { 18 | return ; 19 | } 20 | 21 | const enterKeyCode = 13; 22 | const self = this; 23 | const onKeyPress = (e) => { 24 | if (e.which === enterKeyCode) { 25 | self.setState({ doSearch: e.currentTarget.value }); 26 | } 27 | }; 28 | 29 | return
30 |

31 | Search for a Rustacean: 32 | 33 |

34 |

(by name, irc nick, username for Reddit, GitHub, Discourse, etc.)

35 |
; 36 | } 37 | }; 38 | 39 | export const DoSearch = (props) => { 40 | const getKey = (ps) => ps.match.params.needle; 41 | return ; 42 | }; 43 | 44 | export const SearchResults = (props) => { 45 | let results = []; 46 | for (const result of props.results) { 47 | results.push(); 48 | } 49 | 50 | return
51 | {results} 52 | 53 | <<< back to rustaceans.org front page 54 |
; 55 | }; 56 | -------------------------------------------------------------------------------- /rustaceans.org/src/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { FetchSearch } from './api'; 4 | 5 | export const User = (props) => { 6 | const getKey = (ps) => ps.match.params.user; 7 | return ; 8 | }; 9 | 10 | export const RandomUser = (props) => { 11 | const getKey = () => null; 12 | return ; 13 | }; 14 | 15 | export const Card = (props) => { 16 | let avatar = null; 17 | if (props.avatar) { 18 | avatar = ; 19 | } 20 | 21 | let name = null; 22 | if (props.name) { 23 | name =
{props.name}
; 24 | } 25 | 26 | let irc = null; 27 | if (props.irc) { 28 | let channelsSpan = null; 29 | if (props.irc_channels && props.irc_channels.length) { 30 | let channels = []; 31 | for (const chan of props.irc_channels) { 32 | channels.push( #{chan}); 33 | } 34 | channelsSpan = on {channels}; 35 | } 36 | irc =
irc nick{props.irc}{channelsSpan}
37 | } 38 | let discourse = null; 39 | if (props.discourse) { 40 | discourse =
discourse username{props.discourse}
41 | } 42 | let reddit = null; 43 | if (props.reddit) { 44 | reddit =
reddit username{props.reddit}
45 | } 46 | let twitter = null; 47 | if (props.twitter) { 48 | twitter =
twitter username{props.twitter}
49 | } 50 | let website = null; 51 | if (props.website) { 52 | website = 53 | } 54 | let blog = null; 55 | if (props.blog) { 56 | blog = 57 | } 58 | let email = null; 59 | if (props.email) { 60 | email = 61 | } 62 | 63 | 64 | let notes = null; 65 | if (props.notes) { 66 | notes =
; 67 | } 68 | 69 | return
70 | {avatar} 71 | {name} 72 |
GitHub username{props.username}
73 | {irc} 74 | {discourse} 75 | {reddit} 76 | {twitter} 77 | {website} 78 | {blog} 79 | {email} 80 |
81 | {notes} 82 |
; 83 | }; 84 | -------------------------------------------------------------------------------- /rustaceans.org/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/index.js", 3 | output: { 4 | filename: "./out/rustaceans.out.js", 5 | libraryTarget: 'var', 6 | library: 'Rustaceans' 7 | }, 8 | module: { 9 | loaders: [ 10 | { 11 | test: /\.js$/, 12 | exclude: /node_modules/, 13 | loader: 'babel-loader' 14 | }] 15 | }, 16 | devServer: { 17 | contentBase: '.', 18 | historyApiFallback: true 19 | }, 20 | devtool: 'source-map' 21 | } 22 | --------------------------------------------------------------------------------