├── .gitignore ├── README.md ├── data └── generator.py ├── gen_data.sh ├── index.css ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | token 2 | data/*.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A webpage to show the projects you have open sourced in github. You can take a look to the [live demo](http://nekocode.cn/project-gallery). 2 | 3 | ## How to start 4 | 5 | 0. Request a github [access token](https://github.com/settings/tokens) for the generator script. Then excute `echo your_access_token > token`. 6 | 7 | 0. Install python module PyGithub `pip install pygithub`. 8 | 9 | 0. Excute `./gen_data.sh`. It will generate the `data.json` file into your `data` subdirectory. 10 | 11 | 0. Modify the `data.json`. For example, reorder or classify the repositories. 12 | 13 | 0. Now you can open the `index.html` to see the final effect. 14 | 15 | ## Example 16 | 17 | Checkout to the branch [`mine`](https://github.com/nekocode/project-gallery/tree/mine) to see my personal example. 18 | -------------------------------------------------------------------------------- /data/generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from github import Github 4 | import os.path 5 | import getopt 6 | import sys 7 | import json 8 | 9 | 10 | def to_repo(github_repo): 11 | return { 12 | 'type': 'repo', 13 | 'name': github_repo.name, 14 | 'description': github_repo.description, 15 | 'stars': github_repo.stargazers_count, 16 | 'forks': github_repo.forks_count, 17 | 'lang': github_repo.language, 18 | 'url': github_repo.html_url, 19 | } 20 | 21 | 22 | def main(): 23 | def print_usage(): 24 | print('usage: %s --token=GITHUB_TOKEN \n\t (--json=JSON_PATH)' % __file__) 25 | 26 | opts, args = getopt.getopt(sys.argv[1:], '', ['token=', 'json=']) 27 | opts = {opt[0][2:]: opt[1] for opt in opts} # Convert to a dict 28 | 29 | token = opts['token'] 30 | if token is None: 31 | print_usage() 32 | return 33 | 34 | json_file = opts['json'] 35 | if json_file is None: 36 | opts['json'] = 'data.json' 37 | 38 | print('Reading data from github...') 39 | github_user = Github(token).get_user() 40 | github_repos = [r for r in github_user.get_repos('owner')] 41 | github_repos.extend([r for r in github_user.get_repos('member')]) 42 | github_repos = {repo.name: repo for repo in github_repos} # Convert to a dict 43 | 44 | old_dict = {} 45 | if os.path.exists(json_file): 46 | try: 47 | with open(json_file, 'r', encoding='utf-8') as f: 48 | old_dict = json.load(f) 49 | print('Loaded old data from "%s"' % json_file) 50 | except Exception: 51 | old_dict = {} 52 | 53 | print('Restructuring data...') 54 | config = { 55 | 'title': github_user.name, 56 | 'github': github_user.html_url, 57 | 'description': 'Here are the projects I have open sourced.', 58 | 'footer': github_user.name 59 | } 60 | repos = [] 61 | 62 | # Config 63 | if 'config' in old_dict: 64 | old_config = old_dict['config'] 65 | if 'title' in old_config: 66 | config['title'] = old_config['title'] 67 | 68 | config['github'] = github_user.html_url 69 | 70 | if 'description' in old_config: 71 | config['description'] = old_config['description'] 72 | 73 | if 'footer' in old_config: 74 | config['footer'] = old_config['footer'] 75 | 76 | # Repos 77 | if 'repos' in old_dict: 78 | old_repos = old_dict['repos'] 79 | 80 | for repo in old_repos: 81 | if 'type' not in repo or 'name' not in repo: 82 | continue # Skip this broken repo 83 | 84 | t = repo['type'] 85 | if t == 'category': 86 | # Category 87 | repos.append(repo) 88 | continue 89 | 90 | if t != 'repo': 91 | continue # Skip unknown type 92 | 93 | n = repo['name'] 94 | if n not in github_repos: 95 | continue # Skip the removed repo 96 | 97 | repos.append(to_repo(github_repos[n])) 98 | github_repos.pop(n) # Remove from dict 99 | 100 | github_repos = [repo for repo in github_repos.values()] # Dict to list 101 | 102 | if len(github_repos) != 0: 103 | github_repos = sorted(github_repos, key=lambda x: x.stargazers_count, reverse=True) # Sort by stargazers count 104 | 105 | repos.append({ 106 | 'type': 'category', 107 | 'name': 'Uncategorized', 108 | }) 109 | for repo in github_repos: 110 | repos.append(to_repo(repo)) 111 | 112 | # Write to file 113 | print('Saving data to "%s"...' % json_file) 114 | with open(json_file, 'w', encoding='utf-8') as f: 115 | f.write(json.dumps({ 116 | 'config': config, 117 | 'repos': repos 118 | }, indent=4, sort_keys=True)) 119 | 120 | print('Done') 121 | 122 | if __name__ == '__main__': 123 | main() 124 | -------------------------------------------------------------------------------- /gen_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 data/generator.py --token="$(< token)" --json=data/data.json -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | font: inherit; 6 | font-size: 100%; 7 | vertical-align: baseline; 8 | } 9 | 10 | body { 11 | word-wrap: break-word; 12 | font-family: Optima, 'Lucida Sans', Calibri, Candara, Arial, 'source-han-serif-sc', 'Source Han Serif SC', 'Source Han Serif CN', 'Source Han Serif TC', 'Source Han Serif TW', 'Source Han Serif', 'Songti SC', 'Microsoft YaHei', sans-serif; 13 | line-height: 1.5; 14 | color: #24292e; 15 | background-color: #fff; 16 | margin: 0px; 17 | } 18 | 19 | a { 20 | color: #3e8cb7; 21 | text-decoration: none; 22 | } 23 | a:hover { 24 | text-decoration: underline; 25 | cursor: pointer; 26 | } 27 | 28 | .container { 29 | margin: 0 auto; 30 | width: 910px; 31 | } 32 | @media only screen and (max-width:910px) { 33 | .container { 34 | margin: 0 auto; 35 | width: 580px; 36 | } 37 | } 38 | @media only screen and (max-width:580px) { 39 | .container { 40 | margin: 0 auto; 41 | width: 250px; 42 | } 43 | } 44 | 45 | .pagehead { 46 | position: relative; 47 | padding-top: 40px; 48 | padding-bottom: 13px; 49 | border-bottom: 1px solid #e1e4e8; 50 | display: flex; 51 | justify-content: space-between; 52 | } 53 | 54 | .pagehead h1 { 55 | font-size: 24px; 56 | font-weight: 300; 57 | padding-top: 16px; 58 | padding-bottom: 16px; 59 | margin: auto; 60 | width: 100%; 61 | text-align: left; 62 | } 63 | 64 | @media only screen and (max-width:580px) { 65 | .pagehead { 66 | text-align: center; 67 | display: block; 68 | } 69 | .pagehead h1 { 70 | text-align: center; 71 | } 72 | } 73 | 74 | .pagehead a { 75 | margin: auto; 76 | width: 100%; 77 | text-align: right; 78 | } 79 | 80 | .profile { 81 | border-bottom: 1px solid #e1e4e8; 82 | padding-top: 46px; 83 | padding-bottom: 36px; 84 | } 85 | 86 | .profile p { 87 | font-size: 18px; 88 | width: 76%; 89 | margin: auto; 90 | text-align: center; 91 | } 92 | 93 | .profile ul { 94 | width: 76%; 95 | margin: 0 auto; 96 | margin-top: 16px; 97 | list-style: none; 98 | text-align: center; 99 | font-size: 16px; 100 | } 101 | 102 | .profile ul li { 103 | display: inline-block; 104 | position: relative; 105 | margin-left: 16px; 106 | } 107 | 108 | .profile ul li:first-child { 109 | display: inline-block; 110 | position: relative; 111 | margin-left: 0px; 112 | } 113 | 114 | .category { 115 | font-size: 20px; 116 | margin-bottom: 26px; 117 | } 118 | 119 | .category:first-of-type { 120 | margin-top: 46px; 121 | } 122 | 123 | .category:not(:first-of-type):before { 124 | border-top: 1px solid #eff0f1; 125 | content: ""; 126 | display: block; 127 | /* margin-top: -40px; */ 128 | padding-bottom: 36px; 129 | position: relative; 130 | } 131 | 132 | .repo-list { 133 | display: flex; 134 | list-style: none; 135 | flex-wrap: wrap; 136 | margin-left: -40px; 137 | margin-right: -40px; 138 | margin-top: 0; 139 | margin-bottom: 0; 140 | } 141 | 142 | .repo-item { 143 | position: relative; 144 | width: 200px; 145 | padding-left: 25px; 146 | padding-right: 25px; 147 | padding-top: 16px; 148 | padding-bottom: 16px; 149 | margin-left: 40px; 150 | margin-right: 40px; 151 | margin-bottom: 20px; 152 | border-radius: 3px !important; 153 | font-size: 12px; 154 | color: #5e6874; 155 | } 156 | 157 | .repo-item:hover { 158 | cursor: pointer; 159 | background: #f5f5f5; 160 | } 161 | 162 | .repo-item a { 163 | position: absolute; 164 | left: 0; 165 | top: 0; 166 | width: 100%; 167 | height: 100%; 168 | } 169 | 170 | .repo-item h3 { 171 | font-size: 14px; 172 | color: #3e8cb7; 173 | } 174 | 175 | .octicon { 176 | display: inline-block; 177 | vertical-align: text-bottom; 178 | fill: currentColor; 179 | margin-right: 4px; 180 | } 181 | 182 | .repo-info { 183 | margin-top: 6px; 184 | } 185 | 186 | .repo-info span { 187 | margin-right: 12px; 188 | } 189 | 190 | .repo-item p { 191 | margin-top: 4px; 192 | } 193 | 194 | footer { 195 | margin-top: 46px; 196 | background: #f4f5f6; 197 | border-top: 1px solid #eff0f1; 198 | text-align: center; 199 | padding-top:26px; 200 | padding-bottom:26px; 201 | color: #b0b8c1; 202 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Gallery 9 | 10 | 11 | 12 |
13 |

14 | 15 |
16 | 17 |
18 |

19 | 21 |
22 | 23 | 24 |
25 |
26 | 27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var svgStart = ''; 2 | 3 | var svgFork = ''; 4 | 5 | // https://stackoverflow.com/a/2901298/5729581 6 | function numberWithCommas(x) { 7 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 8 | } 9 | 10 | // https://stackoverflow.com/a/4033310/5729581 11 | function httpGetAsync(theUrl, callback) { 12 | var xmlHttp = new XMLHttpRequest(); 13 | xmlHttp.onreadystatechange = function() { 14 | if (xmlHttp.readyState == 4 && xmlHttp.status == 200) 15 | callback(xmlHttp.responseText); 16 | }; 17 | xmlHttp.open("GET", theUrl, true); // true for asynchronous 18 | xmlHttp.send(null); 19 | } 20 | 21 | function addRepo(parentDom, name, stars, forks, lang, description, url) { 22 | var repoItem = document.createElement("li"); 23 | repoItem.className = "repo-item"; 24 | 25 | var header = '' + '

' + name + '

'; 26 | var repoInfo = '
' + svgStart + stars + '' + svgFork + forks + '' + lang + '
'; 27 | var repoDescription = '

' + description + '

'; 28 | repoItem.innerHTML = header + repoInfo + repoDescription; 29 | 30 | parentDom.appendChild(repoItem); 31 | } 32 | 33 | function addCategory(parentDom, name) { 34 | var h2 = document.createElement("h2"); 35 | h2.className = "category"; 36 | h2.id = name; 37 | h2.innerHTML = name; 38 | parentDom.appendChild(h2); 39 | } 40 | 41 | function addCategoryToMenu(parentDom, name) { 42 | var li = document.createElement("li"); 43 | li.innerHTML = '' + name + ''; 44 | parentDom.appendChild(li); 45 | } 46 | 47 | function setHeader(title, githubUrl) { 48 | document.getElementById("title").innerHTML = title; 49 | github = document.getElementById("github"); 50 | github.innerHTML = githubUrl.replace(/(^\w+:|^)\/\//, ''); 51 | github.setAttribute("href", githubUrl); 52 | } 53 | 54 | function setDescription(txt) { 55 | document.getElementById("description").innerHTML = txt; 56 | } 57 | 58 | function setFooter(txt) { 59 | document.getElementById("footer").innerHTML = txt; 60 | } 61 | 62 | function loadData(data) { 63 | var config = data.config; 64 | var repos = data.repos; 65 | if (!config || !repos) return; 66 | 67 | setHeader(config.title, config.github); 68 | setDescription(config.description); 69 | setFooter(config.footer); 70 | 71 | var categoryMenu = document.getElementById("category-menu"); 72 | 73 | var content = document.getElementById("content"); 74 | var item, type, repoCount = 0, listDom; 75 | for (var i = 0; i < repos.length; i++) { 76 | item = repos[i]; 77 | type = item.type; 78 | if (type == "category") { 79 | addCategory(content, item.name); 80 | addCategoryToMenu(categoryMenu, item.name); 81 | repoCount = 0; 82 | 83 | } else if (type == "repo") { 84 | if (repoCount == 0) { 85 | listDom = document.createElement("ol"); 86 | listDom.className = "repo-list"; 87 | content.appendChild(listDom); 88 | } 89 | addRepo(listDom, item.name, numberWithCommas(item.stars), numberWithCommas(item.forks), item.lang, item.description, item.url); 90 | repoCount++; 91 | } 92 | } 93 | } 94 | 95 | httpGetAsync('data/data.json', function(responseText) { 96 | loadData(JSON.parse(responseText)); 97 | }); --------------------------------------------------------------------------------